diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index 41265897..dc2135a8 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -554,6 +554,9 @@ set(SOURCE_FILES ${SOURCE_FILES} games/pc/pc.cpp games/pc/io.cpp games/pc/bi2x_hook.cpp + games/udn/udn.cpp + games/udn/io.cpp + games/udn/bi2x_hook.cpp # hooks hooks/audio/acm.cpp @@ -605,6 +608,7 @@ set(SOURCE_FILES ${SOURCE_FILES} hooks/graphics/backends/d3d9/d3d9_texture.cpp hooks/graphics/backends/d3d11/d3d11_backend.cpp hooks/graphics/backends/d3d11/d3d11_swapchain.cpp + hooks/graphics/backends/d3d11/d3d11_swapchain_udn.cpp hooks/graphics/backends/d3d11/d3d11_factory.cpp hooks/graphics/backends/d3d11/d3d11_vtable_capture.cpp hooks/graphics/backends/d3d11/d3d11_screenshot.cpp diff --git a/src/spice2x/easrv/easrv.cpp b/src/spice2x/easrv/easrv.cpp index c00f7eac..de17c39a 100644 --- a/src/spice2x/easrv/easrv.cpp +++ b/src/spice2x/easrv/easrv.cpp @@ -573,6 +573,13 @@ static inline void easrv_init_messages() { "\x5b\x99\xa1\x15\x78\x16\x84\xe0\x67\x2c\x75\xf7\xc0\x38\xd5\x20\xf7\x81\xae\x1f\x77\x02\xdf\x0c" "\x60\x68\x2e\x26\xe5\xbb\xb7\xb4\x41\x72\xc4\xa4\x43\x2a\xc8\x81\xd3\x8a\x1f\xe8\x94\x39\x7e\xf6" "\x69\x56\x9e\xaf\x06\x10\xfd\x47\x69\xb8\x14\xe6\x48\x36\xd6\x25\x18\x0c\x2b\x3d\x69"); + if (avs::game::is_model("UDN")) { + // Only UDN needs an ASCII location/name for the Unity score-screen marshaler. + // These 12 encoded bytes replace the name with "SPICE2 LOCAL"; + // its terminator, the other fields and the response length stay unchanged. + EA_FACILITY_GET.replace(311, 12, + "\xf2\x20\xcc\x9a\xe3\x32\x78\x80\x9a\xba\xcd\xc3"); + } EA_PCBEVENT_PUT = std::string( "\x17\x7e\x7c\x0e\x1a\x15\x41\x0b\xae\xd5\xa0\xf5\x2c\xa1\xb9\xd2\x53\xca\xd2\x0d\x81\x34\xdb\x94\x79\xd0" "\xc8\x9d\xb5\xfb\xef\x97"); diff --git a/src/spice2x/games/io.cpp b/src/spice2x/games/io.cpp index 40b9ac18..1a161b19 100644 --- a/src/spice2x/games/io.cpp +++ b/src/spice2x/games/io.cpp @@ -37,6 +37,7 @@ #include "qks/io.h" #include "mfg/io.h" #include "pc/io.h" +#include "udn/io.h" namespace games { @@ -332,6 +333,14 @@ namespace games { analogs.insert({ pc, pc::get_analogs() }); lights.insert({ pc, pc::get_lights() }); file_hints[pc].push_back({"game/svm.exe"}); + + // DANCE aROUND + const std::string udn("DANCE aROUND"); + games.push_back(udn); + buttons.insert({ udn, udn::get_buttons() }); + buttons_help.insert({ udn, udn::get_buttons_help() }); + lights.insert({ udn, udn::get_lights() }); + file_hints[udn].push_back({"game/dancearound_Data"}); } const std::vector &get_games() { diff --git a/src/spice2x/games/udn/bi2x_hook.cpp b/src/spice2x/games/udn/bi2x_hook.cpp new file mode 100644 index 00000000..6e10e305 --- /dev/null +++ b/src/spice2x/games/udn/bi2x_hook.cpp @@ -0,0 +1,686 @@ +#include "bi2x_hook.h" + +#if SPICE64 + +#include +#include +#include +#include +#include + +#include "acio/icca/icca.h" +#include "games/io.h" +#include "io.h" +#include "misc/eamuse.h" +#include "rawinput/rawinput.h" +#include "util/execexe.h" +#include "util/logging.h" +#include "util/tapeled.h" +#include "util/utils.h" + +namespace games::udn { + + struct AIO_SCI_COMM { + uint8_t data[0xf8]; + }; + + struct AIO_NMGR_IOB2 { + uint8_t data[0xe30]; + }; + + struct AIO_IOB2_BI2X_UDN { + uint8_t data[0x3b00]; + }; + + struct AIO_IOB2_BI2X_WRFIRM { + uint8_t data[0x20f48]; + }; + + struct AIO_IOB_ICCA; + +#pragma pack(push, 1) + struct AIO_IOB2_BI2X_UDN__INPUT { + uint8_t DevIoCounter; + uint8_t bExIoAErr; + uint8_t bExIoBErr; + uint8_t bPcPowerOn; + uint8_t bPcPowerCheck; + uint8_t CoinCount; + uint8_t bTest; + uint8_t bService; + uint8_t bCoinSw; + uint8_t bCoinJam; + uint8_t bStart; + uint8_t bUp; + uint8_t bDown; + uint8_t bLeft; + uint8_t bRight; + }; + + struct AIO_IOB2_BI2X_UDN__DEVSTATUS { + uint8_t InputCounter; + uint8_t OutputCounter; + uint8_t TapeLedCounter; + uint8_t TapeLedRate[8]; + AIO_IOB2_BI2X_UDN__INPUT Input; + uint8_t InputData[241]; + uint8_t OutputData[48]; + uint8_t IoResetData[4]; + uint8_t PlyrInHist[18]; + }; + + // Fixed card-status wire format used by the compatibility interface. + // The payload is exactly 58 bytes. + // Field widths remain explicit to keep the boundary stable. + struct AIO_IOB_ICCA__CARDID { + uint8_t CardType; + uint8_t cbCardId; + uint8_t CardId[10]; + }; + + struct AIO_IOB_ICCA__INPUT { + uint8_t cuStatus; + uint8_t cuError; + AIO_IOB_ICCA__CARDID CardId; + uint8_t KeyNo; + uint8_t KeyCount; + uint8_t bKeyDataValid; + uint8_t Key2No; + uint8_t Key2Count; + uint8_t bKey2DataValid; + uint8_t bKey0; + uint8_t bKey1; + uint8_t bKey4; + uint8_t bKey7; + uint8_t bKey00; + uint8_t bKey2; + uint8_t bKey5; + uint8_t bKey8; + uint8_t bKeyBK; + uint8_t bKey3; + uint8_t bKey6; + uint8_t bKey9; + uint8_t CN1_1; + uint8_t CN1_2; + uint8_t CN1_3; + uint8_t CN1_4; + }; + + struct AIO_IOB_ICCA__DEVSTATUS { + uint8_t InputCounter; + uint8_t OutputCounter; + AIO_IOB_ICCA__INPUT Input; + uint8_t InputData[16]; + uint8_t OutputData[4]; + }; +#pragma pack(pop) + + static_assert(sizeof(AIO_SCI_COMM) == 0xf8); + static_assert(sizeof(AIO_NMGR_IOB2) == 0xe30); + static_assert(sizeof(AIO_IOB2_BI2X_UDN) == 0x3b00); + static_assert(sizeof(AIO_IOB2_BI2X_WRFIRM) == 0x20f48); + static_assert(sizeof(AIO_IOB2_BI2X_UDN__INPUT) == 15); + static_assert(sizeof(AIO_IOB2_BI2X_UDN__DEVSTATUS) == 337); + static_assert(sizeof(AIO_IOB_ICCA__CARDID) == 12); + static_assert(sizeof(AIO_IOB_ICCA__INPUT) == 36); + static_assert(sizeof(AIO_IOB_ICCA__DEVSTATUS) == 58); + + using aioIob2Bi2xUDN_Create_t = AIO_IOB2_BI2X_UDN *(__fastcall *)(AIO_NMGR_IOB2 *, uint32_t); + using aioIob2Bi2xUDN_GetDeviceStatus_t = uint32_t (__fastcall *)( + AIO_IOB2_BI2X_UDN *, AIO_IOB2_BI2X_UDN__DEVSTATUS *, uint32_t); + using aioIob2Bi2xAC1_GetDeviceStatus_t = uint32_t (__fastcall *)(void *, void *, uint32_t); + using aioIob2Bi2xUDN_IoReset_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *, uint32_t); + using aioIob2Bi2xUDN_SetWatchDogTimer_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *, uint8_t); + using aioIob2Bi2xUDN_ControlCoinBlocker_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *, uint32_t, bool); + using aioIob2Bi2xUDN_AddCounter_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *, uint32_t, uint32_t); + using aioIob2Bi2xUDN_SetStartLamp_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *, bool); + using aioIob2Bi2xUDN_SetPlayerButtonLamp_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *, uint32_t, bool); + using aioIob2Bi2xUDN_SetIccrLed_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *, uint32_t); + using aioIob2Bi2xUDN_SetTapeLedDataLimit_t = void (__fastcall *)( + AIO_IOB2_BI2X_UDN *, uint32_t, uint8_t, uint8_t); + using aioIob2Bi2xUDN_SetTapeLedData_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *, uint32_t, uint8_t *); + using aioIob2Bi2x_OpenSciUsbCdc_t = AIO_SCI_COMM *(__fastcall *)(uint32_t); + using aioIob2Bi2x_CreateWriteFirmContext_t = AIO_IOB2_BI2X_WRFIRM *(__fastcall *)(uint32_t, uint32_t); + using aioIob2Bi2x_DestroyWriteFirmContext_t = void (__fastcall *)(AIO_IOB2_BI2X_WRFIRM *); + using aioIob2Bi2x_WriteFirmGetState_t = int32_t (__fastcall *)(AIO_IOB2_BI2X_WRFIRM *); + + using aioIobIcca_Create_t = AIO_IOB_ICCA *(__fastcall *)(void *, uint32_t, uint8_t); + using aioIobIcca_GetDeviceStatus_t = uint32_t (__fastcall *)( + AIO_IOB_ICCA *, AIO_IOB_ICCA__DEVSTATUS *, uint32_t); + using aioIobIcca_BeginGetCardId_t = void (__fastcall *)(AIO_IOB_ICCA *); + using aioIobIcca_EndGetCardId_t = void (__fastcall *)(AIO_IOB_ICCA *); + + using aioNMgrIob2_Create_t = AIO_NMGR_IOB2 *(__fastcall *)(AIO_SCI_COMM *, uint32_t); + using aioNMgrIob_BeginManage_t = void (__fastcall *)(AIO_NMGR_IOB2 *); + + using aioNodeMgr_Destroy_t = void (__fastcall *)(AIO_NMGR_IOB2 *); + using aioNodeMgr_GetState_t = int32_t (__fastcall *)(AIO_NMGR_IOB2 *); + using aioNodeMgr_IsReady_t = bool (__fastcall *)(AIO_NMGR_IOB2 *, int32_t); + using aioNodeCtl_Destroy_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *); + using aioNodeCtl_GetState_t = int32_t (__fastcall *)(AIO_IOB2_BI2X_UDN *); + using aioNodeCtl_IsReady_t = bool (__fastcall *)(AIO_IOB2_BI2X_UDN *, int32_t); + + static aioIob2Bi2xUDN_Create_t aioIob2Bi2xUDN_Create_orig = nullptr; + static aioIob2Bi2xUDN_GetDeviceStatus_t aioIob2Bi2xUDN_GetDeviceStatus_orig = nullptr; + static aioIob2Bi2xAC1_GetDeviceStatus_t aioIob2Bi2xAC1_GetDeviceStatus_orig = nullptr; + static aioIob2Bi2xUDN_IoReset_t aioIob2Bi2xUDN_IoReset_orig = nullptr; + static aioIob2Bi2xUDN_SetWatchDogTimer_t aioIob2Bi2xUDN_SetWatchDogTimer_orig = nullptr; + static aioIob2Bi2xUDN_ControlCoinBlocker_t aioIob2Bi2xUDN_ControlCoinBlocker_orig = nullptr; + static aioIob2Bi2xUDN_AddCounter_t aioIob2Bi2xUDN_AddCounter_orig = nullptr; + static aioIob2Bi2xUDN_SetStartLamp_t aioIob2Bi2xUDN_SetStartLamp_orig = nullptr; + static aioIob2Bi2xUDN_SetPlayerButtonLamp_t aioIob2Bi2xUDN_SetPlayerButtonLamp_orig = nullptr; + static aioIob2Bi2xUDN_SetIccrLed_t aioIob2Bi2xUDN_SetIccrLed_orig = nullptr; + static aioIob2Bi2xUDN_SetTapeLedDataLimit_t aioIob2Bi2xUDN_SetTapeLedDataLimit_orig = nullptr; + static aioIob2Bi2xUDN_SetTapeLedData_t aioIob2Bi2xUDN_SetTapeLedData_orig = nullptr; + static aioIob2Bi2x_OpenSciUsbCdc_t aioIob2Bi2x_OpenSciUsbCdc_orig = nullptr; + static aioIob2Bi2x_CreateWriteFirmContext_t aioIob2Bi2x_CreateWriteFirmContext_orig = nullptr; + static aioIob2Bi2x_DestroyWriteFirmContext_t aioIob2Bi2x_DestroyWriteFirmContext_orig = nullptr; + static aioIob2Bi2x_WriteFirmGetState_t aioIob2Bi2x_WriteFirmGetState_orig = nullptr; + + static aioIobIcca_Create_t aioIobIcca_Create_orig = nullptr; + static aioIobIcca_GetDeviceStatus_t aioIobIcca_GetDeviceStatus_orig = nullptr; + static aioIobIcca_BeginGetCardId_t aioIobIcca_BeginGetCardId_orig = nullptr; + static aioIobIcca_EndGetCardId_t aioIobIcca_EndGetCardId_orig = nullptr; + + static aioNMgrIob2_Create_t aioNMgrIob2_Create_orig = nullptr; + static aioNMgrIob_BeginManage_t aioNMgrIob_BeginManage_orig = nullptr; + + static aioNodeMgr_Destroy_t aioNodeMgr_Destroy_orig = nullptr; + static aioNodeMgr_GetState_t aioNodeMgr_GetState_orig = nullptr; + static aioNodeMgr_IsReady_t aioNodeMgr_IsReady_orig = nullptr; + static aioNodeCtl_Destroy_t aioNodeCtl_Destroy_orig = nullptr; + static aioNodeCtl_GetState_t aioNodeCtl_GetState_orig = nullptr; + static aioNodeCtl_IsReady_t aioNodeCtl_IsReady_orig = nullptr; + + static std::unique_ptr aio_sci_comm; + static std::unique_ptr aio_node_manager; + static AIO_NMGR_IOB2 *aio_icca_node_manager = nullptr; + static std::unique_ptr aio_udn; + static std::unique_ptr aio_write_firm; + static uint8_t input_counter = 0; + + // Native ICCA status/begin/end operations also lock the reader state. + // The managed wrapper can destroy its node from a finalizer thread. + static std::mutex icca_mutex; + static AIO_IOB_ICCA *aio_icca = nullptr; + static bool icca_reading = false; + static bool icca_card_ready = false; + static uint8_t icca_card_uid[8] {}; + static uint8_t icca_input_counter = 0; + + static void write_light(Lights::udn_lights_t light, float value) { + auto &lights = get_lights(); + GameAPI::Lights::writeLight(RI_MGR, lights.at(light), value); + } + + static AIO_IOB2_BI2X_UDN *__fastcall aioIob2Bi2xUDN_Create( + AIO_NMGR_IOB2 *node_manager, uint32_t device_id) { + + if (node_manager != aio_node_manager.get()) { + return aioIob2Bi2xUDN_Create_orig(node_manager, device_id); + } + + if (!aio_udn) { + aio_udn = std::make_unique(); + log_info("udn_bi2x", "UDN node created"); + } + return aio_udn.get(); + } + + static uint32_t __fastcall aioIob2Bi2xAC1_GetDeviceStatus( + void *node, void *status, uint32_t status_size) { + + // aioIob2Bi2xUDN.GetDeviceStatus() first invokes its managed AC1 base method. + if (node == aio_udn.get()) { + if (status && status_size > 0) { + std::memset(status, 0, status_size); + } + return status_size; + } + return aioIob2Bi2xAC1_GetDeviceStatus_orig(node, status, status_size); + } + + static uint32_t __fastcall aioIob2Bi2xUDN_GetDeviceStatus( + AIO_IOB2_BI2X_UDN *node, AIO_IOB2_BI2X_UDN__DEVSTATUS *status, + uint32_t status_size) { + + if (node != aio_udn.get()) { + return aioIob2Bi2xUDN_GetDeviceStatus_orig(node, status, status_size); + } + + RI_MGR->devices_flush_output(); + + AIO_IOB2_BI2X_UDN__DEVSTATUS current {}; + current.InputCounter = input_counter; + current.Input.DevIoCounter = input_counter++; + + auto &buttons = get_buttons(); + current.Input.bService = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Service]) ? 1 : 0; + current.Input.bTest = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Test]) ? 1 : 0; + current.Input.bCoinSw = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::CoinMech]) ? 1 : 0; + current.Input.bStart = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Start]) ? 1 : 0; + current.Input.bUp = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Up]) ? 1 : 0; + current.Input.bDown = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Down]) ? 1 : 0; + current.Input.bLeft = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Left]) ? 1 : 0; + current.Input.bRight = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Right]) ? 1 : 0; + // The hardware counter wraps at 256; clamping it would stop accepting + // coins once the shared stock reaches 255. + current.Input.CoinCount = static_cast(eamuse_coin_get_stock()); + + const auto copy_size = std::min(status_size, sizeof(current)); + if (status && copy_size > 0) { + std::memcpy(status, ¤t, copy_size); + } + return copy_size; + } + + static void __fastcall aioIob2Bi2xUDN_IoReset(AIO_IOB2_BI2X_UDN *node, uint32_t reset_bits) { + if (node != aio_udn.get()) { + return aioIob2Bi2xUDN_IoReset_orig(node, reset_bits); + } + } + + static void __fastcall aioIob2Bi2xUDN_SetWatchDogTimer(AIO_IOB2_BI2X_UDN *node, uint8_t count) { + if (node != aio_udn.get()) { + return aioIob2Bi2xUDN_SetWatchDogTimer_orig(node, count); + } + } + + static void __fastcall aioIob2Bi2xUDN_ControlCoinBlocker( + AIO_IOB2_BI2X_UDN *node, uint32_t slot, bool open) { + + if (node != aio_udn.get()) { + return aioIob2Bi2xUDN_ControlCoinBlocker_orig(node, slot, open); + } + eamuse_coin_set_block(!open); + } + + static void __fastcall aioIob2Bi2xUDN_AddCounter( + AIO_IOB2_BI2X_UDN *node, uint32_t counter, uint32_t count) { + + if (node != aio_udn.get()) { + return aioIob2Bi2xUDN_AddCounter_orig(node, counter, count); + } + } + + static void __fastcall aioIob2Bi2xUDN_SetStartLamp(AIO_IOB2_BI2X_UDN *node, bool on) { + if (node != aio_udn.get()) { + return aioIob2Bi2xUDN_SetStartLamp_orig(node, on); + } + write_light(Lights::Start, on ? 1.f : 0.f); + } + + static void __fastcall aioIob2Bi2xUDN_SetPlayerButtonLamp( + AIO_IOB2_BI2X_UDN *node, uint32_t button, bool on) { + + if (node != aio_udn.get()) { + return aioIob2Bi2xUDN_SetPlayerButtonLamp_orig(node, button, on); + } + + static const Lights::udn_lights_t mapping[] { + Lights::Start, + Lights::Up, + Lights::Down, + Lights::Left, + Lights::Right, + }; + if (button < std::size(mapping)) { + write_light(mapping[button], on ? 1.f : 0.f); + } + } + + static void __fastcall aioIob2Bi2xUDN_SetIccrLed(AIO_IOB2_BI2X_UDN *node, uint32_t rgb) { + if (node != aio_udn.get()) { + return aioIob2Bi2xUDN_SetIccrLed_orig(node, 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 aioIob2Bi2xUDN_SetTapeLedDataLimit( + AIO_IOB2_BI2X_UDN *node, uint32_t channel, uint8_t scale, uint8_t limit) { + + if (node != aio_udn.get()) { + return aioIob2Bi2xUDN_SetTapeLedDataLimit_orig(node, channel, scale, limit); + } + } + + static void __fastcall aioIob2Bi2xUDN_SetTapeLedData( + AIO_IOB2_BI2X_UDN *node, uint32_t tape, uint8_t *data) { + + if (node != aio_udn.get()) { + return aioIob2Bi2xUDN_SetTapeLedData_orig(node, tape, data); + } + + struct TapeMapping { + size_t color_count; + Lights::udn_lights_t r; + Lights::udn_lights_t g; + Lights::udn_lights_t b; + }; + static const TapeMapping mapping[] { + {62, Lights::TitleR, Lights::TitleG, Lights::TitleB}, + {152, Lights::PillarRightTopR, Lights::PillarRightTopG, Lights::PillarRightTopB}, + {165, Lights::PillarRightBottomR, Lights::PillarRightBottomG, Lights::PillarRightBottomB}, + {9, Lights::StageLeftR, Lights::StageLeftG, Lights::StageLeftB}, + {17, Lights::CabinetLeftR, Lights::CabinetLeftG, Lights::CabinetLeftB}, + {152, Lights::PillarLeftTopR, Lights::PillarLeftTopG, Lights::PillarLeftTopB}, + {165, Lights::PillarLeftBottomR, Lights::PillarLeftBottomG, Lights::PillarLeftBottomB}, + {9, Lights::StageRightR, Lights::StageRightG, Lights::StageRightB}, + {17, Lights::CabinetRightR, Lights::CabinetRightG, Lights::CabinetRightB}, + }; + + if (data && tapeledutils::is_enabled() && tape < std::size(mapping)) { + const auto &entry = mapping[tape]; + const auto color = tapeledutils::pick_color_from_led_tape(data, entry.color_count); + write_light(entry.r, color.r); + write_light(entry.g, color.g); + write_light(entry.b, color.b); + } + } + + static AIO_SCI_COMM *__fastcall aioIob2Bi2x_OpenSciUsbCdc(uint32_t serial_number) { + if (!aio_sci_comm) { + aio_sci_comm = std::make_unique(); + } + return aio_sci_comm.get(); + } + + static AIO_IOB2_BI2X_WRFIRM *__fastcall aioIob2Bi2x_CreateWriteFirmContext( + uint32_t serial_number, uint32_t iob_mask) { + + if (!aio_write_firm) { + aio_write_firm = std::make_unique(); + } + return aio_write_firm.get(); + } + + static void __fastcall aioIob2Bi2x_DestroyWriteFirmContext(AIO_IOB2_BI2X_WRFIRM *context) { + if (context != aio_write_firm.get()) { + return aioIob2Bi2x_DestroyWriteFirmContext_orig(context); + } + aio_write_firm.reset(); + } + + static int32_t __fastcall aioIob2Bi2x_WriteFirmGetState(AIO_IOB2_BI2X_WRFIRM *context) { + if (context == aio_write_firm.get()) { + // The native state machine treats both 7 (NODEV) and 8 + // (COMPLETED) as a successful terminal state. UDN additionally + // interprets state 8 as "firmware was written this boot" and + // deliberately powers the cabinet down after loading so the real + // I/O board can reboot. There is no physical BI2X board behind + // this emulation, so report NODEV: the check is complete without + // scheduling that otherwise endless firmware-update reboot. + return 7; + } + return aioIob2Bi2x_WriteFirmGetState_orig(context); + } + + static AIO_IOB_ICCA *__fastcall aioIobIcca_Create( + void *node_manager, uint32_t device_id, uint8_t init_mode) { + + auto *node = aioIobIcca_Create_orig(node_manager, device_id, init_mode); + if (node) { + { + std::lock_guard lock(icca_mutex); + aio_icca = node; + aio_icca_node_manager = + reinterpret_cast(node_manager); + icca_reading = false; + icca_card_ready = false; + std::memset(icca_card_uid, 0, sizeof(icca_card_uid)); + } + log_info("udn_bi2x", "ICCA node attached to Spice CardIO bridge"); + } + return node; + } + + static uint32_t __fastcall aioIobIcca_GetDeviceStatus( + AIO_IOB_ICCA *node, AIO_IOB_ICCA__DEVSTATUS *status, uint32_t status_size) { + + std::unique_lock lock(icca_mutex); + if (node != aio_icca) { + lock.unlock(); + return aioIobIcca_GetDeviceStatus_orig(node, status, status_size); + } + + AIO_IOB_ICCA__DEVSTATUS current {}; + current.InputCounter = icca_input_counter++; + current.OutputCounter = current.InputCounter; + + // DNI while idle, BUSY while waiting for a card, READY once a UID is + // available. The card-reader state machine accepts a card only on READY. + current.Input.cuStatus = icca_reading ? 1 : 0; + if (icca_reading && !icca_card_ready && eamuse_card_insert_consume(1, 0)) { + uint8_t uid[8] {}; + if (eamuse_get_card(1, 0, uid)) { + std::memcpy(icca_card_uid, uid, sizeof(icca_card_uid)); + icca_card_ready = true; + log_info( + "udn_bi2x", + "ICCA accepted UID {} from the Spice card queue; reporting READY to Unity", + bin2hex(icca_card_uid, sizeof(icca_card_uid))); + } else { + log_warning( + "udn_bi2x", + "ICCA consumed a card-insert event but Spice returned no UID"); + } + } + + if (icca_card_ready) { + current.Input.cuStatus = 2; + current.Input.CardId.CardType = is_card_uid_felica(icca_card_uid) ? 2 : 1; + current.Input.CardId.cbCardId = sizeof(icca_card_uid); + std::memcpy(current.Input.CardId.CardId, icca_card_uid, sizeof(icca_card_uid)); + } + + // The ICCA keypad scan-code order is identical to eamuse.h. Keeping + // these bits populated also preserves the usual Spice numpad PIN flow. + const auto keypad = eamuse_get_keypad_state(0); + auto *key_states = ¤t.Input.bKey0; + for (size_t key = 0; key < EAM_IO_KEYPAD_COUNT; ++key) { + key_states[key] = (keypad & (1u << key)) ? 1 : 0; + } + + const auto copy_size = std::min(status_size, sizeof(current)); + if (status && copy_size > 0) { + std::memcpy(status, ¤t, copy_size); + } + return copy_size; + } + + static void __fastcall aioIobIcca_BeginGetCardId(AIO_IOB_ICCA *node) { + std::unique_lock lock(icca_mutex); + if (node != aio_icca) { + lock.unlock(); + return aioIobIcca_BeginGetCardId_orig(node); + } + if (!icca_reading) { + log_info("udn_bi2x", "ICCA BeginGetCardId: waiting for the Spice/CardIO queue"); + } + icca_reading = true; + icca_card_ready = false; + std::memset(icca_card_uid, 0, sizeof(icca_card_uid)); + } + + static void __fastcall aioIobIcca_EndGetCardId(AIO_IOB_ICCA *node) { + std::unique_lock lock(icca_mutex); + if (node != aio_icca) { + lock.unlock(); + return aioIobIcca_EndGetCardId_orig(node); + } + if (icca_reading || icca_card_ready) { + log_info("udn_bi2x", "ICCA EndGetCardId: returning to idle"); + } + icca_reading = false; + icca_card_ready = false; + std::memset(icca_card_uid, 0, sizeof(icca_card_uid)); + } + + static AIO_NMGR_IOB2 *__fastcall aioNMgrIob2_Create(AIO_SCI_COMM *sci, uint32_t mode) { + if (sci != aio_sci_comm.get()) { + return aioNMgrIob2_Create_orig(sci, mode); + } + if (!aio_node_manager) { + aio_node_manager = std::make_unique(); + } + return aio_node_manager.get(); + } + + static void __fastcall aioNMgrIob_BeginManage(AIO_NMGR_IOB2 *node_manager) { + if (node_manager != aio_node_manager.get()) { + return aioNMgrIob_BeginManage_orig(node_manager); + } + } + + static void __fastcall aioNodeMgr_Destroy(AIO_NMGR_IOB2 *node_manager) { + if (node_manager != aio_node_manager.get()) { + { + std::lock_guard lock(icca_mutex); + if (node_manager == aio_icca_node_manager) { + aio_icca_node_manager = nullptr; + } + } + return aioNodeMgr_Destroy_orig(node_manager); + } + aio_node_manager.reset(); + } + + static int32_t __fastcall aioNodeMgr_GetState(AIO_NMGR_IOB2 *node_manager) { + bool virtual_manager = node_manager == aio_node_manager.get(); + { + std::lock_guard lock(icca_mutex); + virtual_manager |= node_manager == aio_icca_node_manager; + } + if (virtual_manager) { + return 0; + } + return aioNodeMgr_GetState_orig(node_manager); + } + + static bool __fastcall aioNodeMgr_IsReady(AIO_NMGR_IOB2 *node_manager, int32_t state) { + bool virtual_manager = node_manager == aio_node_manager.get(); + { + std::lock_guard lock(icca_mutex); + virtual_manager |= node_manager == aio_icca_node_manager; + } + if (virtual_manager) { + return true; + } + return aioNodeMgr_IsReady_orig(node_manager, state); + } + + static void __fastcall aioNodeCtl_Destroy(AIO_IOB2_BI2X_UDN *node) { + if (node != aio_udn.get()) { + { + std::lock_guard lock(icca_mutex); + if (reinterpret_cast(node) == aio_icca) { + aio_icca = nullptr; + aio_icca_node_manager = nullptr; + icca_reading = false; + icca_card_ready = false; + std::memset(icca_card_uid, 0, sizeof(icca_card_uid)); + } + } + return aioNodeCtl_Destroy_orig(node); + } + aio_udn.reset(); + } + + static int32_t __fastcall aioNodeCtl_GetState(AIO_IOB2_BI2X_UDN *node) { + bool virtual_node = node == aio_udn.get(); + { + std::lock_guard lock(icca_mutex); + virtual_node |= reinterpret_cast(node) == aio_icca; + } + if (virtual_node) { + return 0; + } + return aioNodeCtl_GetState_orig(node); + } + + static bool __fastcall aioNodeCtl_IsReady(AIO_IOB2_BI2X_UDN *node, int32_t state) { + bool virtual_node = node == aio_udn.get(); + { + std::lock_guard lock(icca_mutex); + virtual_node |= reinterpret_cast(node) == aio_icca; + } + if (virtual_node) { + return true; + } + return aioNodeCtl_IsReady_orig(node, state); + } + + void bi2x_hook_init() { + log_info("udn_bi2x", "init (BI2X + ICCA/CardIO bridge)"); + + // The runtime imports the alias DLL names, not their lib-prefixed originals. + const auto aio_iob2_video = "aio-iob2_video.dll"; + execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_Create", + aioIob2Bi2xUDN_Create, &aioIob2Bi2xUDN_Create_orig); + execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_GetDeviceStatus", + aioIob2Bi2xUDN_GetDeviceStatus, &aioIob2Bi2xUDN_GetDeviceStatus_orig); + execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xAC1_GetDeviceStatus", + aioIob2Bi2xAC1_GetDeviceStatus, &aioIob2Bi2xAC1_GetDeviceStatus_orig); + execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_IoReset", + aioIob2Bi2xUDN_IoReset, &aioIob2Bi2xUDN_IoReset_orig); + execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_SetWatchDogTimer", + aioIob2Bi2xUDN_SetWatchDogTimer, &aioIob2Bi2xUDN_SetWatchDogTimer_orig); + execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_ControlCoinBlocker", + aioIob2Bi2xUDN_ControlCoinBlocker, &aioIob2Bi2xUDN_ControlCoinBlocker_orig); + execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_AddCounter", + aioIob2Bi2xUDN_AddCounter, &aioIob2Bi2xUDN_AddCounter_orig); + execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_SetStartLamp", + aioIob2Bi2xUDN_SetStartLamp, &aioIob2Bi2xUDN_SetStartLamp_orig); + execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_SetPlayerButtonLamp", + aioIob2Bi2xUDN_SetPlayerButtonLamp, &aioIob2Bi2xUDN_SetPlayerButtonLamp_orig); + execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_SetIccrLed", + aioIob2Bi2xUDN_SetIccrLed, &aioIob2Bi2xUDN_SetIccrLed_orig); + execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_SetTapeLedDataLimit", + aioIob2Bi2xUDN_SetTapeLedDataLimit, &aioIob2Bi2xUDN_SetTapeLedDataLimit_orig); + execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_SetTapeLedData", + aioIob2Bi2xUDN_SetTapeLedData, &aioIob2Bi2xUDN_SetTapeLedData_orig); + execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2x_OpenSciUsbCdc", + aioIob2Bi2x_OpenSciUsbCdc, &aioIob2Bi2x_OpenSciUsbCdc_orig); + execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2x_CreateWriteFirmContext", + aioIob2Bi2x_CreateWriteFirmContext, &aioIob2Bi2x_CreateWriteFirmContext_orig); + execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2x_DestroyWriteFirmContext", + aioIob2Bi2x_DestroyWriteFirmContext, &aioIob2Bi2x_DestroyWriteFirmContext_orig); + execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2x_WriteFirmGetState", + aioIob2Bi2x_WriteFirmGetState, &aioIob2Bi2x_WriteFirmGetState_orig); + + const auto aio_iob_video = "aio-iob_video.dll"; + execexe::trampoline_try(aio_iob_video, "aioIobIcca_Create", + aioIobIcca_Create, &aioIobIcca_Create_orig); + execexe::trampoline_try(aio_iob_video, "aioIobIcca_GetDeviceStatus", + aioIobIcca_GetDeviceStatus, &aioIobIcca_GetDeviceStatus_orig); + execexe::trampoline_try(aio_iob_video, "aioIobIcca_BeginGetCardId", + aioIobIcca_BeginGetCardId, &aioIobIcca_BeginGetCardId_orig); + execexe::trampoline_try(aio_iob_video, "aioIobIcca_EndGetCardId", + aioIobIcca_EndGetCardId, &aioIobIcca_EndGetCardId_orig); + + const auto aio_iob = "aio-iob.dll"; + execexe::trampoline_try(aio_iob, "aioNMgrIob2_Create", + aioNMgrIob2_Create, &aioNMgrIob2_Create_orig); + execexe::trampoline_try(aio_iob, "aioNMgrIob_BeginManage", + aioNMgrIob_BeginManage, &aioNMgrIob_BeginManage_orig); + + const auto aio = "aio.dll"; + execexe::trampoline_try(aio, "aioNodeMgr_Destroy", + aioNodeMgr_Destroy, &aioNodeMgr_Destroy_orig); + execexe::trampoline_try(aio, "aioNodeMgr_GetState", + aioNodeMgr_GetState, &aioNodeMgr_GetState_orig); + execexe::trampoline_try(aio, "aioNodeMgr_IsReady", + aioNodeMgr_IsReady, &aioNodeMgr_IsReady_orig); + execexe::trampoline_try(aio, "aioNodeCtl_Destroy", + aioNodeCtl_Destroy, &aioNodeCtl_Destroy_orig); + execexe::trampoline_try(aio, "aioNodeCtl_GetState", + aioNodeCtl_GetState, &aioNodeCtl_GetState_orig); + execexe::trampoline_try(aio, "aioNodeCtl_IsReady", + aioNodeCtl_IsReady, &aioNodeCtl_IsReady_orig); + } +} + +#endif diff --git a/src/spice2x/games/udn/bi2x_hook.h b/src/spice2x/games/udn/bi2x_hook.h new file mode 100644 index 00000000..b54297e2 --- /dev/null +++ b/src/spice2x/games/udn/bi2x_hook.h @@ -0,0 +1,5 @@ +#pragma once + +namespace games::udn { + void bi2x_hook_init(); +} diff --git a/src/spice2x/games/udn/io.cpp b/src/spice2x/games/udn/io.cpp new file mode 100644 index 00000000..f875c659 --- /dev/null +++ b/src/spice2x/games/udn/io.cpp @@ -0,0 +1,91 @@ +#include "io.h" + +#include + +std::vector