diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index babe7b0..50562af 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -411,6 +411,7 @@ set(SOURCE_FILES ${SOURCE_FILES} games/sdvx/sdvx_live2d.cpp games/sdvx/io.cpp games/sdvx/camera.cpp + games/jb/bi2x_hook.cpp games/jb/jb.cpp games/jb/jb_touch.cpp games/jb/io.cpp diff --git a/src/spice2x/acioemu/icca.cpp b/src/spice2x/acioemu/icca.cpp index ff003ed..eb9eddb 100644 --- a/src/spice2x/acioemu/icca.cpp +++ b/src/spice2x/acioemu/icca.cpp @@ -239,7 +239,7 @@ bool ICCADevice::parse_msg(MessageData *msg_in, // SDVX Old cabinet mode if (avs::game::is_model("KFC") && avs::game::SPEC[0] != 'G' && avs::game::SPEC[0] != 'H') answer_type = 1; - if (avs::game::is_model("L44")) + if (avs::game::is_model({ "L44", "T44" })) answer_type = 2; // check answer type diff --git a/src/spice2x/avs/core.cpp b/src/spice2x/avs/core.cpp index 23f77e0..1557187 100644 --- a/src/spice2x/avs/core.cpp +++ b/src/spice2x/avs/core.cpp @@ -1114,6 +1114,7 @@ namespace avs { #endif // jubeat {"jubeat.dll", 0x2000000}, + {"jubeat2019.dll", 0x10000000}, // MUSECA {"museca.dll", 0xC000000}, diff --git a/src/spice2x/avs/ea3.cpp b/src/spice2x/avs/ea3.cpp index 1692bea..bb90110 100644 --- a/src/spice2x/avs/ea3.cpp +++ b/src/spice2x/avs/ea3.cpp @@ -591,6 +591,7 @@ namespace avs { // for proper reporting of Omnimix and other song packs if (_stricmp(EA3_MODEL, "LDJ") == 0 || _stricmp(EA3_MODEL, "L44") == 0 || + _stricmp(EA3_MODEL, "T44") == 0 || _stricmp(EA3_MODEL, "M39") == 0 || _stricmp(EA3_MODEL, "KFC") == 0) { diff --git a/src/spice2x/games/io.cpp b/src/spice2x/games/io.cpp index 15eb0a8..40b9ac1 100644 --- a/src/spice2x/games/io.cpp +++ b/src/spice2x/games/io.cpp @@ -133,6 +133,7 @@ namespace games { buttons_help.insert({ jb, jb::get_buttons_help() }); lights.insert({ jb, jb::get_lights() }); file_hints[jb].push_back({"jubeat.dll"}); + file_hints[jb].push_back({"jubeat2019.dll"}); // mga const std::string mga("Metal Gear"); diff --git a/src/spice2x/games/jb/bi2x_hook.cpp b/src/spice2x/games/jb/bi2x_hook.cpp new file mode 100644 index 0000000..898e818 --- /dev/null +++ b/src/spice2x/games/jb/bi2x_hook.cpp @@ -0,0 +1,336 @@ +#include "bi2x_hook.h" + +#if SPICE64 + +#include +#include +#include + +#include "games/io.h" +#include "games/jb/jb_touch.h" +#include "io.h" +#include "misc/eamuse.h" +#include "rawinput/rawinput.h" +#include "util/detour.h" +#include "util/logging.h" + +namespace games::jb { + + struct AIO_SCI_COMM { + std::array data; + }; + + struct AIO_NMGR_IOB2; + + struct AIO_NMGR_IOB2_VTABLE { + std::array unused; + void (__fastcall *begin_manage)(AIO_NMGR_IOB2 *node_mgr); + }; + + struct AIO_NMGR_IOB2 { + AIO_NMGR_IOB2_VTABLE *vtable; + std::array data; + }; + + struct AIO_IOB2_BI2X_T44 { + std::array data; + }; + + struct AIO_IOB2_BI2X_T44_DEVSTATUS { + std::array data; + }; + + struct AIO_IOB2_BI2X_WRFIRM { + uint8_t data; + }; + + static_assert(sizeof(AIO_NMGR_IOB2) == 0x80); + static_assert(sizeof(AIO_IOB2_BI2X_T44_DEVSTATUS) == 0x140); + + using aioIob2Bi2xT44_Create_t = AIO_IOB2_BI2X_T44 *(__fastcall *)(AIO_NMGR_IOB2 *node_mgr, + uint32_t device_id); + using aioIob2Bi2xT44_GetDeviceStatus_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node, + AIO_IOB2_BI2X_T44_DEVSTATUS *status); + using aioIob2Bi2xT44_IoReset_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node, uint32_t reset); + using aioIob2Bi2xT44_SetWatchDogTimer_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node, uint8_t count); + using aioIob2Bi2xT44_ControlCoinBlocker_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node, + uint32_t slot, bool open); + using aioIob2Bi2xT44_AddCounter_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node, + uint32_t counter, uint32_t count); + using aioIob2Bi2xT44_SetIccrLed_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node, uint32_t color); + using aioIob2Bi2xT44_SetTapeLedData_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node, + uint32_t tape, const void *data); + using aioIob2Bi2x_OpenSciUsbCdc_t = AIO_SCI_COMM *(__fastcall *)(uint32_t serial_number); + using aioIob2Bi2x_CreateWriteFirmContext_t = AIO_IOB2_BI2X_WRFIRM *(__fastcall *)( + uint32_t serial_number, uint32_t iob_mask); + using aioIob2Bi2x_DestroyWriteFirmContext_t = void (__fastcall *)(AIO_IOB2_BI2X_WRFIRM *context); + using aioIob2Bi2x_WriteFirmGetState_t = int32_t (__fastcall *)(AIO_IOB2_BI2X_WRFIRM *context); + using aioIob2Bi2x_WriteFirmIsCompleted_t = bool (__fastcall *)(int32_t state); + using aioIob2Bi2x_WriteFirmIsError_t = bool (__fastcall *)(int32_t state); + using aioNMgrIob2_Create_t = AIO_NMGR_IOB2 *(__fastcall *)(AIO_SCI_COMM *sci, uint32_t mode); + using aioSci_Destroy_t = void (__fastcall *)(AIO_SCI_COMM *sci); + using aioNodeMgr_Destroy_t = void (__fastcall *)(AIO_NMGR_IOB2 *node_mgr); + using aioNodeCtl_Destroy_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node); + using aioNodeCtl_UpdateDevicesStatus_t = void (__fastcall *)(); + + static aioIob2Bi2xT44_Create_t aioIob2Bi2xT44_Create_orig = nullptr; + static aioIob2Bi2xT44_GetDeviceStatus_t aioIob2Bi2xT44_GetDeviceStatus_orig = nullptr; + static aioIob2Bi2xT44_IoReset_t aioIob2Bi2xT44_IoReset_orig = nullptr; + static aioIob2Bi2xT44_SetWatchDogTimer_t aioIob2Bi2xT44_SetWatchDogTimer_orig = nullptr; + static aioIob2Bi2xT44_ControlCoinBlocker_t aioIob2Bi2xT44_ControlCoinBlocker_orig = nullptr; + static aioIob2Bi2xT44_AddCounter_t aioIob2Bi2xT44_AddCounter_orig = nullptr; + static aioIob2Bi2xT44_SetIccrLed_t aioIob2Bi2xT44_SetIccrLed_orig = nullptr; + static aioIob2Bi2xT44_SetTapeLedData_t aioIob2Bi2xT44_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 aioIob2Bi2x_WriteFirmIsCompleted_t aioIob2Bi2x_WriteFirmIsCompleted_orig = nullptr; + static aioIob2Bi2x_WriteFirmIsError_t aioIob2Bi2x_WriteFirmIsError_orig = nullptr; + static aioNMgrIob2_Create_t aioNMgrIob2_Create_orig = nullptr; + static aioSci_Destroy_t aioSci_Destroy_orig = nullptr; + static aioNodeMgr_Destroy_t aioNodeMgr_Destroy_orig = nullptr; + static aioNodeCtl_Destroy_t aioNodeCtl_Destroy_orig = nullptr; + static aioNodeCtl_UpdateDevicesStatus_t aioNodeCtl_UpdateDevicesStatus_orig = nullptr; + + static AIO_SCI_COMM *aio_sci_comm = nullptr; + static AIO_NMGR_IOB2 *aio_node_mgr = nullptr; + static AIO_IOB2_BI2X_T44 *aio_t44 = nullptr; + static AIO_IOB2_BI2X_WRFIRM *aio_write_firm = nullptr; + static uint8_t input_counter = 0; + + static void __fastcall aioNMgrIob_BeginManage(AIO_NMGR_IOB2 *) { + } + + static AIO_NMGR_IOB2_VTABLE aio_node_mgr_vtable { + {}, + aioNMgrIob_BeginManage, + }; + + static AIO_SCI_COMM *__fastcall aioIob2Bi2x_OpenSciUsbCdc(uint32_t) { + aio_sci_comm = new AIO_SCI_COMM {}; + return aio_sci_comm; + } + + static AIO_NMGR_IOB2 *__fastcall aioNMgrIob2_Create(AIO_SCI_COMM *sci, uint32_t mode) { + if (sci != aio_sci_comm) { + return aioNMgrIob2_Create_orig(sci, mode); + } + + aio_node_mgr = new AIO_NMGR_IOB2 {}; + aio_node_mgr->vtable = &aio_node_mgr_vtable; + return aio_node_mgr; + } + + static AIO_IOB2_BI2X_T44 *__fastcall aioIob2Bi2xT44_Create( + AIO_NMGR_IOB2 *node_mgr, uint32_t device_id) { + if (node_mgr != aio_node_mgr) { + return aioIob2Bi2xT44_Create_orig(node_mgr, device_id); + } + + log_info("jb::bi2x", "T44 node created"); + aio_t44 = new AIO_IOB2_BI2X_T44 {}; + return aio_t44; + } + + static void __fastcall aioIob2Bi2xT44_GetDeviceStatus( + AIO_IOB2_BI2X_T44 *node, AIO_IOB2_BI2X_T44_DEVSTATUS *status) { + if (node != aio_t44) { + return aioIob2Bi2xT44_GetDeviceStatus_orig(node, status); + } + + RI_MGR->devices_flush_output(); + std::memset(status, 0, sizeof(*status)); + + status->data[0x00] = input_counter; + status->data[0x03] = input_counter++; + status->data[0x0A] = static_cast(eamuse_coin_get_stock()); + + games::jb::touch_update(); + const auto touched = games::jb::touch_state(); + auto &buttons = get_buttons(); + + status->data[0x04] = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Test]) ? 0xFF : 0; + status->data[0x05] = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Service]) ? 0xFF : 0; + status->data[0x06] = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::CoinMech]) ? 0xFF : 0; + + static constexpr size_t PANEL_ORDER[16] = { + 3, 7, 11, 15, + 2, 6, 10, 14, + 1, 5, 9, 13, + 0, 4, 8, 12, + }; + for (size_t status_index = 0; status_index < std::size(PANEL_ORDER); status_index++) { + const auto panel_index = PANEL_ORDER[status_index]; + if (touched[panel_index] || GameAPI::Buttons::getState( + RI_MGR, buttons[Buttons::Button1 + panel_index])) { + status->data[0x0F + status_index] = 0xFF; + } + } + } + + static void __fastcall aioIob2Bi2xT44_IoReset(AIO_IOB2_BI2X_T44 *node, uint32_t reset) { + if (node != aio_t44) { + return aioIob2Bi2xT44_IoReset_orig(node, reset); + } + } + + static void __fastcall aioIob2Bi2xT44_SetWatchDogTimer(AIO_IOB2_BI2X_T44 *node, uint8_t count) { + if (node != aio_t44) { + return aioIob2Bi2xT44_SetWatchDogTimer_orig(node, count); + } + } + + static void __fastcall aioIob2Bi2xT44_ControlCoinBlocker( + AIO_IOB2_BI2X_T44 *node, uint32_t slot, bool open) { + if (node != aio_t44) { + return aioIob2Bi2xT44_ControlCoinBlocker_orig(node, slot, open); + } + + eamuse_coin_set_block(!open); + } + + static void __fastcall aioIob2Bi2xT44_AddCounter( + AIO_IOB2_BI2X_T44 *node, uint32_t counter, uint32_t count) { + if (node != aio_t44) { + return aioIob2Bi2xT44_AddCounter_orig(node, counter, count); + } + } + + static void __fastcall aioIob2Bi2xT44_SetIccrLed(AIO_IOB2_BI2X_T44 *node, uint32_t color) { + if (node != aio_t44) { + return aioIob2Bi2xT44_SetIccrLed_orig(node, color); + } + } + + static void __fastcall aioIob2Bi2xT44_SetTapeLedData( + AIO_IOB2_BI2X_T44 *node, uint32_t tape, const void *data) { + if (node != aio_t44) { + return aioIob2Bi2xT44_SetTapeLedData_orig(node, tape, data); + } + } + + static AIO_IOB2_BI2X_WRFIRM *__fastcall aioIob2Bi2x_CreateWriteFirmContext(uint32_t, uint32_t) { + aio_write_firm = new AIO_IOB2_BI2X_WRFIRM {}; + return aio_write_firm; + } + + static void __fastcall aioIob2Bi2x_DestroyWriteFirmContext(AIO_IOB2_BI2X_WRFIRM *context) { + if (context != aio_write_firm) { + return aioIob2Bi2x_DestroyWriteFirmContext_orig(context); + } + + delete aio_write_firm; + aio_write_firm = nullptr; + } + + static int32_t __fastcall aioIob2Bi2x_WriteFirmGetState(AIO_IOB2_BI2X_WRFIRM *context) { + if (context != aio_write_firm) { + return aioIob2Bi2x_WriteFirmGetState_orig(context); + } + return 8; + } + + static bool __fastcall aioIob2Bi2x_WriteFirmIsCompleted(int32_t state) { + if (aio_write_firm) { + return true; + } + return aioIob2Bi2x_WriteFirmIsCompleted_orig(state); + } + + static bool __fastcall aioIob2Bi2x_WriteFirmIsError(int32_t state) { + if (aio_write_firm) { + return false; + } + return aioIob2Bi2x_WriteFirmIsError_orig(state); + } + + static void __fastcall aioSci_Destroy(AIO_SCI_COMM *sci) { + if (sci != aio_sci_comm) { + return aioSci_Destroy_orig(sci); + } + + delete aio_sci_comm; + aio_sci_comm = nullptr; + } + + static void __fastcall aioNodeMgr_Destroy(AIO_NMGR_IOB2 *node_mgr) { + if (node_mgr != aio_node_mgr) { + return aioNodeMgr_Destroy_orig(node_mgr); + } + + delete aio_node_mgr; + aio_node_mgr = nullptr; + } + + static void __fastcall aioNodeCtl_Destroy(AIO_IOB2_BI2X_T44 *node) { + if (node != aio_t44) { + return aioNodeCtl_Destroy_orig(node); + } + + delete aio_t44; + aio_t44 = nullptr; + } + + static void __fastcall aioNodeCtl_UpdateDevicesStatus() { + } + + void bi2x_hook_init() { + static bool initialized = false; + if (initialized) { + return; + } + initialized = true; + + log_info("jb::bi2x", "initializing T44 hooks"); + + const auto libaio_iob2_video = "libaio-iob2_video.dll"; + detour::trampoline_try(libaio_iob2_video, "aioIob2Bi2xT44_Create", + aioIob2Bi2xT44_Create, &aioIob2Bi2xT44_Create_orig); + detour::trampoline_try(libaio_iob2_video, + "?GetDeviceStatus@AIO_IOB2_BI2X_T44@@QEBAXAEAUDEVSTATUS@1@@Z", + aioIob2Bi2xT44_GetDeviceStatus, &aioIob2Bi2xT44_GetDeviceStatus_orig); + detour::trampoline_try(libaio_iob2_video, "?IoReset@AIO_IOB2_BI2X_T44@@QEAAXI@Z", + aioIob2Bi2xT44_IoReset, &aioIob2Bi2xT44_IoReset_orig); + detour::trampoline_try(libaio_iob2_video, + "?SetWatchDogTimer@AIO_IOB2_BI2X_T44@@QEAAXE@Z", + aioIob2Bi2xT44_SetWatchDogTimer, &aioIob2Bi2xT44_SetWatchDogTimer_orig); + detour::trampoline_try(libaio_iob2_video, + "?ControlCoinBlocker@AIO_IOB2_BI2X_T44@@QEAAXI_N@Z", + aioIob2Bi2xT44_ControlCoinBlocker, &aioIob2Bi2xT44_ControlCoinBlocker_orig); + detour::trampoline_try(libaio_iob2_video, "?AddCounter@AIO_IOB2_BI2X_T44@@QEAAXII@Z", + aioIob2Bi2xT44_AddCounter, &aioIob2Bi2xT44_AddCounter_orig); + detour::trampoline_try(libaio_iob2_video, "?SetIccrLed@AIO_IOB2_BI2X_T44@@QEAAXI@Z", + aioIob2Bi2xT44_SetIccrLed, &aioIob2Bi2xT44_SetIccrLed_orig); + detour::trampoline_try(libaio_iob2_video, + "?SetTapeLedData@AIO_IOB2_BI2X_T44@@QEAAXIPEBX@Z", + aioIob2Bi2xT44_SetTapeLedData, &aioIob2Bi2xT44_SetTapeLedData_orig); + + const auto libaio_iob = "libaio-iob.dll"; + detour::trampoline_try(libaio_iob, "aioIob2Bi2x_OpenSciUsbCdc", + aioIob2Bi2x_OpenSciUsbCdc, &aioIob2Bi2x_OpenSciUsbCdc_orig); + detour::trampoline_try(libaio_iob, "aioIob2Bi2x_CreateWriteFirmContext", + aioIob2Bi2x_CreateWriteFirmContext, &aioIob2Bi2x_CreateWriteFirmContext_orig); + detour::trampoline_try(libaio_iob, "aioIob2Bi2x_DestroyWriteFirmContext", + aioIob2Bi2x_DestroyWriteFirmContext, &aioIob2Bi2x_DestroyWriteFirmContext_orig); + detour::trampoline_try(libaio_iob, "aioIob2Bi2x_WriteFirmGetState", + aioIob2Bi2x_WriteFirmGetState, &aioIob2Bi2x_WriteFirmGetState_orig); + detour::trampoline_try(libaio_iob, "aioIob2Bi2x_WriteFirmIsCompleted", + aioIob2Bi2x_WriteFirmIsCompleted, &aioIob2Bi2x_WriteFirmIsCompleted_orig); + detour::trampoline_try(libaio_iob, "aioIob2Bi2x_WriteFirmIsError", + aioIob2Bi2x_WriteFirmIsError, &aioIob2Bi2x_WriteFirmIsError_orig); + detour::trampoline_try(libaio_iob, "aioNMgrIob2_Create", + aioNMgrIob2_Create, &aioNMgrIob2_Create_orig); + + const auto libaio = "libaio.dll"; + detour::trampoline_try(libaio, "aioSci_Destroy", aioSci_Destroy, &aioSci_Destroy_orig); + detour::trampoline_try(libaio, "aioNodeMgr_Destroy", + aioNodeMgr_Destroy, &aioNodeMgr_Destroy_orig); + detour::trampoline_try(libaio, "aioNodeCtl_Destroy", + aioNodeCtl_Destroy, &aioNodeCtl_Destroy_orig); + detour::trampoline_try(libaio, "aioNodeCtl_UpdateDevicesStatus", + aioNodeCtl_UpdateDevicesStatus, &aioNodeCtl_UpdateDevicesStatus_orig); + } +} + +#endif \ No newline at end of file diff --git a/src/spice2x/games/jb/bi2x_hook.h b/src/spice2x/games/jb/bi2x_hook.h new file mode 100644 index 0000000..19af798 --- /dev/null +++ b/src/spice2x/games/jb/bi2x_hook.h @@ -0,0 +1,9 @@ +#pragma once + +#if SPICE64 + +namespace games::jb { + void bi2x_hook_init(); +} + +#endif \ No newline at end of file diff --git a/src/spice2x/games/jb/jb.cpp b/src/spice2x/games/jb/jb.cpp index 331b546..cbbbd79 100644 --- a/src/spice2x/games/jb/jb.cpp +++ b/src/spice2x/games/jb/jb.cpp @@ -3,6 +3,8 @@ #include #include +#include "avs/game.h" +#include "bi2x_hook.h" #include "cfg/configurator.h" #include "util/logging.h" #include "util/detour.h" @@ -10,6 +12,8 @@ namespace games::jb { +#if !SPICE64 + // fixes "IP ADDR CHANGE" errors with unusual network setups (e.g. a VPN) static BOOL __stdcall network_addr_is_changed() { return 0; @@ -31,6 +35,8 @@ namespace games::jb { return 0; } +#endif + JBGame::JBGame() : Game("Jubeat") { } @@ -60,8 +66,18 @@ namespace games::jb { void JBGame::attach() { Game::attach(); +#if SPICE64 + if (avs::game::DLL_NAME == "jubeat2019.dll") { + libutils::load_library("libaio.dll"); + libutils::load_library("libaio-iob.dll"); + libutils::load_library("libaio-iob2_video.dll"); + bi2x_hook_init(); + } +#endif + touch_attach(); +#if !SPICE64 // enable debug logging of gftools HMODULE gftools = libutils::try_module("gftools.dll"); detour::inline_hook((void *) GFDbgSetReportFunc, libutils::try_proc( @@ -75,6 +91,8 @@ namespace games::jb { network, "network_get_network_check_info")); detour::inline_hook((void *) network_get_dhcp_result, libutils::try_proc( network, "network_get_dhcp_result")); +#endif + } void JBGame::detach() { diff --git a/src/spice2x/games/jb/jb_touch.cpp b/src/spice2x/games/jb/jb_touch.cpp index 4798643..f5c13b8 100644 --- a/src/spice2x/games/jb/jb_touch.cpp +++ b/src/spice2x/games/jb/jb_touch.cpp @@ -20,6 +20,8 @@ // (6, 8) in landscape. #define JB_BUTTON_SIZE 160 #define JB_MAX_BUTTON_GAP 38 +#define JB_T44_BUTTON_SIZE 224 +#define JB_T44_BUTTON_GAP 33 // improved and plus modes use this reach around each button. must be >= the // diagonal half of the widest gap (~27px) so the grid centre still reaches a button. @@ -41,6 +43,7 @@ namespace games::jb { static std::atomic_bool TOUCH_ENABLE = false; static bool TOUCH_ATTACHED = false; static bool IS_PORTRAIT = true; + static bool IS_T44 = false; static std::atomic_uint16_t TOUCH_STATE = 0; // fixed-size contact view used by the debug overlay @@ -85,35 +88,45 @@ namespace games::jb { // --- touch geometry ------------------------------------------------------ // gaps between the four buttons along one axis (the middle gap is 1px wider) static const int JB_BUTTON_GAPS[3] = { 37, JB_MAX_BUTTON_GAP, 37 }; + static const int JB_T44_BUTTON_GAPS[3] = { + JB_T44_BUTTON_GAP, + JB_T44_BUTTON_GAP, + JB_T44_BUTTON_GAP, + }; struct AxisGeometry { + int size; int button[4]; // left/top edge of each button }; // left/top edges of the four buttons along one axis, starting at `first` - static AxisGeometry axis_geometry(int first) { + static AxisGeometry axis_geometry(int first, int size, const int gaps[3]) { AxisGeometry g {}; + g.size = size; g.button[0] = first; for (int i = 1; i < 4; i++) { - g.button[i] = g.button[i - 1] + JB_BUTTON_SIZE + JB_BUTTON_GAPS[i - 1]; + g.button[i] = g.button[i - 1] + size + gaps[i - 1]; } return g; } // button edges for the current orientation static void touch_geometry(AxisGeometry &gx, AxisGeometry &gy) { - if (IS_PORTRAIT) { - gx = axis_geometry(8); - gy = axis_geometry(602); + if (IS_T44) { + gx = axis_geometry(37, JB_T44_BUTTON_SIZE, JB_T44_BUTTON_GAPS); + gy = axis_geometry(864, JB_T44_BUTTON_SIZE, JB_T44_BUTTON_GAPS); + } else if (IS_PORTRAIT) { + gx = axis_geometry(8, JB_BUTTON_SIZE, JB_BUTTON_GAPS); + gy = axis_geometry(602, JB_BUTTON_SIZE, JB_BUTTON_GAPS); } else { - gx = axis_geometry(6); - gy = axis_geometry(8); + gx = axis_geometry(6, JB_BUTTON_SIZE, JB_BUTTON_GAPS); + gy = axis_geometry(8, JB_BUTTON_SIZE, JB_BUTTON_GAPS); } } // distance from `p` to a button along one axis (0 when inside) - static int axis_distance(int p, int button) { - int end = button + JB_BUTTON_SIZE - 1; + static int axis_distance(int p, int button, int size) { + int end = button + size - 1; if (p < button) { return button - p; } @@ -131,8 +144,8 @@ namespace games::jb { int best_dist = 0; for (int r = 0; r < 4; r++) { for (int c = 0; c < 4; c++) { - int dx = axis_distance(px, gx.button[c]); - int dy = axis_distance(py, gy.button[r]); + int dx = axis_distance(px, gx.button[c], gx.size); + int dy = axis_distance(py, gy.button[r], gy.size); int dist = dx * dx + dy * dy; if (dist <= radius * radius && (best_index < 0 || dist < best_dist)) { best_dist = dist; @@ -145,7 +158,8 @@ namespace games::jb { // detection reach for the current algorithm (0 = register only inside a button) static int touch_radius() { - return TOUCH_ALGORITHM == AcAccurate ? 0 : JB_TOUCH_RADIUS; + return TOUCH_ALGORITHM == AcAccurate || (IS_T44 && TOUCH_ALGORITHM == Legacy) ? + 0 : JB_TOUCH_RADIUS; } // mark the buttons a touch at (px, py) hits: only the nearest within `radius`, or @@ -162,8 +176,8 @@ namespace games::jb { } for (int r = 0; r < 4; r++) { for (int c = 0; c < 4; c++) { - int dx = axis_distance(px, gx.button[c]); - int dy = axis_distance(py, gy.button[r]); + int dx = axis_distance(px, gx.button[c], gx.size); + int dy = axis_distance(py, gy.button[r], gy.size); if (dx * dx + dy * dy <= radius * radius) { state |= uint16_t(1) << (r * 4 + c); } @@ -184,11 +198,15 @@ namespace games::jb { // one-time touch window attach if (!TOUCH_ATTACHED) { + IS_T44 = avs::game::is_model("T44"); + IS_PORTRAIT = IS_T44 || avs::game::is_model("L44"); + // find the game window: prefer the foreground window, else search by - // title (the model name prefixes the window title in every version) + // title (T44 uses a fixed title instead of the model prefix) + const char *window_title = IS_T44 ? "jubeat 10 main" : avs::game::MODEL; HWND wnd = GetForegroundWindow(); - if (!string_begins_with(GetActiveWindowTitle(), avs::game::MODEL)) { - wnd = FindWindowBeginsWith(avs::game::MODEL); + if (!string_begins_with(GetActiveWindowTitle(), window_title)) { + wnd = FindWindowBeginsWith(window_title); } if (!wnd) { log_warning("jubeat", "could not find window handle for touch"); @@ -197,10 +215,6 @@ namespace games::jb { return; } - // only the L44 model runs in portrait; set this before starting the - // touch-window thread so the renderer only observes the final value - IS_PORTRAIT = avs::game::is_model("L44"); - log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd)); // let the rawinput stack correct the aspect ratio @@ -232,7 +246,7 @@ namespace games::jb { return !touch_matured(tp, now_ms, threshold_ms); }); - if (TOUCH_ALGORITHM == Legacy) { + if (TOUCH_ALGORITHM == Legacy && !IS_T44) { // legacy: evenly divide the play area into a 4x4 grid auto offset = IS_PORTRAIT ? 580 : 0; @@ -311,7 +325,7 @@ namespace games::jb { for (int c = 0; c < 4; c++) { cells[r * 4 + c] = { gx.button[c], gy.button[r], - gx.button[c] + JB_BUTTON_SIZE, gy.button[r] + JB_BUTTON_SIZE + gx.button[c] + gx.size, gy.button[r] + gy.size }; } } @@ -352,8 +366,8 @@ namespace games::jb { const AxisGeometry &gx, const AxisGeometry &gy, int arc_radius) { int c = index % 4; int r = index / 4; - double mid = std::atan2((gy.button[r] + JB_BUTTON_SIZE / 2) - py, - (gx.button[c] + JB_BUTTON_SIZE / 2) - px); + double mid = std::atan2((gy.button[r] + gy.size / 2) - py, + (gx.button[c] + gx.size / 2) - px); const double quarter = 3.14159265358979323846 / 2.0; const int segments = 16; POINT arc[segments + 1]; @@ -425,7 +439,7 @@ namespace games::jb { } // legacy divides the field evenly; the other algorithms use button squares - bool legacy = (TOUCH_ALGORITHM == Legacy); + bool legacy = TOUCH_ALGORITHM == Legacy && !IS_T44; AxisGeometry gx {}, gy {}; if (!legacy) { touch_geometry(gx, gy); diff --git a/src/spice2x/hooks/lang.cpp b/src/spice2x/hooks/lang.cpp index fb94aba..99a4f32 100644 --- a/src/spice2x/hooks/lang.cpp +++ b/src/spice2x/hooks/lang.cpp @@ -296,8 +296,9 @@ void hooks::lang::early_init() { &GetLocaleInfoA_orig); } - // for TDJ subscreen search keyboard - if (avs::game::is_model("LDJ") && games::iidx::TDJ_MODE) { + // for TDJ subscreen search keyboard and T44 narrow-string handling + if ((avs::game::is_model("LDJ") && games::iidx::TDJ_MODE) || + avs::game::is_model("T44")) { log_info("hooks::lang", "hooking IsDBCSLeadByte"); detour::trampoline_try( "kernel32.dll", @@ -306,7 +307,7 @@ void hooks::lang::early_init() { &IsDBCSLeadByte_orig); } - if (games::gitadora::is_arena_model()) { + if (games::gitadora::is_arena_model() || avs::game::is_model("T44")) { log_info("hooks::lang", "hooking WideCharToMultiByte"); detour::trampoline_try( "kernel32.dll", diff --git a/src/spice2x/launcher/launcher.cpp b/src/spice2x/launcher/launcher.cpp index 385a540..233b33e 100644 --- a/src/spice2x/launcher/launcher.cpp +++ b/src/spice2x/launcher/launcher.cpp @@ -1855,6 +1855,13 @@ int main_implementation(int argc, char *argv[]) { break; } + if (check_dll("jubeat2019.dll")) { + avs::game::DLL_NAME = "jubeat2019.dll"; + attach_io = true; + attach_jb = true; + break; + } + // RB if (check_dll("reflecbeat.dll")) { avs::game::DLL_NAME = "reflecbeat.dll"; @@ -2283,7 +2290,7 @@ int main_implementation(int argc, char *argv[]) { games.push_back(new games::sdvx::SDVXGame()); } if (attach_jb) { - avs::core::set_default_heap_size("jubeat.dll"); + avs::core::set_default_heap_size(avs::game::DLL_NAME); games.push_back(new games::jb::JBGame()); } if (attach_nostalgia) { diff --git a/src/spice2x/misc/eamuse.cpp b/src/spice2x/misc/eamuse.cpp index a19c9c0..a89d2ad 100644 --- a/src/spice2x/misc/eamuse.cpp +++ b/src/spice2x/misc/eamuse.cpp @@ -673,7 +673,8 @@ void eamuse_autodetect_game() { eamuse_set_game("Beatmania IIDX"); else if (avs::game::is_model("J44") || avs::game::is_model("K44") || - avs::game::is_model("L44")) + avs::game::is_model("L44") || + avs::game::is_model("T44")) eamuse_set_game("Jubeat"); else if (avs::game::is_model("KDM")) eamuse_set_game("Dance Evolution");