diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 17a64d4..9bde183 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -105,5 +105,5 @@ if (conditional) * We don't use GSL. * Don't throw exceptions. * Stick to smart pointers. For interfacing with C Win32 API and raw buffers, use `unique_plain_ptr`. - * Writing in C is also completely acceptable. + * Writing in C is also completely acceptable but prefer to use RAII patterns. * Other than that, there are no strict rules for code formatting, but please attempt to emulate the style around the code you are modifying. diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index e280460..5ec347b 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -565,6 +565,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 @@ -621,6 +624,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 @@ -632,6 +636,8 @@ set(SOURCE_FILES ${SOURCE_FILES} hooks/networkhook.cpp hooks/icmphook_net.cpp hooks/icmphook_iphlpapi.cpp + hooks/nicspoof.cpp + hooks/nicspoof_tunnel.cpp hooks/powrprof.cpp #hooks/rom.cpp hooks/setupapihook.cpp @@ -733,6 +739,8 @@ set(SOURCE_FILES ${SOURCE_FILES} reader/crypt.cpp # sdk + sdk/d3d9.cpp + sdk/modules.cpp sdk/sdk.cpp # stubs @@ -1061,8 +1069,16 @@ if(NOT MSVC) endif() # sdk_sample_v0_cpp.dll (64 bit) -set(SOURCE_FILES sdk/sample/v0/cpp/v0_cpp.cpp) +set(SOURCE_FILES + sdk/sample/v0/cpp/v0_cpp.cpp + sdk/sample/v0/cpp/v0_cpp_imgui.cpp + external/imgui/imgui.cpp + external/imgui/imgui_draw.cpp + external/imgui/imgui_tables.cpp + external/imgui/imgui_widgets.cpp + external/imgui/backends/imgui_impl_dx9.cpp) add_library(spicetools_sdk_sample_v0_cpp_64 SHARED ${SOURCE_FILES} ${RESOURCE_FILES} sdk/sample/v0/cpp/v0_cpp.def) +target_link_libraries(spicetools_sdk_sample_v0_cpp_64 PRIVATE imm32) set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES PREFIX "") set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES OUTPUT_NAME "sdk_sample_v0_cpp") diff --git a/src/spice2x/acio/icca/icca.cpp b/src/spice2x/acio/icca/icca.cpp index f8493cb..ef37629 100644 --- a/src/spice2x/acio/icca/icca.cpp +++ b/src/spice2x/acio/icca/icca.cpp @@ -118,11 +118,11 @@ static inline void update_card(int unit_id) { const bool card_presented = eamuse_card_insert_consume(icca_get_active_count(), unit_id); const bool key_pressed = (kb_insert_press && !kb_insert_press_old[unit_id]); - // beatstream and nostalgia have logic that requires ac_io_icca_get_uid_felica to return the + // beatstream, nostalgia and Dance Evolution have logic that requires ac_io_icca_get_uid_felica to return the // exact same card number multiple times in a row in order for the card number to be read... // for whatever reason setting this to 3-4 doesn't work the very first time the game boots up // so we use 10 just to be safe - const bool need_felica_retries = avs::game::is_model({"NBT", "PAN"}); + const bool need_felica_retries = avs::game::is_model({"KDM", "NBT", "PAN"}); if (need_felica_retries && (card_presented || key_pressed)) { unit->felica_retries = 10; } diff --git a/src/spice2x/api/controller.cpp b/src/spice2x/api/controller.cpp index 1a6c4dd..d1400e6 100644 --- a/src/spice2x/api/controller.cpp +++ b/src/spice2x/api/controller.cpp @@ -8,6 +8,7 @@ #include "client.h" #include "cfg/configurator.h" #include "external/rapidjson/document.h" +#include "external/rapidjson/error/en.h" #include "util/crypt.h" #include "util/logging.h" #include "util/utils.h" @@ -92,6 +93,7 @@ Controller::Controller(unsigned short port, std::string password, bool pretty) // bind socket to address if (bind(this->server, (sockaddr *) &server_address, sizeof(sockaddr)) == -1) { log_warning("api", "could not bind socket on port {}: {}", port, get_last_error_string()); + closesocket(this->server); this->server = INVALID_SOCKET; if (!cfg::CONFIGURATOR_STANDALONE) { log_fatal("api", "failed to start server"); @@ -102,6 +104,7 @@ Controller::Controller(unsigned short port, std::string password, bool pretty) // set socket to listen if (listen(this->server, server_backlog) == -1) { log_warning("api", "could not listen to socket on port {}: {}", port, get_last_error_string()); + closesocket(this->server); this->server = INVALID_SOCKET; if (!cfg::CONFIGURATOR_STANDALONE) { log_fatal("api", "failed to start server"); @@ -312,6 +315,8 @@ bool Controller::process_request(ClientState *state, const char *in, size_t in_s // check for parse error if (document.HasParseError()) { + log_warning("api", "Request JSON parse error: {} (byte offset {}, decrypted request length {} bytes)", + GetParseError_En(document.GetParseError()), document.GetErrorOffset(), in_size); // return empty response and close connection out->push_back(0); @@ -443,7 +448,9 @@ void Controller::free_socket() { this->server = INVALID_SOCKET; } - this->websocket->free_socket(); + if (this->websocket) { + this->websocket->free_socket(); + } for (auto &s : this->serial) { s->free_port(); diff --git a/src/spice2x/api/controller.h b/src/spice2x/api/controller.h index 5413f44..7a4a356 100644 --- a/src/spice2x/api/controller.h +++ b/src/spice2x/api/controller.h @@ -41,21 +41,21 @@ namespace api { bool pretty; // server - WebSocketController *websocket; + WebSocketController *websocket = nullptr; std::vector serial; std::vector server_workers; std::vector server_handlers; std::mutex server_handlers_m; std::vector client_states; std::mutex client_states_m; - SOCKET server; + SOCKET server = INVALID_SOCKET; void server_worker(); void connection_handler(ClientState client_state); public: // state - bool server_running; + bool server_running = false; // constructor / destructor Controller(unsigned short port, std::string password, bool pretty); diff --git a/src/spice2x/api/modules/coin.cpp b/src/spice2x/api/modules/coin.cpp index 4f4937d..14e955e 100644 --- a/src/spice2x/api/modules/coin.cpp +++ b/src/spice2x/api/modules/coin.cpp @@ -61,7 +61,7 @@ namespace api::modules { return error_type(res, "amount", "int"); // add to coin stock - eamuse_coin_set_stock(eamuse_coin_get_stock() + std::max(0, req.params[0].GetInt())); + eamuse_coin_add(std::max(0, req.params[0].GetInt())); } /* diff --git a/src/spice2x/easrv/easrv.cpp b/src/spice2x/easrv/easrv.cpp index c00f7ea..de17c39 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/external/scard/scard.cpp b/src/spice2x/external/scard/scard.cpp index 9edfd24..f24fb92 100644 --- a/src/spice2x/external/scard/scard.cpp +++ b/src/spice2x/external/scard/scard.cpp @@ -107,6 +107,7 @@ static const BYTE UID_CMD[5] = { 0xFFu, 0xCAu, 0x00u, 0x00u, 0x00u }; enum scard_atr_protocol { SCARD_ATR_PROTOCOL_ISO14443_PART3 = 0x03, SCARD_ATR_PROTOCOL_ISO15693_PART3 = 0x0B, + SCARD_ATR_PROTOCOL_ISO15693_PART4 = 0x0C, SCARD_ATR_PROTOCOL_FELICA_212K = 0x11, SCARD_ATR_PROTOCOL_FELICA_424K = 0x12, }; @@ -178,11 +179,14 @@ void scard_update(SCARDCONTEXT hContext, LPCTSTR readerName, uint8_t unit_no) { BYTE cardProtocol = atr[12]; BOOL shouldReverseUid = false; bool is_felica = false; - if (cardProtocol == SCARD_ATR_PROTOCOL_ISO15693_PART3) { + if (cardProtocol == SCARD_ATR_PROTOCOL_ISO14443_PART3) { + log_info("scard", "card protocol: ISO14443_PART3"); + } else if (cardProtocol == SCARD_ATR_PROTOCOL_ISO15693_PART3) { log_info("scard", "card protocol: ISO15693_PART3"); shouldReverseUid = true; - } else if (cardProtocol == SCARD_ATR_PROTOCOL_ISO14443_PART3) { - log_info("scard", "card protocol: ISO14443_PART3"); + } else if (cardProtocol == SCARD_ATR_PROTOCOL_ISO15693_PART4) { + log_info("scard", "card protocol: ISO15693_PART4"); + shouldReverseUid = true; } else if (cardProtocol == SCARD_ATR_PROTOCOL_FELICA_212K) { log_info("scard", "card protocol: FELICA_212K"); is_felica = true; diff --git a/src/spice2x/games/gitadora/gitadora.cpp b/src/spice2x/games/gitadora/gitadora.cpp index 460b1b6..d99c770 100644 --- a/src/spice2x/games/gitadora/gitadora.cpp +++ b/src/spice2x/games/gitadora/gitadora.cpp @@ -39,7 +39,7 @@ namespace games::gitadora { std::optional PICK_ALGO = socd::SocdAlgorithm::PreferRecent; std::optional ARENA_WINDOW_COUNT = std::nullopt; bool ARENA_TWO_HEAD_EXCLUSIVE = false; - bool ARENA_SUBSCREEN_LANDSCAPE = false; + ArenaSubscreenLandscape ARENA_SUBSCREEN_LANDSCAPE = ArenaSubscreenLandscape::Off; std::optional ASIO_DRIVER = std::nullopt; bool ALLOW_REALTEK_AUDIO = false; bool NATIVE_TOUCH = false; @@ -48,7 +48,7 @@ namespace games::gitadora { if (GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) { return GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value(); } - if (ARENA_SUBSCREEN_LANDSCAPE) { + if (arena_subscreen_landscape()) { return { ARENA_SUBSCREEN_LANDSCAPE_WIDTH, ARENA_SUBSCREEN_LANDSCAPE_HEIGHT }; } return { ARENA_SUBSCREEN_WIDTH, ARENA_SUBSCREEN_HEIGHT }; @@ -372,11 +372,11 @@ namespace games::gitadora { "arena model: unsupported window count: {}", count); } - if (ARENA_SUBSCREEN_LANDSCAPE && !ARENA_TWO_HEAD_EXCLUSIVE) { + if (arena_subscreen_landscape() && !ARENA_TWO_HEAD_EXCLUSIVE) { log_warning( "gitadora", "arena model: landscape subscreen needs full screen two-window mode, ignoring"); - ARENA_SUBSCREEN_LANDSCAPE = false; + ARENA_SUBSCREEN_LANDSCAPE = ArenaSubscreenLandscape::Off; } if (ARENA_TWO_HEAD_EXCLUSIVE) { const auto [host_width, host_height] = arena_subscreen_host_size(); diff --git a/src/spice2x/games/gitadora/gitadora.h b/src/spice2x/games/gitadora/gitadora.h index 7e6edaf..0dd2218 100644 --- a/src/spice2x/games/gitadora/gitadora.h +++ b/src/spice2x/games/gitadora/gitadora.h @@ -12,6 +12,14 @@ namespace games::gitadora { + // what a landscape monitor driving the SMALL head does with the space beside the + // portrait subscreen image + enum class ArenaSubscreenLandscape { + Off, // SMALL head stays portrait + Small, // subscreen centered, the space either side stays black + All, // that space shows the LEFT and RIGHT heads instead + }; + // settings extern bool TWOCHANNEL; extern bool DISABLE_FRAME_LIMITER; @@ -22,11 +30,19 @@ namespace games::gitadora { extern std::optional PICK_ALGO; extern std::optional ARENA_WINDOW_COUNT; extern bool ARENA_TWO_HEAD_EXCLUSIVE; - extern bool ARENA_SUBSCREEN_LANDSCAPE; + extern ArenaSubscreenLandscape ARENA_SUBSCREEN_LANDSCAPE; extern std::optional ASIO_DRIVER; extern bool ALLOW_REALTEK_AUDIO; extern bool NATIVE_TOUCH; + static inline bool arena_subscreen_landscape() { + return ARENA_SUBSCREEN_LANDSCAPE != ArenaSubscreenLandscape::Off; + } + + static inline bool arena_subscreen_shows_sides() { + return ARENA_SUBSCREEN_LANDSCAPE == ArenaSubscreenLandscape::All; + } + // arena SMALL subscreen (touch panel) resolution static constexpr int ARENA_SUBSCREEN_WIDTH = 800; static constexpr int ARENA_SUBSCREEN_HEIGHT = 1280; diff --git a/src/spice2x/games/iidx/camera.cpp b/src/spice2x/games/iidx/camera.cpp index 68a7d48..ec33d73 100644 --- a/src/spice2x/games/iidx/camera.cpp +++ b/src/spice2x/games/iidx/camera.cpp @@ -2,6 +2,7 @@ #if SPICE64 && !SPICE_XP +#include #include #include "mf_wrappers.h" #include "avs/game.h" @@ -46,9 +47,7 @@ struct PredefinedHook { uintptr_t hook_afp_texture_offset; }; -PredefinedHook g_predefinedHooks[] = {}; - -const DWORD g_predefinedHooksLength = ARRAYSIZE(g_predefinedHooks); +std::array g_predefinedHooks = {}; namespace games::iidx { @@ -110,14 +109,14 @@ namespace games::iidx { auto pe = fmt::format("{:x}_{:x}", time_date_stamp, address_of_entry_point); log_info("iidx:camhook", "Locating predefined hook addresses for LDJ-{}", pe); - for (DWORD i = 0; i < g_predefinedHooksLength; i++) { - if (pe.compare(g_predefinedHooks[i].pe_identifier) == 0) { + for (auto &hook : g_predefinedHooks) { + if (pe.compare(hook.pe_identifier) == 0) { log_misc("iidx:camhook", "Found predefined addresses"); - addr_hook_a = g_predefinedHooks[i].hook_a; - addr_textures = g_predefinedHooks[i].hook_textures; - addr_camera_manager = g_predefinedHooks[i].hook_camera_manager; - addr_device_offset = g_predefinedHooks[i].hook_device_offset; - addr_afp_texture_offset = g_predefinedHooks[i].hook_afp_texture_offset; + addr_hook_a = hook.hook_a; + addr_textures = hook.hook_textures; + addr_camera_manager = hook.hook_camera_manager; + addr_device_offset = hook.hook_device_offset; + addr_afp_texture_offset = hook.hook_afp_texture_offset; return TRUE; } } @@ -200,11 +199,11 @@ namespace games::iidx { addr_camera_manager = (addr_camera_manager_ptr - (uint8_t*) iidx_module) + disp_camera_manager + 7; // addr_device_offset - search_from = (addr_hook_a_ptr - (uint8_t*) iidx_module); + search_from = addr_hook_a; uint8_t *addr_device_ptr = reinterpret_cast(find_pattern_from( iidx_module, - "488B89", - "XXX", + "488B8F00000000488B014C8B870000000033D2FF9028010000", + "XXX????XXXXXX????XXXXXXXX", 3, 0, search_from)); if (addr_device_ptr == nullptr) { @@ -212,7 +211,7 @@ namespace games::iidx { return FALSE; } - addr_device_offset = *addr_device_ptr; + addr_device_offset = *reinterpret_cast(addr_device_ptr); // --- addr_afp_texture_offset --- uint8_t *addr_afp_texture_ptr = reinterpret_cast(find_pattern( @@ -234,13 +233,27 @@ namespace games::iidx { static void **__fastcall camera_hook_a(PBYTE a1) { std::call_once(hook_a_init, [&]{ device = *reinterpret_cast(a1 + addr_device_offset); - auto const preview = *reinterpret_cast((uint8_t*)iidx_module + addr_textures); + auto const registry = reinterpret_cast( + reinterpret_cast(iidx_module) + addr_textures - offsetof(Camera::TextureRegistry, begin)); auto const manager = reinterpret_cast((uint8_t*)iidx_module + addr_camera_manager); - camera_texture_a = manager->cameras->a->d3d9_texture(addr_afp_texture_offset); - camera_texture_b = manager->cameras->b->d3d9_texture(addr_afp_texture_offset); - preview_texture_a = preview; - preview_texture_b = preview + 2; + auto const camera_count = manager->begin ? manager->end - manager->begin : 0; + auto const bind_camera = [&](size_t index, LPDIRECT3DTEXTURE9*& texture, LPDIRECT3DTEXTURE9*& preview) { + if (index >= static_cast(camera_count)) { + return; // IIDX 34 only has the TOP camera. + } + auto const camera = manager->begin[index]; + auto const afp = camera ? camera->afp_texture(addr_afp_texture_offset) : nullptr; + if (!afp || !registry->begin || afp->handle < registry->handle_base || + afp->handle - registry->handle_base >= registry->end - registry->begin) { + log_warning("iidx:camhook", "invalid texture destination for camera {}", index); + return; + } + texture = &afp->texture; + preview = registry->begin + (afp->handle - registry->handle_base); + }; + bind_camera(0, camera_texture_a, preview_texture_a); + bind_camera(1, camera_texture_b, preview_texture_b); init_local_camera(); }); @@ -382,10 +395,10 @@ namespace games::iidx { log_misc("iidx:camhook", "Adding user-preferred cameras"); for (size_t i = 0; i < numDevices && LOCAL_CAMERA_LIST.size() < 2; i++) { auto pActivate = ppDevices[i]; - if ((int)i == preferred_top_index && top_camera == nullptr) { + if ((int)i == preferred_top_index && top_camera == nullptr && camera_texture_a != nullptr) { log_misc("iidx:camhook", "Adding user-preferred top camera {} / '{}'", i, top_camera_id); top_camera = add_top_camera(pActivate); - } else if ((int)i == preferred_front_index && front_camera == nullptr) { + } else if ((int)i == preferred_front_index && front_camera == nullptr && camera_texture_b != nullptr) { log_misc("iidx:camhook", "Adding user-preferred front camera {} / '{}'", i, front_camera_id); front_camera = add_front_camera(pActivate); } @@ -403,16 +416,16 @@ namespace games::iidx { if (!FLIP_CAMS) { // top camera first, then front - if (top_camera == nullptr) { + if (top_camera == nullptr && camera_texture_a != nullptr) { top_camera = add_top_camera(pActivate); - } else if (front_camera == nullptr) { + } else if (front_camera == nullptr && camera_texture_b != nullptr) { front_camera = add_front_camera(pActivate); } } else { // front first, then top - if (front_camera == nullptr) { + if (front_camera == nullptr && camera_texture_b != nullptr) { front_camera = add_front_camera(pActivate); - } else if (top_camera == nullptr) { + } else if (top_camera == nullptr && camera_texture_a != nullptr) { top_camera = add_top_camera(pActivate); } } diff --git a/src/spice2x/games/iidx/local_camera.h b/src/spice2x/games/iidx/local_camera.h index 431c9e2..af3b802 100644 --- a/src/spice2x/games/iidx/local_camera.h +++ b/src/spice2x/games/iidx/local_camera.h @@ -70,21 +70,31 @@ namespace games::iidx { class IIDXCameraSourceReaderCallback; namespace Camera { + struct AfpTexture { + void* vftbl; + IDirect3DTexture9* texture; + uint32_t handle; + }; + + struct TextureRegistry { + uint32_t handle_base; + uint32_t next_slot; + IDirect3DTexture9** begin; + IDirect3DTexture9** end; + IDirect3DTexture9** capacity_end; + }; + struct PlayVideoCamera { - IDirect3DTexture9** d3d9_texture(const uintptr_t offset) { - auto const afp_texture = *reinterpret_cast - (reinterpret_cast(this) + offset); - return reinterpret_cast(afp_texture + 0x8); + AfpTexture* afp_texture(const uintptr_t offset) { + return *reinterpret_cast(reinterpret_cast(this) + offset); } }; struct CCameraManager2 { - using camera_pointers = struct { - PlayVideoCamera* a; - PlayVideoCamera* b; - }; void* vftbl; - camera_pointers* cameras; + PlayVideoCamera** begin; + PlayVideoCamera** end; + PlayVideoCamera** capacity_end; }; } diff --git a/src/spice2x/games/io.cpp b/src/spice2x/games/io.cpp index 40b9ac1..1a161b1 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 0000000..6e10e30 --- /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 0000000..b54297e --- /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 0000000..f875c65 --- /dev/null +++ b/src/spice2x/games/udn/io.cpp @@ -0,0 +1,91 @@ +#include "io.h" + +#include + +std::vector