// enable touch functions - set version to windows 7 // mingw otherwise doesn't load touch stuff #define _WIN32_WINNT 0x0601 #include #include #include #include "touch.h" #include "avs/game.h" #include "external/imgui/imgui.h" #include "hooks/graphics/graphics.h" #include "misc/eamuse.h" #include "overlay/overlay.h" #include "rawinput/touch.h" #include "util/circular_buffer.h" #include "util/detour.h" #include "util/libutils.h" #include "util/logging.h" #include "util/utils.h" #include "handler.h" #include "win7.h" #include "win8.h" // constants const char *SPICETOUCH_CLASS_NAME = "spiceTouchClass"; const char *SPICETOUCH_FONT_NAME = "Courier New"; const char *INSERT_CARD_TEXT = "Insert Card"; // settings static const int TOUCH_EVENT_BUFFER_SIZE = 1024 * 4; static const int TOUCH_EVENT_BUFFER_THRESHOLD1 = 1024 * 2; static const int TOUCH_EVENT_BUFFER_THRESHOLD2 = 1024 * 3; // in mainline spicetools, this was false (show by default) // in spice2x, this is true (hide by default) bool SPICETOUCH_CARD_DISABLE = true; HWND SPICETOUCH_TOUCH_HWND = nullptr; int SPICETOUCH_TOUCH_X = 0; int SPICETOUCH_TOUCH_Y = 0; int SPICETOUCH_TOUCH_WIDTH = 0; int SPICETOUCH_TOUCH_HEIGHT = 0; // touch states std::vector TOUCH_POINTS; std::mutex TOUCH_POINTS_M; static circular_buffer TOUCH_EVENTS(TOUCH_EVENT_BUFFER_SIZE); std::mutex TOUCH_EVENTS_M; // general states static bool SPICETOUCH_INITIALIZED = false; static bool SPICETOUCH_ATTACHED = false; static bool SPICETOUCH_ATTACHED_DXHOOK = false; static HWND SPICETOUCH_HWND = nullptr; static WNDPROC SPICETOUCH_OLD_PROC = nullptr; static bool SPICETOUCH_ENABLE_MOUSE = true; static bool SPICETOUCH_REGISTERED_TOUCH = false; static bool SPICETOUCH_CALL_OLD_PROC = false; static std::thread *SPICETOUCH_TOUCH_THREAD = nullptr; static HFONT SPICETOUCH_FONT; static RECT SPICETOUCH_CARD_RECT; static bool SPICETOUCH_CARD_ENABLED = false; static const char *LOG_MODULE_NAME = "touch"; static TouchHandler *TOUCH_HANDLER = nullptr; TouchHandler::TouchHandler(std::string name) { log_info("touch", "Using touch handler: {}", name); } class RawInputTouchHandler : public TouchHandler { public: RawInputTouchHandler() : TouchHandler("rawinput") { } virtual bool window_register(HWND hWnd) override { return true; } virtual bool window_unregister(HWND hWnd) override { return true; } virtual void handle_message(msg_handler_result&, HWND, UINT, WPARAM, LPARAM) override { } }; /* * Add touch event but take care of buffer size * Be careful, this doesn't lock the mutex on it's own */ void add_touch_event(TouchEvent *te) { // check if first threshold is passed if (TOUCH_EVENTS.size() > TOUCH_EVENT_BUFFER_THRESHOLD1) { switch (te->type) { case TOUCH_DOWN: // ignore touch down events after first threshold return; case TOUCH_MOVE: // add move event if we're not over the second threshold if (TOUCH_EVENTS.size() <= TOUCH_EVENT_BUFFER_THRESHOLD2) { TOUCH_EVENTS.put(*te); } return; case TOUCH_UP: // check if buffer is full if (TOUCH_EVENTS.full()) { // ignore event return; } // when the buffer isn't full yet, add the touch up event TOUCH_EVENTS.put(*te); return; default: return; } } // add the touch up event TOUCH_EVENTS.put(*te); } static void touch_initialize() { // check if already initialized if (SPICETOUCH_INITIALIZED) { return; } SPICETOUCH_INITIALIZED = true; // initialize handler if (RI_MGR && rawinput::touch::is_enabled(RI_MGR.get())) { TOUCH_HANDLER = new RawInputTouchHandler(); } else if (Win8Handler::is_available()) { TOUCH_HANDLER = new Win8Handler(); } else if (Win7Handler::is_available()) { TOUCH_HANDLER = new Win7Handler(); } else { log_warning(LOG_MODULE_NAME, "no touch handler available"); } } static inline void touch_register_window(HWND hWnd) { // register touch handling for window if (TOUCH_HANDLER != nullptr) { TOUCH_HANDLER->window_register(hWnd); } } static inline void touch_unregister_window(HWND hWnd) { // unregister touch handling for window if (TOUCH_HANDLER != nullptr) { TOUCH_HANDLER->window_unregister(hWnd); } } bool is_touch_available() { // initialize touch handler touch_initialize(); // Check if a touch handler has been set. No need to call `is_available` here // as `touch_initialize` does that. return TOUCH_HANDLER != nullptr; } void update_card_button() { // check if enabled if (!SPICETOUCH_CARD_ENABLED) { return; } // check touch points for (TouchPoint touchPoint : TOUCH_POINTS) { POINT pt {}; pt.x = touchPoint.x; pt.y = touchPoint.y; if (PtInRect(&SPICETOUCH_CARD_RECT, pt) != 0) { eamuse_card_insert(0); } } } static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { // check if touch was registered if (!SPICETOUCH_REGISTERED_TOUCH) { SPICETOUCH_REGISTERED_TOUCH = true; // check if touch is available if (is_touch_available()) { // notify the handler of our window TOUCH_HANDLER->window_register(hWnd); // enable card unless the feature is disabled if (!SPICETOUCH_CARD_DISABLE) { SPICETOUCH_CARD_ENABLED = true; } } } // update rawinput touch display resolution if (msg == WM_DISPLAYCHANGE) { log_info("touch", "detected display mode change"); rawinput::touch::display_update(); } if (msg == WM_CLOSE) { if ((GRAPHICS_IIDX_WSUB && hWnd == TDJ_SUBSCREEN_WINDOW) || (hWnd == SDVX_SUBSCREEN_WINDOW)) { log_misc("touch", "ignore WM_CLOSE for subscreen window"); return false; } } // check messages for dedicated window if (SPICETOUCH_TOUCH_THREAD != nullptr) { // check if overlay is enabled auto overlay_enabled = overlay::OVERLAY && overlay::OVERLAY->renderer == overlay::OverlayRenderer::SOFTWARE; // we don't want mouse events to interfere SPICETOUCH_ENABLE_MOUSE = !overlay_enabled || !overlay::OVERLAY->get_active(); switch (msg) { case WM_MOUSEACTIVATE: { // Set the main window as the active window to reactivate the imgui cursor HWND parent = GetParent(hWnd); if (GetActiveWindow() != parent) { SetActiveWindow(parent); } return MA_NOACTIVATE; } case WM_SETCURSOR: { // set cursor back to the overlay one if (overlay_enabled && LOWORD(lParam) == HTCLIENT && overlay::OVERLAY->update_cursor()) { //return true; TODO: can make cursor invisible? } break; } case WM_TIMER: { InvalidateRect(hWnd, NULL, TRUE); break; } case WM_PAINT: { // get window rect HWND parent = GetParent(hWnd); RECT windowRect {}, clientRect {}; GetWindowRect(parent, &windowRect); GetClientRect(parent, &clientRect); // adjust to client area POINT p1 {.x = clientRect.left, .y = clientRect.top}; POINT p2 {.x = clientRect.right, .y = clientRect.bottom}; ClientToScreen(parent, &p1); ClientToScreen(parent, &p2); windowRect.left = p1.x; windowRect.top = p1.y; windowRect.right = p2.x; windowRect.bottom = p2.y; // check if rect needs to update RECT windowRectOld {}; GetWindowRect(hWnd, &windowRectOld); if (memcmp(&windowRectOld, &windowRect, sizeof(RECT)) != 0) { SetWindowPos(hWnd, HWND_TOP, windowRect.left, windowRect.top, windowRect.right - windowRect.left, windowRect.bottom - windowRect.top, SWP_NOZORDER | SWP_NOREDRAW | SWP_NOREPOSITION | SWP_NOACTIVATE); } // draw overlay if (overlay_enabled) { // update and render overlay::OVERLAY->update(); overlay::OVERLAY->new_frame(); overlay::OVERLAY->render(); // get pixel data int width, height; uint32_t *pixel_data = overlay::OVERLAY.get()->sw_get_pixel_data(&width, &height); if (width > 0 && height > 0) { // create bitmap HBITMAP bitmap = CreateBitmap(width, height, 1, 8 * sizeof(uint32_t), pixel_data); // prepare paint PAINTSTRUCT paint {}; HDC hdc = BeginPaint(hWnd, &paint); HDC hdcMem = CreateCompatibleDC(hdc); SetBkMode(hdc, TRANSPARENT); /* * draw bitmap * - this currently sets the background to black because of SRCCOPY * - SRCPAINT will blend but colors are wrong * - once this is figured out we could also try hooking WM_PAINT and * draw directly to the game window */ SelectObject(hdcMem, bitmap); BitBlt(hdc, 0, 0, width, height, hdcMem, 0, 0, SRCCOPY); // clean up DeleteObject(bitmap); DeleteDC(hdcMem); EndPaint(hWnd, &paint); } } // draw card input if (SPICETOUCH_CARD_ENABLED && (SPICETOUCH_FONT != nullptr)) { // prepare paint PAINTSTRUCT paint {}; HDC hdc = BeginPaint(hWnd, &paint); SetBkMode(hdc, TRANSPARENT); // create brushes HBRUSH brushBorder = CreateSolidBrush(RGB(0, 196, 0)); HBRUSH brushFill = CreateSolidBrush(RGB(255, 192, 203)); // get window rect RECT windowRect {}; GetWindowRect(hWnd, &windowRect); bool should_rotate = avs::game::is_model({ "J44", "K44" }); // create box rect RECT boxRect {}; if (should_rotate) { boxRect.left = windowRect.right - 75; boxRect.top = 20; boxRect.right = windowRect.right - 44; boxRect.bottom = 151; } else { boxRect.left = 20; boxRect.top = 44; boxRect.right = 141; boxRect.bottom = 75; } // save box rect for touch input SPICETOUCH_CARD_RECT = boxRect; // draw borders FillRect(hdc, &boxRect, brushBorder); // modify box rect boxRect.left += 1; boxRect.top += 1; boxRect.right -= 1; boxRect.bottom -= 1; // fill box FillRect(hdc, &boxRect, brushFill); // modify box rect if (should_rotate) { boxRect.left += 5 + 20; boxRect.top += 5; } else { boxRect.left += 5; boxRect.top += 5; } // draw text SelectObject(hdc, SPICETOUCH_FONT); SetTextColor(hdc, RGB(0, 196, 0)); DrawText(hdc, INSERT_CARD_TEXT, -1, &boxRect, DT_LEFT | DT_BOTTOM | DT_NOCLIP); // delete objects DeleteObject(brushFill); DeleteObject(brushBorder); // end paint EndPaint(hWnd, &paint); return 0; } // call default window procedure return DefWindowProc(hWnd, msg, wParam, lParam); } case WM_CREATE: { // set to layered window SetWindowLong(hWnd, GWL_EXSTYLE, GetWindowLong(hWnd, GWL_EXSTYLE) | WS_EX_LAYERED); // set alpha value SetLayeredWindowAttributes(hWnd, 0, 0xFFu, LWA_ALPHA); SetLayeredWindowAttributes(hWnd, RGB(255, 192, 203), 0, LWA_COLORKEY); // get window rect RECT windowRect {}; GetWindowRect(hWnd, &windowRect); bool should_rotate = avs::game::is_model({ "J44", "K44" }); auto rotation = should_rotate ? 2700 : 0; // load font SPICETOUCH_FONT = CreateFont(20, 0, rotation, rotation, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, NONANTIALIASED_QUALITY, DEFAULT_PITCH, SPICETOUCH_FONT_NAME); if (SPICETOUCH_FONT == nullptr) { log_warning(LOG_MODULE_NAME, "font '{}' could not be loaded", SPICETOUCH_FONT_NAME); } return 0; } case WM_DESTROY: { PostQuitMessage(0); return 0; } default: break; } } msg_handler_result result { .action = ACTION_PASS, .return_value = 0, }; // check if imgui is handling this mouse event if (overlay::OVERLAY != nullptr && overlay::OVERLAY->get_active() && ImGui::GetIO().WantCaptureMouse) { result.action = ACTION_RETURN_DEFAULT; } else if (TOUCH_HANDLER != nullptr) { // call touch handler TOUCH_HANDLER->handle_message(result, hWnd, msg, wParam, lParam); } if (result.action == ACTION_RETURN_STORED) { // return the value from the touch handler return result.return_value; } if (result.action == ACTION_PASS) { // parse mouse messages switch (msg) { case WM_LBUTTONDOWN: { // check if mouse is enabled if (SPICETOUCH_ENABLE_MOUSE) { // lock touch points std::lock_guard lock_points(TOUCH_POINTS_M); std::lock_guard lock_events(TOUCH_EVENTS_M); // remove all points with ID 0 for (size_t x = 0; x < TOUCH_POINTS.size(); x++) { TouchPoint *tp = &TOUCH_POINTS[x]; if (tp->id == 0u) { // generate touch up event TouchEvent te { .id = tp->id, .x = tp->x, .y = tp->y, .type = TOUCH_UP, .mouse = tp->mouse, }; add_touch_event(&te); // erase touch point TOUCH_POINTS.erase(TOUCH_POINTS.begin() + x); } } // create touch point TouchPoint tp { .id = 0, .x = GET_X_LPARAM(lParam), .y = GET_Y_LPARAM(lParam), .mouse = true, }; TOUCH_POINTS.push_back(tp); // add touch down event TouchEvent te { .id = tp.id, .x = tp.x, .y = tp.y, .type = TOUCH_DOWN, .mouse = tp.mouse, }; add_touch_event(&te); // card button update_card_button(); } break; } case WM_MOUSEMOVE: { // check if mouse is enabled if (SPICETOUCH_ENABLE_MOUSE) { // lock touch points std::lock_guard lock_points(TOUCH_POINTS_M); std::lock_guard lock_events(TOUCH_EVENTS_M); // update point for (auto &x : TOUCH_POINTS) { TouchPoint *tp = &x; // find ID 0 if (tp->id == 0u) { // update touch point position tp->x = GET_X_LPARAM(lParam); tp->y = GET_Y_LPARAM(lParam); // add touch move event TouchEvent te { .id = tp->id, .x = tp->x, .y = tp->y, .type = TOUCH_MOVE, .mouse = tp->mouse, }; add_touch_event(&te); break; } } } break; } case WM_LBUTTONUP: { // check if mouse is enabled if (SPICETOUCH_ENABLE_MOUSE) { // lock touch points std::lock_guard lock_points(TOUCH_POINTS_M); std::lock_guard lock_events(TOUCH_EVENTS_M); // remove all points with ID 0 for (size_t x = 0; x < TOUCH_POINTS.size(); x++) { TouchPoint *tp = &TOUCH_POINTS[x]; if (tp->id == 0u) { // generate touch up event TouchEvent te { .id = tp->id, .x = tp->x, .y = tp->y, .type = TOUCH_UP, .mouse = tp->mouse, }; add_touch_event(&te); // remove touch point TOUCH_POINTS.erase(TOUCH_POINTS.begin() + x); } } } break; } default: // call original function if (SPICETOUCH_CALL_OLD_PROC && SPICETOUCH_OLD_PROC != nullptr) { return SPICETOUCH_OLD_PROC(hWnd, msg, wParam, lParam); } } } // clean up if (SPICETOUCH_ATTACHED_DXHOOK) { return 0; } else { return DefWindowProc(hWnd, msg, wParam, lParam); } } void touch_attach_wnd(HWND hWnd) { // check if already attached if (SPICETOUCH_ATTACHED) { return; } SPICETOUCH_ATTACHED = true; // initialize touch handler touch_initialize(); touch_register_window(hWnd); // hook window process SPICETOUCH_HWND = hWnd; SPICETOUCH_OLD_PROC = (WNDPROC) SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR) SpiceTouchWndProc); // update rawinput rawinput::touch::display_update(); } void touch_attach_dx_hook() { // check if already attached if (SPICETOUCH_ATTACHED) { return; } SPICETOUCH_ATTACHED = true; SPICETOUCH_ATTACHED_DXHOOK = true; // initialize touch handler touch_initialize(); // add dx hook graphics_add_wnd_proc(SpiceTouchWndProc); // update rawinput rawinput::touch::display_update(); } void touch_create_wnd(HWND hWnd, bool overlay) { // check if already attached if (SPICETOUCH_ATTACHED) { return; } SPICETOUCH_ATTACHED = true; // initialize touch handler touch_initialize(); // create thread SPICETOUCH_TOUCH_THREAD = new std::thread([hWnd, overlay]() { // create class WNDCLASSEX wndClass {}; wndClass.cbSize = sizeof(WNDCLASSEX); wndClass.style = 3; wndClass.lpfnWndProc = SpiceTouchWndProc; wndClass.cbClsExtra = 0; wndClass.cbWndExtra = 0; wndClass.hInstance = GetModuleHandle(nullptr); wndClass.hIcon = nullptr; wndClass.hCursor = LoadCursor(nullptr, IDC_ARROW); wndClass.hbrBackground = CreateSolidBrush(RGB(255, 192, 203)); wndClass.lpszMenuName = nullptr; wndClass.lpszClassName = SPICETOUCH_CLASS_NAME; wndClass.hIconSm = nullptr; // register class if (!RegisterClassExA(&wndClass)) { log_warning(LOG_MODULE_NAME, "failed to register SpiceTouch class: {}", GetLastError()); return; } // calculate touch window dimensions update_spicetouch_window_dimensions(hWnd); // create window HWND touch_window = CreateWindowExA( 0, SPICETOUCH_CLASS_NAME, "SpiceTools Touch", (DWORD) CW_USEDEFAULT, SPICETOUCH_TOUCH_X, SPICETOUCH_TOUCH_Y, SPICETOUCH_TOUCH_WIDTH, SPICETOUCH_TOUCH_HEIGHT, hWnd, nullptr, GetModuleHandle(nullptr), nullptr ); log_misc( LOG_MODULE_NAME, "create SpiceTouch window ({}x{} @ {}, {})", SPICETOUCH_TOUCH_WIDTH, SPICETOUCH_TOUCH_HEIGHT, SPICETOUCH_TOUCH_X, SPICETOUCH_TOUCH_Y); // check window if (touch_window == nullptr) { log_warning(LOG_MODULE_NAME, "failed to create SpiceTouch window: {}", GetLastError()); return; } // save reference SPICETOUCH_TOUCH_HWND = touch_window; // window settings ShowWindow(touch_window, SW_SHOWNOACTIVATE); UpdateWindow(touch_window); // register touch_register_window(touch_window); // overlay if (overlay && overlay::ENABLED) { if (overlay::OVERLAY) { log_warning(LOG_MODULE_NAME, "requested overlay, but already existing"); } else { // create instance overlay::OVERLAY.reset(new overlay::SpiceOverlay(touch_window)); // draw overlay in 30 FPS SetTimer(touch_window, 1, 1000 / 30, NULL); } } // window loop MSG msg {}; while (GetMessage(&msg, nullptr, 0, 0) && SPICETOUCH_TOUCH_THREAD != nullptr) { TranslateMessage(&msg); DispatchMessage(&msg); } // kill overlay if (overlay) { overlay::OVERLAY.reset(); } // unregister touch_unregister_window(touch_window); log_misc(LOG_MODULE_NAME, "window closed"); }); // update rawinput rawinput::touch::display_update(); } void touch_detach() { // remove window process hook if (SPICETOUCH_HWND != nullptr) { touch_unregister_window(SPICETOUCH_HWND); SetWindowLongPtr(SPICETOUCH_HWND, GWLP_WNDPROC, (LONG_PTR) SPICETOUCH_OLD_PROC); SPICETOUCH_HWND = nullptr; SPICETOUCH_OLD_PROC = nullptr; } // remove dx hook graphics_remove_wnd_proc(SpiceTouchWndProc); SPICETOUCH_TOUCH_THREAD = nullptr; SPICETOUCH_ATTACHED_DXHOOK = false; SPICETOUCH_ATTACHED = false; } void touch_write_points(std::vector *touch_points) { // check size first if (touch_points->empty()) { return; } // lock std::lock_guard points(TOUCH_POINTS_M); std::lock_guard events(TOUCH_EVENTS_M); // iterate through all the provided touch points for (auto &tp : *touch_points) { // find touch point to update bool found = false; for (auto &arr_tp : TOUCH_POINTS) { if (arr_tp.id == tp.id) { // mark as found found = true; // update position arr_tp.x = tp.x; arr_tp.y = tp.y; // add touch move event TouchEvent te { .id = tp.id, .x = tp.x, .y = tp.y, .type = TOUCH_MOVE, .mouse = tp.mouse, }; add_touch_event(&te); } } // create new touch point when not found if (!found) { // add touch point TOUCH_POINTS.push_back(tp); // add touch down event TouchEvent te { .id = tp.id, .x = tp.x, .y = tp.y, .type = TOUCH_DOWN, .mouse = tp.mouse, }; add_touch_event(&te); } } } void touch_remove_points(std::vector *touch_point_ids) { // check size first if (touch_point_ids->empty()) { return; } // lock std::lock_guard points(TOUCH_POINTS_M); std::lock_guard events(TOUCH_EVENTS_M); // find the touch points to remove for (auto id : *touch_point_ids) { for (size_t x = 0; x < TOUCH_POINTS.size(); x++) { TouchPoint *tp = &TOUCH_POINTS[x]; // check if the IDs match if (tp->id == id) { // add touch up event TouchEvent te { .id = id, .x = tp->x, .y = tp->y, .type = TOUCH_UP, .mouse = tp->mouse, }; add_touch_event(&te); // delete touch point TOUCH_POINTS.erase(TOUCH_POINTS.begin() + x); break; } } } } void touch_get_points(std::vector &touch_points, bool overlay) { // update timeouts if (RI_MGR) { rawinput::touch::update_timeouts(RI_MGR.get()); } // overlay override if (!overlay && overlay::OVERLAY && overlay::OVERLAY->get_active() && !overlay::OVERLAY->can_transform_touch_input() && ImGui::GetIO().WantCaptureMouse) { return; } // lock std::lock_guard lock(TOUCH_POINTS_M); // append touch points touch_points.insert(touch_points.end(), TOUCH_POINTS.begin(), TOUCH_POINTS.end()); } void touch_get_events(std::vector &touch_events, bool overlay) { // update timeouts if (RI_MGR) { rawinput::touch::update_timeouts(RI_MGR.get()); } // lock std::lock_guard lock(TOUCH_EVENTS_M); // overlay override if (!overlay && overlay::OVERLAY && overlay::OVERLAY->get_active() && !overlay::OVERLAY->can_transform_touch_input() && ImGui::GetIO().WantCaptureMouse) { TOUCH_EVENTS.reset(); return; } // append touch points while (!TOUCH_EVENTS.empty()) { touch_events.push_back(TOUCH_EVENTS.get()); } } void update_spicetouch_window_dimensions(HWND hWnd) { RECT clientRect {}; GetClientRect(hWnd, &clientRect); // adjust to client area POINT topleft {.x = 0, .y = 0}; ClientToScreen(hWnd, &topleft); POINT bottomright {.x = clientRect.right, .y = clientRect.bottom}; ClientToScreen(hWnd, &bottomright); SPICETOUCH_TOUCH_X = topleft.x; SPICETOUCH_TOUCH_Y = topleft.y; SPICETOUCH_TOUCH_WIDTH = bottomright.x - topleft.x; SPICETOUCH_TOUCH_HEIGHT = bottomright.y - topleft.y; }