From 0e76dd832a7125ba3415da246b1ebd826e389ab9 Mon Sep 17 00:00:00 2001 From: kyoubate-haruka <46010460+kyoubate-haruka@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:41:40 +0000 Subject: [PATCH] diva: input rewrite, dinput/xinput, button alternation, auto-slide, wintouch, printer (#129) * Rewrote the input handler Added support for dinput and xinput, full keyboard slider cell binding (#127), hold-takeovers, button alternation and auto-sliding (PS4 behaviour). See segatools.ini for a detailed description about everything. * Split touch handling into backends and added Wintouch touch emulation Now you can use actual touchscreens as well as mouse emulation was not the greatest when trying to use an actual touchscreen. * Fixed input issues gamebtns was always uninitialized, so good luck trying to do anything. How did nobody ever complain about this? lol * Fixed resolution issues caused by chaining both gfx hooks The diva-specific hook is the only one we need. Stacking both has strange effects. * Added support for the MITSUBISHI CP9550DS printer Q: Why? A: /shrug ![https://puu.sh/KQlev/ce2ef0efed.bmp](https://puu.sh/KQlev/ce2ef0efed.bmp) I am not particularly good at this game (or the pass requirements are insane, 23+12 misses out of ~550 is a fail!?), so if someone could test how this feels as opposed to the PS4 version, that would help a lot! ![https://puu.sh/KQldU/fa4c848ff9.png](https://puu.sh/KQldU/fa4c848ff9.png) Also something that happens but that I can't reliably reproduce is that my aime card reader randomly becomes NG after a dozen game starts until I delete eeprom.bin. If that is the case, the only packet sent is SEND_HEX_DATA. Reviewed-on: https://gitea.tendokyu.moe/TeamTofuShop/segatools/pulls/129 Co-authored-by: kyoubate-haruka <46010460+kyoubate-haruka@users.noreply.github.com> Co-committed-by: kyoubate-haruka <46010460+kyoubate-haruka@users.noreply.github.com> --- common/hooklib/config.c | 32 +++ common/hooklib/config.h | 2 + common/hooklib/meson.build | 2 + common/hooklib/printer_cp.c | 237 +++++++++++++++++++ common/hooklib/printer_cp.h | 35 +++ dist/diva/segatools.ini | 176 +++++++++++--- games/divahook/config.c | 5 +- games/divahook/config.h | 3 +- games/divahook/dllmain.c | 10 +- games/divahook/jvs.c | 5 + games/divahook/slider.c | 2 +- games/divaio/config.c | 147 +++++++++--- games/divaio/config.h | 46 +++- games/divaio/divaio.c | 426 ++++++++++++++++++---------------- games/divaio/divaio.h | 2 + games/divaio/input/backend.h | 30 +++ games/divaio/input/di-dev.c | 83 +++++++ games/divaio/input/di-dev.h | 18 ++ games/divaio/input/di.c | 335 ++++++++++++++++++++++++++ games/divaio/input/di.h | 7 + games/divaio/input/kb.c | 156 +++++++++++++ games/divaio/input/kb.h | 9 + games/divaio/input/wnd.c | 86 +++++++ games/divaio/input/wnd.h | 5 + games/divaio/input/xi.c | 128 ++++++++++ games/divaio/input/xi.h | 11 + games/divaio/meson.build | 24 ++ games/divaio/touch/backend.h | 8 + games/divaio/touch/mouse.c | 79 +++++++ games/divaio/touch/mouse.h | 7 + games/divaio/touch/wintouch.c | 164 +++++++++++++ games/divaio/touch/wintouch.h | 7 + 32 files changed, 2013 insertions(+), 274 deletions(-) create mode 100644 common/hooklib/printer_cp.c create mode 100644 common/hooklib/printer_cp.h create mode 100644 games/divaio/input/backend.h create mode 100644 games/divaio/input/di-dev.c create mode 100644 games/divaio/input/di-dev.h create mode 100644 games/divaio/input/di.c create mode 100644 games/divaio/input/di.h create mode 100644 games/divaio/input/kb.c create mode 100644 games/divaio/input/kb.h create mode 100644 games/divaio/input/wnd.c create mode 100644 games/divaio/input/wnd.h create mode 100644 games/divaio/input/xi.c create mode 100644 games/divaio/input/xi.h create mode 100644 games/divaio/touch/backend.h create mode 100644 games/divaio/touch/mouse.c create mode 100644 games/divaio/touch/mouse.h create mode 100644 games/divaio/touch/wintouch.c create mode 100644 games/divaio/touch/wintouch.h diff --git a/common/hooklib/config.c b/common/hooklib/config.c index 88aa9c1..0d9babb 100644 --- a/common/hooklib/config.c +++ b/common/hooklib/config.c @@ -152,6 +152,38 @@ void printer_cx_config_load(struct printer_cx_config *cfg, const wchar_t *filena } +void printer_cp_config_load(struct printer_cp_config *cfg, const wchar_t *filename){ + assert(cfg != NULL); + assert(filename != NULL); + + cfg->enable = GetPrivateProfileIntW(L"printer", L"enable", 1, filename); + cfg->print_time = GetPrivateProfileIntW(L"printer", L"printTime", 35, filename); + + GetPrivateProfileStringW( + L"printer", + L"printerOutPath", + L"DEVICE\\print", + cfg->printer_out_path, + _countof(cfg->printer_out_path), + filename); + + wchar_t tmpstr[7]; + + GetPrivateProfileStringW( + L"printer", + L"serialNo", + L"FAKEPR", + tmpstr, + _countof(tmpstr), + filename); + + size_t n = wcstombs(cfg->serial_no, tmpstr, sizeof(cfg->serial_no)); + for (int i = n; i < sizeof(cfg->serial_no); i++) { + cfg->serial_no[i] = '\0'; + } + +} + void y3_dll_config_load( struct y3_dll_config *cfg, const wchar_t *filename) diff --git a/common/hooklib/config.h b/common/hooklib/config.h index b776407..f42183b 100644 --- a/common/hooklib/config.h +++ b/common/hooklib/config.h @@ -5,6 +5,7 @@ #include "hooklib/dvd.h" #include "hooklib/touch.h" #include "hooklib/printer_chc.h" +#include "hooklib/printer_cp.h" #include "hooklib/printer_cx.h" struct y3_config { @@ -28,5 +29,6 @@ void dvd_config_load(struct dvd_config *cfg, const wchar_t *filename); void touch_screen_config_load(struct touch_screen_config *cfg, const wchar_t *filename); void printer_chc_config_load(struct printer_chc_config *cfg, const wchar_t *filename); void printer_cx_config_load(struct printer_cx_config *cfg, const wchar_t *filename); +void printer_cp_config_load(struct printer_cp_config *cfg, const wchar_t *filename); void y3_config_load(struct y3_config *cfg, const wchar_t *filename); void y3_dll_config_load(struct y3_dll_config *cfg, const wchar_t *filename); \ No newline at end of file diff --git a/common/hooklib/meson.build b/common/hooklib/meson.build index de87676..b9aa606 100644 --- a/common/hooklib/meson.build +++ b/common/hooklib/meson.build @@ -35,6 +35,8 @@ hooklib_lib = static_library( 'touch.h', 'printer_chc.c', 'printer_chc.h', + 'printer_cp.c', + 'printer_cp.h', 'printer_cx.c', 'printer_cx.h', 'y3.c', diff --git a/common/hooklib/printer_cp.c b/common/hooklib/printer_cp.c new file mode 100644 index 0000000..e490b69 --- /dev/null +++ b/common/hooklib/printer_cp.c @@ -0,0 +1,237 @@ +/* + * MITSUBISHI CP9550DS printer basic emulator, used only in Project Diva. + * I only analyzed the bare minimum to get this to work, as it's kinda a + * feature nobody really cares about. + */ + +// ReSharper disable CppParameterNeverUsed +// ReSharper disable CppParameterMayBeConstPtrOrRef +#include +#include +#include +#include +#include + +#include "printer_cp.h" + +#include "imageutil.h" +#include "hook/table.h" +#include "util/dprintf.h" + +static int WINAPI hook_CPU9CheckPrinter(char* result, void* a2); +static int WINAPI hook_CPUXSearchPrinters(char* data, int countRequested, void* a3, int* connectedPrinterCount); +static int WINAPI hook_CPUASendImage(struct printer_cp_image* image, void* a2, void* a3, void* a4); +static int WINAPI hook_CPUXInit2(); +static int WINAPI hook_CPU9CheckPrintEnd(int a1, bool* printEnd, void* a3); +static int WINAPI hook_CPU9CheckPaperRemain(char* result, void* a2); +static int WINAPI hook_CPU9GetFWInfo(char* firmware, void* a2); +static int WINAPI hook_CPU9PrintOut(void* a1, void* a2); +static int WINAPI hook_CPUXInit(); +static int WINAPI hook_CPUASetPrintParameter(void* a1, void* a2); + +static const struct hook_symbol printer_syms[] = { + { + .name = "CPU9CheckPrinter", + .patch = hook_CPU9CheckPrinter, + }, + { + .name = "CPUXSearchPrinters", + .patch = hook_CPUXSearchPrinters, + }, + { + .name = "CPUASendImage", + .patch = hook_CPUASendImage, + }, + { + .name = "CPUXInit2", + .patch = hook_CPUXInit2, + }, + { + .name = "CPU9CheckPrintEnd", + .patch = hook_CPU9CheckPrintEnd, + }, + { + .name = "CPU9CheckPaperRemain", + .patch = hook_CPU9CheckPaperRemain, + }, + { + .name = "CPU9GetFWInfo", + .patch = hook_CPU9GetFWInfo, + }, + { + .name = "CPU9PrintOut", + .patch = hook_CPU9PrintOut, + }, + { + .name = "CPUXInit", + .patch = hook_CPUXInit, + }, + { + .name = "CPUASetPrintParameter", + .patch = hook_CPUASetPrintParameter, + }, +}; + +static struct printer_cp_config printer_config; +static ULONGLONG print_start_time = 0; + +HRESULT printer_cp_hook_init(const struct printer_cp_config* cfg, HINSTANCE self) { + assert(cfg != NULL); + + if (!cfg->enable) { + return S_FALSE; + } + + memcpy(&printer_config, cfg, sizeof(*cfg)); + + printer_cp_hook_insert_hooks(NULL); + printer_cp_hook_insert_hooks(self); + + if (!CreateDirectoryW(cfg->printer_out_path, NULL)) { + if (GetLastError() != ERROR_ALREADY_EXISTS) { + dprintf("CP9550DS: Failed to create directory: %lx\n", GetLastError()); + return HRESULT_FROM_WIN32(GetLastError()); + } + } + + return S_OK; +} + +void printer_cp_hook_insert_hooks(HMODULE target) { + hook_table_apply(target, "CPUSBSG.dll", printer_syms, _countof(printer_syms)); +} + +static int WINAPI hook_CPU9CheckPrinter(char* result, void* a2) { + dprintf("CP9550DS: %s\n", __func__); + + // this can be null!? + if (result != NULL) { + memset(result, 0, 8); + result[0] = PRINTER_STATUS_READY; + } + + return PRINTER_ERROR_NONE; +} + +static int WINAPI hook_CPUXSearchPrinters(char* data, int countRequested, void* a3, int* connectedPrinterCount) { + dprintf("CP9550DS: %s(%d)\n", __func__, countRequested); + + *connectedPrinterCount = 1; + + if (countRequested == 0) { + // I guess this is requesting a buffer size? + // This doesn't seem to happen always. + dprintf("CP9550DS: countRequested = 0?\n"); + return PRINTER_ERROR_105; + } + + // unknown + data[0] = 0x01; + data[1] = 0x01; + data[2] = 0x01; + data[3] = 0x01; + // serial + strcpy(data + 4, printer_config.serial_no); + // unknown + data[10] = 0x01; + data[11] = 0x01; + + return PRINTER_ERROR_NONE; +} + +static int WINAPI hook_CPUASendImage(struct printer_cp_image* image, void* a2, void* a3, void* a4) { + dprintf("CP9550DS: %s(%d, %d, %d, %d, %d)\n", __func__, image->unk1, image->unk2, image->unk3, image->width, image->height); + + wchar_t dumpPath[MAX_PATH]; + + SYSTEMTIME t; + GetLocalTime(&t); + + swprintf_s( + dumpPath, MAX_PATH, + L"%s\\CP9550DS_%04d%02d%02d_%02d%02d%02d.bmp", + printer_config.printer_out_path, t.wYear, t.wMonth, t.wDay, t.wHour, t.wMinute, t.wSecond); + + int size = image->width * image->height * 3; + uint8_t* raw_image = (uint8_t*) malloc(size); + + if (raw_image == NULL) { + dprintf("CP9550DS: Memory allocation failed\n"); + return PRINTER_ERROR_GENERIC; + } + + for (int i = 0; i < image->width * image->height; i++) { + // flip red and blue channels + raw_image[i * 3] = *(image->data + i * 3 + 2); + raw_image[i * 3 + 1] = *(image->data + i * 3 + 1); + raw_image[i * 3 + 2] = *(image->data + i * 3); + } + + dprintf("CP9550DS: Saving image to %ls\n", dumpPath); + + int ret = WriteDataToBitmapFile(dumpPath, 24, image->width, image->height, raw_image, size, NULL, 0, false); + + free(raw_image); + + if (ret < 0) { + dprintf("CP9550DS: WriteDataToBitmapFile returned %d\n", ret); + return PRINTER_ERROR_GENERIC; + } + + print_start_time = GetTickCount64(); + + return PRINTER_ERROR_NONE; +} + +static int WINAPI hook_CPUXInit2() { + dprintf("CP9550DS: %s\n", __func__); + + return PRINTER_ERROR_NONE; +} + +static int WINAPI hook_CPU9CheckPrintEnd(int a1, bool* printEnd, void* a3) { + + *printEnd = GetTickCount64() - print_start_time > printer_config.print_time * 1000; + + return PRINTER_ERROR_NONE; +} + +static int WINAPI hook_CPU9CheckPaperRemain(char* result, void* a2) { + dprintf("CP9550DS: %s\n", __func__); + + // paper count + result[0] = 0xFF; + + return PRINTER_ERROR_NONE; +} + +static int WINAPI hook_CPU9GetFWInfo(char* firmware, void* a2) { + dprintf("CP9550DS: %s\n", __func__); + + // this doesn't seem to be checked anywhere. shows in test menu though. + // this is also a string?? + memset(firmware, 0, 18); + strcpy(firmware, "293A30"); + strcpy(firmware + 6, "378B10"); + strcpy(firmware + 12, "170A20"); + + return PRINTER_ERROR_NONE; +} + +static int WINAPI hook_CPU9PrintOut(void* a1, void* a2) { + dprintf("CP9550DS: %s\n", __func__); + + return PRINTER_ERROR_NONE; +} + +static int WINAPI hook_CPUXInit() { + dprintf("CP9550DS: %s\n", __func__); + + return PRINTER_ERROR_NONE; +} + +static int WINAPI hook_CPUASetPrintParameter(void* a1, void* a2) { + dprintf("CP9550DS: %s\n", __func__); + + return PRINTER_ERROR_NONE; +} diff --git a/common/hooklib/printer_cp.h b/common/hooklib/printer_cp.h new file mode 100644 index 0000000..3aeb5f0 --- /dev/null +++ b/common/hooklib/printer_cp.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +struct printer_cp_config { + bool enable; + wchar_t printer_out_path[MAX_PATH]; + char serial_no[7]; + uint32_t print_time; +}; + +struct printer_cp_image { + uint8_t* data; + uint32_t unk1; + uint32_t unk2; + uint32_t unk3; + uint32_t width; + uint32_t height; +}; + +enum printer_cp_status { + PRINTER_STATUS_READY = 0, + PRINTER_STATUS_PREPARING = 1, +}; + +enum printer_cp_error { + PRINTER_ERROR_NONE = 0, + PRINTER_ERROR_GENERIC = 100, + PRINTER_ERROR_105 = 105 +}; + +HRESULT printer_cp_hook_init(const struct printer_cp_config* cfg, HINSTANCE self); +void printer_cp_hook_insert_hooks(HMODULE target); diff --git a/dist/diva/segatools.ini b/dist/diva/segatools.ini index c92d0df..0220593 100644 --- a/dist/diva/segatools.ini +++ b/dist/diva/segatools.ini @@ -58,6 +58,11 @@ addrSuffix=11 ; exactly one machine and set this to 0 on all others. dipsw1=1 +; Chassis Test button virtual-key code. Default is the F4 key. +sw1=0x73 +; Chassis Service button virtual-key code. Default is the F5 key. +sw2=0x74 + [keychip] ; Keychip serial number. Keychip serials observed in the wild follow this ; pattern: `A\d{2}(E|X)-(01|20)[ABCDU]\d{8}`. @@ -81,15 +86,27 @@ serialNo=ACAE01A99999999 ; Enable/Disable Elo Touchsystems 2701 (SEGA 838-14772) emulation. enable=1 +; Touchscreen input type, use "mouse" for mouse or "wintouch" for touchscreen. +mode=mouse + [gfx] -; Enables the graphics hook. +; Enables the graphics hook. If you use a third-party render hook, this +; should be turned off. enable=1 ; Force the game to run windowed. -windowed=1 +windowed=0 ; Add a frame to the game window if running windowed. framed=0 -; Enable DPI awareness for the game process, preventing Windows from stretching the game window if a DPI scaling higher than 100% is used -dpiAware=1 + +[printer] +; MITSUBISHI CP9550DS printer emulation setting. +enable=1 +; Insert the path to the image output directory here. +printerOutPath="DEVICE\print" +; Change the printer serial number here. Maximum 6 characters. +serialNo=FAKEPR +; Printing time in seconds. Set to 0 to instantly complete. +printTime=35 ; ----------------------------------------------------------------------------- ; Custom IO settings @@ -129,30 +146,137 @@ service=0x71 ; Keyboard button to increment coin counter. Default is the F3 key. coin=0x72 -; Touchscreen input type, currently only "mouse" is supported. -mode=mouse +; Minimum time in milliseconds a button needs to be pressed down for a hold-transfer. +; +; On the console versions, you can quickly press a button by alternating the primary +; and the extra button of the same symbol. This still works while the first button is +; being registered as down. The arcade version does not know this concept, so we need +; to trick a little to get this to work. +; +; For example: If you are pressing Cross, and you are pressing D-Pad Down while Cross +; is still being registered as pressed, DivaIO will release Cross for a single frame +; to make sure the second press registers as a press. +; +; Unfortunately this will break "hold transfers", i.e. pressing and holding Cross for +; a HOLD note and then pressing D-Pad Down while also holding Cross and then releasing +; Cross so you do not break the hold note. Therefore you can specify here a timeout +; range when switching from a button of the same symbol while both are being registered +; as pressed to keep it being held down instead of releasing it for one frame. +; +; The default timeline looks as follow: +; <---DOUBLETAP---><-----------HOLD-TRANSFER-----------> +; | | | | | +; 0s 0.5s 1s 2s 3s +; 1st Tap +; +; These settings work regardless of input mode. +; Please note that the resolution of the timer used is in the 10-16ms range. +; If you do not want to use hold-transfers (and always re-tap), set both values to 0. +holdTransferTimeMin=500 +holdTransferTimeMax=2000 + +;doubleTapReleaseDelayFrames=1 + +; Input mode. Use "keyboard" for keyboard, "dinput" for DirectInput or +; "xinput" for XInput. +mode=keyboard [slider] -cell1=0x51 -cell2=0x57 -cell3=0x45 -cell4=0x52 -cell5=0x55 -cell6=0x49 -cell7=0x4F -cell8=0x50 +; Enable slider emulation. If you have real AC slider, set this to 0. +; Slider serial port must be COM11. +;enable=1 -[buttons] -key1=0x27 -key2=0x28 -key3=0x25 -key4=0x26 -key5=0x20 +; Key bindings for automatic sliding. When the specified key is held down, +; the slider is constantly swiped into that direction. If both are pressed, +; two swipes into opposite directions are performed. This is PS4 behaviour. +; By default, this is left shift and right shift. +autoSlideLeft=0xA0 +autoSlideRight=0xA1 -; Sliders : <- QWER UIOP -> -; Triangle : Up arrow -; Square : Left Arrow -; Cross : Down Arrow -; Circle : Right arrow -; Enter : Space +; Key bindings for each of the 32 touch cells, if auto-sliding is not used. +; The default key map, depicted in left-to-right order, is as follows: +; +; QQQQWWWWEEEERRRRUUUUIIIIOOOOPPPP +; +;cell1=0x51 +;cell2=0x51 +; ... +;cell31=0x50 +;cell32=0x50 +[keyboard] +; Defaults: +; +; Triangle: Up Arrow +; Square: Left Arrow +; Cross: Down Arrow +; Circle: Right Arrow +; Start: Space + +triangle=0x27 +square=0x28 +cross=0x25 +circle=0x26 +start=0x20 + +[dinput] +; Name of the DirectInput device to use (or any text that occurs in its name) +; Example: Wireless Controller +; +; If this is left blank then the first DirectInput device will be used. +deviceName= + +; DirectInput button numbers to map to menu inputs. Note that buttons are +; numbered from 1; some software numbers buttons from 0. +; Use 0 to disable a button. The defaults are for a PS4 controller. +test=9 +service=14 +triangle=4 +square=1 +cross=2 +circle=3 +start=10 + +; Secondary bindings for the game buttons. These can be used for alternating +; or hold-transfers like in the PS4 version. If your controller reports +; directional buttons as a hat switch, you don't need to bind these. +extraTriangle=0 +extraSquare=0 +extraCross=0 +extraCircle=0 + +; Bindings for automatic sliding. When the specified button is held down, +; the slider is constantly swiped into that direction. If both are pressed, +; two swipes into opposite directions are performed. This is PS4 behaviour. +autoSlideLeftButton=5 +autoSlideRightButton=6 + +; Axes to use for auto-sliding. Valid axis names are: +; +; X, Y, Z, RX, RY, RZ, U, V +; +; (U and V are old names for Slider 1 and Slider 2 - DirectInput sliders, +; not the Project Diva slider device) +; +; By default, the positive direction will slide to the right, and the +; negative direction will slide to the left. This can be inverted if desired. +; PS4 behaviour is axis1 being the left stick and axis2 being the right stick. +autoSlideAxis1=X +autoSlideAxis2=Z +autoSlideAxis1Invert=0 +autoSlideAxis2Invert=0 + +[xinput] +; On XInput controllers, buttons are fixed depending on their position. +; A / Pad Down = Cross +; B / Pad Right = Circle +; X / Pad Left = Square +; Y / Pad Up = Triangle +; Start = Start +; Left Stick Horizontal = Auto-Slide 1 +; Right Stick Horizontal = Auto-Slide 2 +; LB / LT = Auto-Slide 1 +; RB / RT = Auto-Slide 2 + +; Enables SELECT to be used as TEST. +useSelect=0 diff --git a/games/divahook/config.c b/games/divahook/config.c index 243fa05..b2d64e4 100644 --- a/games/divahook/config.c +++ b/games/divahook/config.c @@ -1,20 +1,16 @@ #include -#include #include #include "amex/amex.h" #include "amex/config.h" #include "board/config.h" -#include "board/sg-reader.h" #include "hooklib/config.h" -#include "hooklib/dvd.h" #include "divahook/config.h" #include "platform/config.h" -#include "platform/platform.h" #include "util/io-path.h" void diva_dll_config_load( @@ -66,4 +62,5 @@ void diva_hook_config_load( diva_dll_config_load(&cfg->dll, filename); slider_config_load(&cfg->slider, filename); elo_config_load(&cfg->touch, filename); + printer_cp_config_load(&cfg->printer, filename); } diff --git a/games/divahook/config.h b/games/divahook/config.h index 3828252..f4b9392 100644 --- a/games/divahook/config.h +++ b/games/divahook/config.h @@ -7,7 +7,7 @@ #include "board/sg-reader.h" #include "hooklib/dvd.h" -#include "hooklib/touch.h" +#include "hooklib/printer_cp.h" #include "gfxhook/config.h" @@ -26,6 +26,7 @@ struct diva_hook_config { struct diva_dll_config dll; struct slider_config slider; struct elo_config touch; + struct printer_cp_config printer; }; void diva_dll_config_load( diff --git a/games/divahook/dllmain.c b/games/divahook/dllmain.c index 33d412e..bf2fd12 100644 --- a/games/divahook/dllmain.c +++ b/games/divahook/dllmain.c @@ -7,6 +7,7 @@ COM1: 838-14772 (Elo Touchsystems 2701) Touch Controller Board COM10: TN32MSEC003S "Gen 1" Aime Reader COM11: 837-15275 Touch Slider + USB: MITSUBISHI CP9550DS Printer */ #include @@ -22,11 +23,11 @@ #include "divahook/jvs.h" #include "divahook/slider.h" -#include "gfxhook/gfx.h" #include "gfxhook/gl.h" #include "hook/process.h" +#include "hooklib/printer_cp.h" #include "hooklib/serial.h" #include "hooklib/spike.h" @@ -65,7 +66,6 @@ static DWORD CALLBACK diva_pre_startup(void) /* Hook Win32 APIs */ dvd_hook_init(&diva_hook_cfg.dvd, diva_hook_mod); - gfx_hook_init(&diva_hook_cfg.gfx); gfx_gl_hook_init(&diva_hook_cfg.gfx, diva_hook_mod); serial_hook_init(); @@ -111,8 +111,10 @@ static DWORD CALLBACK diva_pre_startup(void) goto fail; } - if (diva_hook_cfg.amex.jvs.enable && diva_hook_cfg.amex.jvs.foreground) { - fgdet_init(L"Hatsune Miku Project DIVA Arcade Future Tone", false); + hr = printer_cp_hook_init(&diva_hook_cfg.printer, diva_hook_mod); + + if (FAILED(hr)) { + goto fail; } /* Initialize debug helpers */ diff --git a/games/divahook/jvs.c b/games/divahook/jvs.c index e53f1ac..5d9fe82 100644 --- a/games/divahook/jvs.c +++ b/games/divahook/jvs.c @@ -14,6 +14,7 @@ #include "jvs/jvs-bus.h" #include "util/dprintf.h" +#include "util/fg-detect.h" static void diva_jvs_read_switches(void *ctx, struct io3_switch_state *out); static void diva_jvs_read_coin_counter( @@ -63,6 +64,10 @@ static void diva_jvs_read_switches(void *ctx, struct io3_switch_state *out) opbtn = 0; gamebtn = 0; + if (!fgdet_in_foreground()) { + return; + } + diva_dll.jvs_poll(&opbtn, &gamebtn); if (gamebtn & DIVA_IO_GAMEBTN_CIRCLE) { diff --git a/games/divahook/slider.c b/games/divahook/slider.c index 3046142..61e3dc1 100644 --- a/games/divahook/slider.c +++ b/games/divahook/slider.c @@ -164,7 +164,7 @@ static HRESULT slider_req_nop(uint8_t cmd) { struct slider_hdr resp; - dprintf("Diva slider: No-op cmd 0x%#02x\n", cmd); + dprintf("Diva slider: No-op cmd %#02x\n", cmd); resp.sync = SLIDER_FRAME_SYNC; resp.cmd = cmd; diff --git a/games/divaio/config.c b/games/divaio/config.c index ef5f0fc..299026a 100644 --- a/games/divaio/config.c +++ b/games/divaio/config.c @@ -1,60 +1,135 @@ #include #include -#include #include #include #include "divaio/config.h" -static const int diva_io_default_buttons[] = { - VK_RIGHT, VK_DOWN, VK_LEFT, VK_UP, VK_SPACE -}; - static const int diva_io_default_slider[] = { 'Q', 'W', 'E', 'R', 'U', 'I', 'O', 'P' }; -void diva_io_config_load( - struct diva_io_config *cfg, - const wchar_t *filename) -{ - wchar_t key[5]; - wchar_t cell[8]; - int i; - int c; +void diva_kb_config_load( + struct diva_kb_config* cfg, + const wchar_t* filename) { + wchar_t cell[32]; assert(cfg != NULL); assert(filename != NULL); + cfg->vk_start = GetPrivateProfileIntW(L"keyboard", L"start", VK_SPACE, filename); + cfg->vk_cross = GetPrivateProfileIntW(L"keyboard", L"cross", VK_DOWN, filename); + cfg->vk_circle = GetPrivateProfileIntW(L"keyboard", L"circle", VK_RIGHT, filename); + cfg->vk_triangle = GetPrivateProfileIntW(L"keyboard", L"triangle", VK_UP, filename); + cfg->vk_square = GetPrivateProfileIntW(L"keyboard", L"square", VK_LEFT, filename); + + cfg->vk_auto_left = GetPrivateProfileIntW(L"slider", L"autoSlideLeft", VK_LSHIFT, filename); + cfg->vk_auto_right = GetPrivateProfileIntW(L"slider", L"autoSlideRight", VK_RSHIFT, filename); + + for (int c = 0; c < _countof(cfg->vk_slider); c++) { + swprintf_s(cell, _countof(cell), L"cell%i", c + 1); + cfg->vk_slider[c] = GetPrivateProfileIntW( + L"slider", + cell, + diva_io_default_slider[c / 4], + filename); + } +} + +void diva_di_config_load(struct diva_di_config* cfg, const wchar_t* filename) { + wchar_t key[8]; + int i; + + assert(cfg != NULL); + assert(filename != NULL); + + GetPrivateProfileStringW( + L"dinput", + L"deviceName", + L"", + cfg->device_name, + _countof(cfg->device_name), + filename); + + cfg->test = GetPrivateProfileIntW(L"dinput", L"test", 0, filename); + cfg->service = GetPrivateProfileIntW(L"dinput", L"service", 0, filename); + + cfg->square[0] = GetPrivateProfileIntW(L"dinput", L"square", 0, filename); + cfg->triangle[0] = GetPrivateProfileIntW(L"dinput", L"triangle", 0, filename); + cfg->cross[0] = GetPrivateProfileIntW(L"dinput", L"cross", 0, filename); + cfg->circle[0] = GetPrivateProfileIntW(L"dinput", L"circle", 0, filename); + cfg->start = GetPrivateProfileIntW(L"dinput", L"start", 0, filename); + + cfg->square[1] = GetPrivateProfileIntW(L"dinput", L"extraSquare", 0, filename); + cfg->triangle[1] = GetPrivateProfileIntW(L"dinput", L"extraTriangle", 0, filename); + cfg->cross[1] = GetPrivateProfileIntW(L"dinput", L"extraCross", 0, filename); + cfg->circle[1] = GetPrivateProfileIntW(L"dinput", L"extraCircle", 0, filename); + + cfg->auto_left_button = GetPrivateProfileIntW(L"dinput", L"autoSlideLeftButton", 0, filename); + cfg->auto_right_button = GetPrivateProfileIntW(L"dinput", L"autoSlideRightButton", 0, filename); + + GetPrivateProfileStringW( + L"dinput", + L"autoSlideAxis1", + L"", + cfg->auto_left_axis, + _countof(cfg->auto_left_axis), + filename); + GetPrivateProfileStringW( + L"dinput", + L"autoSlideAxis2", + L"", + cfg->auto_right_axis, + _countof(cfg->auto_right_axis), + filename); + + cfg->auto_left_invert = GetPrivateProfileIntW(L"dinput", L"autoSlideAxis1Invert", 0, filename); + cfg->auto_right_invert = GetPrivateProfileIntW(L"dinput", L"autoSlideAxis2Invert", 0, filename); + +} + +void diva_xi_config_load(struct diva_xi_config* cfg, const wchar_t* filename) { + assert(cfg != NULL); + assert(filename != NULL); + + cfg->use_select_as_test = GetPrivateProfileIntW( + L"xinput", + L"useSelect", + 1, + filename); +} + +void diva_io_config_load(struct diva_io_config* cfg, const wchar_t* filename) { + assert(cfg != NULL); + assert(filename != NULL); + cfg->vk_test = GetPrivateProfileIntW(L"io3", L"test", VK_F1, filename); cfg->vk_service = GetPrivateProfileIntW(L"io3", L"service", VK_F2, filename); cfg->vk_coin = GetPrivateProfileIntW(L"io3", L"coin", VK_F3, filename); + cfg->hold_transfer_time_min = GetPrivateProfileIntW(L"io3", L"holdTransferTimeMin", 500, filename); + cfg->hold_transfer_time_max = GetPrivateProfileIntW(L"io3", L"holdTransferTimeMax", 2000, filename); + cfg->dropped_input_frames = GetPrivateProfileIntW(L"io3", L"doubleTapReleaseDelayFrames", 1, filename); + + GetPrivateProfileStringW( + L"io3", + L"mode", + L"keyboard", + cfg->input_mode, + _countof(cfg->input_mode), + filename); + /* Load touch input type. Mouse input by default. */ GetPrivateProfileStringW( - L"touch", - L"mode", - L"mouse", - cfg->touch_mode, - _countof(cfg->touch_mode), - filename); + L"touch", + L"mode", + L"mouse", + cfg->touch_mode, + _countof(cfg->touch_mode), + filename); - for (i = 0 ; i < _countof(cfg->vk_buttons) ; i++) { - swprintf_s(key, _countof(key), L"key%i", i + 1); - cfg->vk_buttons[i] = GetPrivateProfileIntW( - L"buttons", - key, - diva_io_default_buttons[i], - filename); - } - - for (c = 0 ; c < _countof(cfg->vk_slider) ; c++) { - swprintf_s(cell, _countof(cell), L"cell%i", c + 1); - cfg->vk_slider[c] = GetPrivateProfileIntW( - L"slider", - cell, - diva_io_default_slider[c], - filename); - } + diva_kb_config_load(&cfg->kb, filename); + diva_di_config_load(&cfg->di, filename); + diva_xi_config_load(&cfg->xi, filename); } diff --git a/games/divaio/config.h b/games/divaio/config.h index 8e01d81..98aa9d7 100644 --- a/games/divaio/config.h +++ b/games/divaio/config.h @@ -1,15 +1,57 @@ #pragma once +#include #include #include +struct diva_di_config { + wchar_t device_name[64]; + uint8_t test; + uint8_t service; + uint8_t start; + uint8_t cross[2]; + uint8_t circle[2]; + uint8_t square[2]; + uint8_t triangle[2]; + uint8_t auto_left_button; + uint8_t auto_right_button; + wchar_t auto_left_axis[16]; + wchar_t auto_right_axis[16]; + bool auto_left_invert; + bool auto_right_invert; +}; + +struct diva_xi_config { + bool use_select_as_test; +}; + +struct diva_kb_config { + uint8_t vk_cross; + uint8_t vk_circle; + uint8_t vk_triangle; + uint8_t vk_square; + uint8_t vk_start; + uint8_t vk_slider[32]; + uint8_t vk_auto_left; + uint8_t vk_auto_right; +}; + struct diva_io_config { - uint8_t vk_buttons[5]; - uint8_t vk_slider[8]; + uint8_t vk_test; uint8_t vk_service; uint8_t vk_coin; + + wchar_t input_mode[16]; wchar_t touch_mode[16]; + + uint16_t hold_transfer_time_min; + uint16_t hold_transfer_time_max; + uint8_t dropped_input_frames; + + struct diva_kb_config kb; + struct diva_di_config di; + struct diva_xi_config xi; }; void diva_io_config_load( diff --git a/games/divaio/divaio.c b/games/divaio/divaio.c index f74b599..01451ee 100644 --- a/games/divaio/divaio.c +++ b/games/divaio/divaio.c @@ -1,75 +1,166 @@ #include +#include #include -#include #include #include -#include "divaio/divaio.h" #include "divaio/config.h" +#include "divaio/divaio.h" +#include "divaio/input/backend.h" +#include "divaio/touch/backend.h" +#include "divaio/touch/mouse.h" +#include "divaio/touch/wintouch.h" +#include "input/di.h" +#include "input/kb.h" +#include "input/xi.h" #include "util/env.h" #include "util/dprintf.h" +#include "util/fg-detect.h" #include "util/str.h" -static unsigned int __stdcall diva_io_slider_thread_proc(void *ctx); -static unsigned int __stdcall diva_io_touch_thread_proc(void *ctx); +#define DIVA_SLIDER_AUTO_FRAME_DELAY 5 -static HRESULT diva_io_touch_config_apply( - const struct diva_io_config *cfg); +static unsigned int __stdcall diva_io_slider_thread_proc(void* ctx); +static unsigned int __stdcall diva_io_touch_thread_proc(void* ctx); -static const wchar_t app_title[] = L"Hatsune Miku Project DIVA Arcade Future Tone"; +static LARGE_INTEGER performanceFrequency; +static struct diva_io_config diva_io_cfg; +static const struct diva_io_backend* diva_io_backend; static bool diva_io_coin; static uint16_t diva_io_coins; +static struct diva_button_states diva_button_states = {0}; + static HANDLE diva_io_slider_thread; static bool diva_io_slider_stop_flag; -static struct diva_io_config diva_io_cfg; -static bool diva_io_config_initted = false; -static bool diva_io_window_focus = true; +static int8_t slider_auto_left_pos = 0; +static int8_t slider_auto_right_pos = DIVA_SLIDER_CELL_COUNT; +static const struct diva_touch_backend* diva_touch_backend; static HANDLE diva_io_touch_thread; -static int diva_io_m1 = VK_LBUTTON; -static int diva_io_touch_points = 1; static bool diva_io_touch_stop_flag; -uint16_t diva_io_get_api_version(void) -{ +uint16_t diva_io_get_api_version(void) { return 0x0101; } -HRESULT diva_io_jvs_init(void) -{ - HRESULT hr; +HRESULT diva_io_jvs_init(void) { + assert(diva_io_backend == NULL); - if(!diva_io_config_initted) { - diva_io_config_load(&diva_io_cfg, get_config_path()); - diva_io_config_initted = true; + + QueryPerformanceFrequency(&performanceFrequency); + + HINSTANCE inst = GetModuleHandleW(NULL); + + if (inst == NULL) { + HRESULT hr = HRESULT_FROM_WIN32(GetLastError()); + dprintf("GetModuleHandleW failed: %lx\n", hr); + + return hr; } - hr = diva_io_touch_config_apply(&diva_io_cfg); + diva_io_config_load(&diva_io_cfg, get_config_path()); + + dprintf("Diva IO: Touch: --- Begin Configuration ---\n"); + + if (wstr_ieq(diva_io_cfg.touch_mode, L"mouse")) { + dprintf("Diva IO: Touch: Mouse emulation\n"); + + diva_touch_mouse_init(&diva_touch_backend); + } else if (wstr_ieq(diva_io_cfg.touch_mode, L"wintouch")) { + dprintf("Diva IO: Touch: WinTouch conversion\n"); + + diva_touch_wintouch_init(&diva_touch_backend); + } else { + dprintf("Diva IO: Touch: Invalid touch mode \"%S\". Use 'mouse' or 'keyboard'.\n", diva_io_cfg.touch_mode); + + return E_INVALIDARG; + } + + dprintf("Diva IO: Touch: --- End Configuration ---\n"); + + if (wstr_ieq(diva_io_cfg.input_mode, L"keyboard")) { + dprintf("Diva IO: Using keyboard\n"); + + return diva_kb_init(&diva_io_cfg.kb, &diva_io_backend); + } else if (wstr_ieq(diva_io_cfg.input_mode, L"dinput")) { + dprintf("Diva IO: Using DirectInput\n"); + + return diva_di_init(&diva_io_cfg.di, inst, &diva_io_backend); + } else if (wstr_ieq(diva_io_cfg.input_mode, L"xinput")) { + dprintf("Diva IO: Using XInput\n"); + + return diva_xi_init(&diva_io_cfg.xi, &diva_io_backend); + } else { + dprintf("Diva IO: Invalid input mode \"%S\". Use 'keyboard', 'dinput' or 'xinput'.\n", diva_io_cfg.touch_mode); + + return E_INVALIDARG; + } - return hr; } -void diva_io_jvs_poll(uint8_t *opbtn_out, uint8_t *gamebtn_out) -{ - uint8_t opbtn; - uint8_t gamebtn; - size_t i; +inline static void diva_io_reset_hold_timeouts(struct diva_button_state* state) { + ULONGLONG ticks = GetTickCount64(); + state->doubleTapTimeoutBegin = ticks + diva_io_cfg.hold_transfer_time_min; + state->doubleTapTimeoutEnd = ticks + diva_io_cfg.hold_transfer_time_max; +} - opbtn = 0; +static void diva_io_set_game_button(uint8_t* gamebtn, int input_bit, struct diva_button_state* state) { - HWND hwnd = GetForegroundWindow(); - wchar_t window_title[MAX_PATH]; - GetWindowTextW(hwnd, window_title, MAX_PATH); + if (state->dropFrames > 0) { + state->dropFrames--; + return; + } - if (!wstr_eq(window_title, app_title)) { - if (diva_io_window_focus) { - diva_io_window_focus = false; + // anything pressed + if (state->primary || state->secondary) { + + // just pressed anything + if ((state->primary || state->secondary) && !(state->prevPrimary || state->prevSecondary)) { + diva_io_reset_hold_timeouts(state); } - } else if (!diva_io_window_focus) { - diva_io_window_focus = true; + + // just pressed both in the same frame + // OR if we switched buttons exactly in the same frame, this is also a double-tap! + if ( + (state->primary && state->secondary && !(state->prevPrimary && state->prevSecondary)) || + (!state->primary && state->prevPrimary && state->secondary && !state->prevSecondary) || + (state->primary && !state->prevPrimary && !state->secondary && state->prevSecondary) + ) { + + // if we are outside the takeover range, add dropped frame(s) to simulate re-press + ULONGLONG ticks = GetTickCount64(); + if (ticks < state->doubleTapTimeoutBegin || ticks > state->doubleTapTimeoutEnd) { + state->dropFrames = diva_io_cfg.dropped_input_frames; + + // also reset double-tap timer for quick alternations + diva_io_reset_hold_timeouts(state); + } + + // otherwise it's a hold-takeover, do nothing special + } + + if (state->dropFrames == 0) { + *gamebtn |= input_bit; + } + } + + state->prevPrimary = state->primary; + state->prevSecondary = state->secondary; +} + +void diva_io_jvs_poll(uint8_t* opbtn_out, uint8_t* gamebtn_out) { + assert(opbtn_out != NULL); + assert(gamebtn_out != NULL); + assert(diva_io_backend != NULL); + + uint8_t opbtn = 0; + uint8_t gamebtn = 0; + + if (diva_io_backend->get_opbtns != NULL) { + diva_io_backend->get_opbtns(&opbtn); } if (GetAsyncKeyState(diva_io_cfg.vk_test) & 0x8000) { @@ -80,18 +171,24 @@ void diva_io_jvs_poll(uint8_t *opbtn_out, uint8_t *gamebtn_out) opbtn |= DIVA_IO_OPBTN_SERVICE; } - for (i = 0 ; i < _countof(diva_io_cfg.vk_buttons) ; i++) { - if (GetAsyncKeyState(diva_io_cfg.vk_buttons[i]) & 0x8000) { - gamebtn |= 1 << i; - } + if (diva_io_backend->get_gamebtns != NULL) { + diva_io_backend->get_gamebtns(&diva_button_states); } + if (diva_button_states.start) { + gamebtn |= DIVA_IO_GAMEBTN_START; + } + + diva_io_set_game_button(&gamebtn, DIVA_IO_GAMEBTN_CIRCLE, &diva_button_states.circle); + diva_io_set_game_button(&gamebtn, DIVA_IO_GAMEBTN_CROSS, &diva_button_states.cross); + diva_io_set_game_button(&gamebtn, DIVA_IO_GAMEBTN_TRIANGLE, &diva_button_states.triangle); + diva_io_set_game_button(&gamebtn, DIVA_IO_GAMEBTN_SQUARE, &diva_button_states.square); + *opbtn_out = opbtn; *gamebtn_out = gamebtn; } -void diva_io_jvs_read_coin_counter(uint16_t *out) -{ +void diva_io_jvs_read_coin_counter(uint16_t* out) { if (out == NULL) { return; } @@ -108,28 +205,29 @@ void diva_io_jvs_read_coin_counter(uint16_t *out) *out = diva_io_coins; } -HRESULT diva_io_slider_init(void) -{ +HRESULT diva_io_slider_init(void) { return S_OK; } -void diva_io_slider_start(diva_io_slider_callback_t callback) -{ +void diva_io_slider_start(diva_io_slider_callback_t callback) { if (diva_io_slider_thread != NULL) { return; } - diva_io_slider_thread = (HANDLE) _beginthreadex( - NULL, - 0, - diva_io_slider_thread_proc, - callback, - 0, - NULL); + if (diva_io_backend == NULL) { + return; + } + + diva_io_slider_thread = (HANDLE)_beginthreadex( + NULL, + 0, + diva_io_slider_thread_proc, + callback, + 0, + NULL); } -void diva_io_slider_stop(void) -{ +void diva_io_slider_stop(void) { diva_io_slider_stop_flag = true; WaitForSingleObject(diva_io_slider_thread, INFINITE); @@ -138,27 +236,67 @@ void diva_io_slider_stop(void) diva_io_slider_stop_flag = false; } -void diva_io_slider_set_leds(const uint8_t *rgb) -{} +void diva_io_slider_set_leds(const uint8_t* rgb) { +} -static unsigned int __stdcall diva_io_slider_thread_proc(void *ctx) -{ - diva_io_slider_callback_t callback; - uint8_t pressure_val; - uint8_t pressure[32]; - size_t i; +static unsigned int __stdcall diva_io_slider_thread_proc(void* ctx) { + uint8_t pressure[DIVA_SLIDER_CELL_COUNT]; + uint32_t slider_status = 0; + LARGE_INTEGER last_auto_time, current_auto_time; - callback = ctx; + const diva_io_slider_callback_t callback = ctx; + + QueryPerformanceCounter(&last_auto_time); while (!diva_io_slider_stop_flag) { - for (i = 0 ; i < 8 ; i++) { - if (GetAsyncKeyState(diva_io_cfg.vk_slider[i]) & 0x8000) { - pressure_val = 20; - } else { - pressure_val = 0; + + // Move the auto-slider every 30 ms to simulate constant movement. It's only actually + // sent to the game if the IO backend returns true for either left or right hand. + QueryPerformanceCounter(¤t_auto_time); + if ((double)(current_auto_time.QuadPart - last_auto_time.QuadPart) / ((double)performanceFrequency.QuadPart / 1000) > 30) { + // Wrap around the 32 cells whenever we hit the edges. + // Ensure we have a few frames of no-touch when wrapping around, otherwise + // the game misdetects this as a very far swipe into the other direction, + // causing the menu to scroll back and forth. + // Unsure if there's a better solution. + if (++slider_auto_right_pos >= DIVA_SLIDER_CELL_COUNT + DIVA_SLIDER_AUTO_FRAME_DELAY) { + slider_auto_right_pos = DIVA_SLIDER_CELL_COUNT / 2; + } + if (--slider_auto_left_pos < -DIVA_SLIDER_AUTO_FRAME_DELAY) { + slider_auto_left_pos = DIVA_SLIDER_CELL_COUNT / 2 - 1; } - memset(&pressure[4 * i], pressure_val, 4); + last_auto_time = current_auto_time; + } + + slider_status = 0; + + if (diva_io_backend->get_slider != NULL) { + diva_io_backend->get_slider(&slider_status); + } + + if (diva_io_backend->get_auto_status != NULL) { + bool l; + bool r; + + diva_io_backend->get_auto_status(&l, &r); + + if (l && slider_auto_left_pos > -1 && slider_auto_left_pos < DIVA_SLIDER_CELL_COUNT) { + slider_status |= 1 << slider_auto_left_pos; + } + if (r && slider_auto_right_pos > -1 && slider_auto_right_pos < DIVA_SLIDER_CELL_COUNT) { + slider_status |= 1 << slider_auto_right_pos; + } + + // Reset auto-sliders to center if they aren't moved + if (!l && !r) { + slider_auto_left_pos = DIVA_SLIDER_CELL_COUNT / 2 - 1; + slider_auto_right_pos = DIVA_SLIDER_CELL_COUNT / 2; + } + } + + for (int i = 0; i < DIVA_SLIDER_CELL_COUNT; i++) { + pressure[i] = ((slider_status >> i) & 1) != 0 ? 20 : 0; } callback(pressure); @@ -168,13 +306,11 @@ static unsigned int __stdcall diva_io_slider_thread_proc(void *ctx) return 0; } -HRESULT diva_io_led_init(void) -{ +HRESULT diva_io_led_init(void) { return S_OK; } -void diva_io_led_set_leds(uint8_t board, const uint8_t *rgb) -{ +void diva_io_led_set_leds(uint8_t board, const uint8_t* rgb) { #if 0 dprintf("DIVA LED: LEFT PARTITION RED: %02X\n", rgb[0]); dprintf("DIVA LED: LEFT PARTITION GREEN: %02X\n", rgb[1]); @@ -187,56 +323,31 @@ void diva_io_led_set_leds(uint8_t board, const uint8_t *rgb) dprintf("DIVA LED: BTN SQUARE: %02X\n", rgb[8]); dprintf("DIVA LED: BTN CIRCLE: %02X\n", rgb[9]); #endif - - return; } -HRESULT diva_io_touch_init() -{ - return S_OK; -} - -static HRESULT diva_io_touch_config_apply( - const struct diva_io_config *cfg - ) -{ - dprintf("Diva IO: Touch: --- Begin Configuration ---\n"); - - if (wstr_ieq(cfg->touch_mode, L"mouse")) { - dprintf("Diva IO: Touch: Mouse emulation\n"); - - /* Work around GetAsyncKeyState returning physical mouse button states - instead of logical */ - diva_io_m1 = GetSystemMetrics(SM_SWAPBUTTON) ? VK_RBUTTON : VK_LBUTTON; - - } else { - dprintf("Diva IO: Touch: Invalid touch mode \"%S\". Use 'mouse'." - "\n", cfg->touch_mode); - - return E_INVALIDARG; +HRESULT diva_io_touch_init() { + if (diva_touch_backend->init != NULL) { + return diva_touch_backend->init(); } - dprintf("Diva IO: Touch: --- End Configuration ---\n"); return S_OK; } -void diva_io_touch_start(diva_io_touch_callback_t callback) -{ +void diva_io_touch_start(diva_io_touch_callback_t callback) { if (diva_io_touch_thread != NULL) { return; } - diva_io_touch_thread = (HANDLE) _beginthreadex( - NULL, - 0, - diva_io_touch_thread_proc, - callback, - 0, - NULL); + diva_io_touch_thread = (HANDLE)_beginthreadex( + NULL, + 0, + diva_io_touch_thread_proc, + callback, + 0, + NULL); } -void diva_io_touch_stop(void) -{ +void diva_io_touch_stop(void) { diva_io_touch_stop_flag = true; WaitForSingleObject(diva_io_touch_thread, INFINITE); @@ -245,100 +356,15 @@ void diva_io_touch_stop(void) diva_io_touch_stop_flag = false; } -static unsigned int __stdcall diva_io_touch_thread_proc(void *ctx) -{ - diva_io_touch_callback_t callback; - HWND hwnd; - POINT point; - BOOL ok; - - uint8_t status = 0; - uint16_t x = 0; - uint16_t y = 0; - uint16_t last_x = 0; - uint16_t last_y = 0; - uint8_t id = 0; - bool touch = false; - - callback = ctx; +static unsigned int __stdcall diva_io_touch_thread_proc(void* ctx) { + const diva_io_touch_callback_t callback = ctx; while (!diva_io_touch_stop_flag) { - if (!diva_io_window_focus) { - status = 0; - x = 0; - y = 0; - id = 0; - touch = false; - goto end; + if (diva_touch_backend->update != NULL) { + diva_touch_backend->update(callback); } - if (wstr_ieq(diva_io_cfg.touch_mode, L"mouse")) - { - if (GetAsyncKeyState(diva_io_m1) & 0x8000) - { - /* Get cursor location and map to window size */ - ok = GetCursorPos(&point); - - if (!ok) { - status = 0; - x = 0; - y = 0; - id = 0; - touch = false; - goto end; - } - - hwnd = GetForegroundWindow(); - - ok = ScreenToClient(hwnd, &point); - - if (!ok) { - status = 0; - x = 0; - y = 0; - id = 0; - touch = false; - goto end; - } - - /* Set status */ - if (!touch) { - status = DIVA_IO_TOUCH_DOWN; - touch = true; - } else { - status = DIVA_IO_TOUCH_STREAM; - } - - /* Set coordinates */ - if (point.x < 0) point.x = 0; - if (point.y < 0) point.y = 0; - x = (uint16_t)point.x; - y = (uint16_t)point.y; - last_x = x; - last_y = y; - } - else - { - if (touch) { - status = DIVA_IO_TOUCH_LIFTOFF; - x = last_x; - y = last_y; - touch = false; - } else { - /* No touch event */ - status = 0; - x = 0; - y = 0; - } - } - - /* Mouse always acts as single-touch */ - id = 1; - } - -end: - callback(status, x, y, id); Sleep(1); } diff --git a/games/divaio/divaio.h b/games/divaio/divaio.h index 47be34c..b1dec08 100644 --- a/games/divaio/divaio.h +++ b/games/divaio/divaio.h @@ -14,6 +14,8 @@ #include #include +#define DIVA_SLIDER_CELL_COUNT 32 + enum { DIVA_IO_OPBTN_TEST = 0x01, DIVA_IO_OPBTN_SERVICE = 0x02 diff --git a/games/divaio/input/backend.h b/games/divaio/input/backend.h new file mode 100644 index 0000000..186f0cb --- /dev/null +++ b/games/divaio/input/backend.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +#include "divaio/divaio.h" + +struct diva_button_state { + bool primary; + bool prevPrimary; + bool secondary; + bool prevSecondary; + uint8_t dropFrames; + ULONGLONG doubleTapTimeoutBegin; + ULONGLONG doubleTapTimeoutEnd; +}; + +struct diva_button_states { + bool start; + struct diva_button_state triangle; + struct diva_button_state square; + struct diva_button_state circle; + struct diva_button_state cross; +}; + +struct diva_io_backend { + void (*get_opbtns)(uint8_t *opbtn); + void (*get_gamebtns)(struct diva_button_states *gamebtn); + void (*get_slider)(uint32_t *slider); + void (*get_auto_status)(bool* left, bool* right); +}; diff --git a/games/divaio/input/di-dev.c b/games/divaio/input/di-dev.c new file mode 100644 index 0000000..33f35d2 --- /dev/null +++ b/games/divaio/input/di-dev.c @@ -0,0 +1,83 @@ +#include +#include + +#include + +#include "di-dev.h" + +#include "util/dprintf.h" + +HRESULT diva_di_dev_start(IDirectInputDevice8W *dev, HWND wnd) +{ + HRESULT hr; + + assert(dev != NULL); + assert(wnd != NULL); + + hr = IDirectInputDevice8_SetCooperativeLevel( + dev, + wnd, + DISCL_BACKGROUND | DISCL_EXCLUSIVE); + + if (FAILED(hr)) { + dprintf("DirectInput: SetCooperativeLevel failed: %08x\n", (int) hr); + + return hr; + } + + hr = IDirectInputDevice8_SetDataFormat(dev, &c_dfDIJoystick); + + if (FAILED(hr)) { + dprintf("DirectInput: SetDataFormat failed: %08x\n", (int) hr); + + return hr; + } + + hr = IDirectInputDevice8_Acquire(dev); + + if (FAILED(hr)) { + dprintf("DirectInput: Acquire failed: %08x\n", (int) hr); + + return hr; + } + + return hr; +} + + +HRESULT diva_di_dev_poll( + IDirectInputDevice8W *dev, + HWND wnd, + union diva_di_state *out) +{ + HRESULT hr; + MSG msg; + + assert(dev != NULL); + assert(wnd != NULL); + assert(out != NULL); + + memset(out, 0, sizeof(*out)); + + /* Pump our dummy window's message queue just in case DirectInput or an + IHV DirectInput driver somehow relies on it */ + + while (PeekMessageW(&msg, wnd, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + hr = IDirectInputDevice8_GetDeviceState( + dev, + sizeof(out->st), + &out->st); + + if (FAILED(hr)) { + dprintf("DirectInput: GetDeviceState error: %08x\n", (int) hr); + } + + /* JVS lacks a protocol for reporting hardware errors from poll command + responses, so this ends up returning zeroed input state instead. */ + + return hr; +} diff --git a/games/divaio/input/di-dev.h b/games/divaio/input/di-dev.h new file mode 100644 index 0000000..064a281 --- /dev/null +++ b/games/divaio/input/di-dev.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +#include + +union diva_di_state { + DIJOYSTATE st; + uint8_t bytes[sizeof(DIJOYSTATE)]; +}; + +HRESULT diva_di_dev_start(IDirectInputDevice8W *dev, HWND wnd); +HRESULT diva_di_dev_poll( + IDirectInputDevice8W *dev, + HWND wnd, + union diva_di_state *out); + diff --git a/games/divaio/input/di.c b/games/divaio/input/di.c new file mode 100644 index 0000000..6a29096 --- /dev/null +++ b/games/divaio/input/di.c @@ -0,0 +1,335 @@ +#include +#include + +#include "divaio/config.h" +#include "divaio/input/backend.h" +#include "divaio/input/di.h" +#include "divaio/input/di-dev.h" +#include "divaio/input/wnd.h" + +#include "util/dprintf.h" +#include "util/str.h" + +struct diva_di_axis { + wchar_t name[4]; + size_t off; +}; + +static const struct diva_di_axis diva_di_axes[] = { + /* Just map DIJOYSTATE for now, we can map DIJOYSTATE2 later if needed */ + { .name = L"X", .off = DIJOFS_X }, + { .name = L"Y", .off = DIJOFS_Y }, + { .name = L"Z", .off = DIJOFS_Z }, + { .name = L"RX", .off = DIJOFS_RX }, + { .name = L"RY", .off = DIJOFS_RY }, + { .name = L"RZ", .off = DIJOFS_RZ }, + { .name = L"U", .off = DIJOFS_SLIDER(0) }, + { .name = L"V", .off = DIJOFS_SLIDER(1) }, +}; + + +static HRESULT diva_di_config_apply(const struct diva_di_config* cfg); +static BOOL CALLBACK diva_di_enum_callback( + const DIDEVICEINSTANCEW* dev, + void* ctx); +static void diva_di_get_gamebtns(struct diva_button_states* states); +static void diva_di_get_opbtns(uint8_t* opbtn_out); +static uint8_t diva_di_decode_pov(DWORD pov); +static void diva_di_get_auto_status(bool* left, bool* right); + +static const struct diva_io_backend diva_di_backend = { + .get_gamebtns = diva_di_get_gamebtns, + .get_opbtns = diva_di_get_opbtns, + .get_auto_status = diva_di_get_auto_status, +}; + +static HWND diva_di_wnd; +static IDirectInput8W* diva_di_api; +static IDirectInputDevice8W* diva_di_dev; +static uint8_t diva_di_start; +static uint8_t diva_di_service; +static uint8_t diva_di_test; +static uint8_t diva_di_cross[2]; +static uint8_t diva_di_triangle[2]; +static uint8_t diva_di_square[2]; +static uint8_t diva_di_circle[2]; +static uint8_t diva_di_auto_button_left; +static uint8_t diva_di_auto_button_right; +static const struct diva_di_axis* diva_di_auto_axis_left = NULL; +static const struct diva_di_axis* diva_di_auto_axis_right = NULL; +static bool diva_di_auto_left_invert; +static bool diva_di_auto_right_invert; + +static union diva_di_state state = {0}; + +HRESULT diva_di_init( + const struct diva_di_config* cfg, + HINSTANCE inst, + const struct diva_io_backend** backend) { + assert(cfg != NULL); + assert(backend != NULL); + + *backend = NULL; + + HRESULT hr = diva_di_config_apply(cfg); + + if (FAILED(hr)) { + return hr; + } + + hr = diva_io_wnd_create(inst, &diva_di_wnd); + + if (FAILED(hr)) { + return hr; + } + + hr = DirectInput8Create( + inst, + DIRECTINPUT_VERSION, + &IID_IDirectInput8W, + (void**)&diva_di_api, + NULL); + + if (FAILED(hr)) { + dprintf("DirectInput: DirectInput8Create failed: %08x\n", (int)hr); + return hr; + } + + hr = IDirectInput8_EnumDevices( + diva_di_api, + DI8DEVCLASS_GAMECTRL, + diva_di_enum_callback, + (void *) cfg, + DIEDFL_ATTACHEDONLY); + + if (FAILED(hr)) { + dprintf("DirectInput: EnumDevices failed: %08x\n", (int)hr); + + return hr; + } + + if (diva_di_dev == NULL) { + dprintf("Stick: Controller not found\n"); + + return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); + } + + hr = diva_di_dev_start(diva_di_dev, diva_di_wnd); + + if (FAILED(hr)) { + return hr; + } + + dprintf("DirectInput: Controller initialized\n"); + + *backend = &diva_di_backend; + + return S_OK; +} + +static const struct diva_di_axis* diva_di_get_axis(const wchar_t* name) { + for (int i = 0; i < _countof(diva_di_axes); i++) { + const struct diva_di_axis* axis = &diva_di_axes[i]; + + if (wstr_ieq(name, axis->name)) { + return axis; + } + } + + return NULL; +} + +static HRESULT diva_di_config_apply(const struct diva_di_config* cfg) { + + if (cfg->start > 32) { + dprintf("Stick: Invalid start button: %i\n", cfg->start); + return E_INVALIDARG; + } + + for (int i = 0; i < 2; i++) { + if (cfg->triangle[i] > 32) { + dprintf("Stick: Invalid triangle button %d: %i\n", i+1, cfg->triangle[i]); + return E_INVALIDARG; + } + + if (cfg->square[i] > 32) { + dprintf("Stick: Invalid square button %d: %i\n", i+1, cfg->square[i]); + return E_INVALIDARG; + } + + if (cfg->circle[i] > 32) { + dprintf("Stick: Invalid circle button %d: %i\n", i+1, cfg->circle[i]); + return E_INVALIDARG; + } + + if (cfg->cross[i] > 32) { + dprintf("Stick: Invalid cross button %d: %i\n", i+1, cfg->cross[i]); + return E_INVALIDARG; + } + } + + if (cfg->auto_left_button > 32) { + dprintf("Stick: Invalid auto slide left button: %i\n", cfg->auto_left_button); + return E_INVALIDARG; + } + + if (cfg->auto_right_button > 32) { + dprintf("Stick: Invalid auto slide right button: %i\n", cfg->auto_right_button); + return E_INVALIDARG; + } + + diva_di_auto_axis_left = diva_di_get_axis(cfg->auto_left_axis); + diva_di_auto_axis_right = diva_di_get_axis(cfg->auto_right_axis); + + /* Print some debug output to make sure config works... */ + + dprintf("Stick: --- Begin configuration ---\n"); + dprintf("Stick: Device name . . : Contains \"%S\"\n", cfg->device_name); + dprintf("Stick: Start key . . . : %u\n", cfg->start); + for (int i = 0; i < 2; i++) { + dprintf("Stick: Square key %d . . : %u\n", i+1, cfg->square[i]); + dprintf("Stick: Triangle key %d . : %u\n", i+1, cfg->triangle[i]); + dprintf("Stick: Cross key %d . . : %u\n", i+1, cfg->cross[i]); + dprintf("Stick: Circle key %d . . : %u\n", i+1, cfg->circle[i]); + } + dprintf("Stick: Left slide key . : %u\n", cfg->auto_left_button); + dprintf("Stick: Right slide key : %u\n", cfg->auto_right_button); + dprintf("Stick: Left slide axis : %ls\n", diva_di_auto_axis_left != NULL ? diva_di_auto_axis_left->name : L"Disabled"); + dprintf("Stick: Right slide axis : %ls\n", diva_di_auto_axis_right != NULL ? diva_di_auto_axis_right->name : L"Disabled"); + dprintf("Stick: --- End configuration ---\n"); + + diva_di_start = cfg->start; + diva_di_service = cfg->service; + diva_di_test = cfg->test; + + diva_di_auto_button_left = cfg->auto_left_button; + diva_di_auto_button_right = cfg->auto_right_button; + diva_di_auto_left_invert = cfg->auto_left_invert; + diva_di_auto_right_invert = cfg->auto_right_invert; + + for (int i = 0; i < 2; i++) { + diva_di_cross[i] = cfg->cross[i]; + diva_di_circle[i] = cfg->circle[i]; + diva_di_triangle[i] = cfg->triangle[i]; + diva_di_square[i] = cfg->square[i]; + } + + return S_OK; +} + +static BOOL CALLBACK diva_di_enum_callback( + const DIDEVICEINSTANCEW* dev, + void* ctx) { + const struct diva_di_config* cfg = ctx; + + if (wcsstr(dev->tszProductName, cfg->device_name) == NULL) { + return DIENUM_CONTINUE; + } + + dprintf("Stick: Using DirectInput device \"%S\"\n", dev->tszProductName); + + HRESULT hr = IDirectInput8_CreateDevice( + diva_di_api, + &dev->guidInstance, + &diva_di_dev, + NULL); + + if (FAILED(hr)) { + dprintf("Stick: CreateDevice failed: %08x\n", (int)hr); + } + + return DIENUM_STOP; +} + +static void diva_di_get_gamebtns(struct diva_button_states* states) { + assert(states != NULL); + + HRESULT hr = diva_di_dev_poll(diva_di_dev, diva_di_wnd, &state); + + if (FAILED(hr)) { + return; + } + + uint16_t pov = diva_di_decode_pov(state.st.rgdwPOV[0]); + + states->start = diva_di_start && state.st.rgbButtons[diva_di_start - 1]; + states->cross.primary = diva_di_cross[0] && state.st.rgbButtons[diva_di_cross[0] - 1]; + states->cross.secondary = diva_di_cross[1] && state.st.rgbButtons[diva_di_cross[1] - 1]; + states->cross.secondary |= pov & DIVA_IO_GAMEBTN_CROSS; + states->circle.primary = diva_di_circle[0] && state.st.rgbButtons[diva_di_circle[0] - 1]; + states->circle.secondary = diva_di_circle[1] && state.st.rgbButtons[diva_di_circle[1] - 1]; + states->circle.secondary |= pov & DIVA_IO_GAMEBTN_CIRCLE; + states->square.primary = diva_di_square[0] && state.st.rgbButtons[diva_di_square[0] - 1]; + states->square.secondary = diva_di_square[1] && state.st.rgbButtons[diva_di_square[1] - 1]; + states->square.secondary |= pov & DIVA_IO_GAMEBTN_SQUARE; + states->triangle.primary = diva_di_triangle[0] && state.st.rgbButtons[diva_di_triangle[0] - 1]; + states->triangle.secondary = diva_di_triangle[1] && state.st.rgbButtons[diva_di_triangle[1] - 1]; + states->triangle.secondary |= pov & DIVA_IO_GAMEBTN_TRIANGLE; +} + +static uint8_t diva_di_decode_pov(DWORD pov) { + switch (pov) { + case 0: return DIVA_IO_GAMEBTN_TRIANGLE; + case 4500: return DIVA_IO_GAMEBTN_TRIANGLE | DIVA_IO_GAMEBTN_CIRCLE; + case 9000: return DIVA_IO_GAMEBTN_CIRCLE; + case 13500: return DIVA_IO_GAMEBTN_CIRCLE | DIVA_IO_GAMEBTN_CROSS; + case 18000: return DIVA_IO_GAMEBTN_CROSS; + case 22500: return DIVA_IO_GAMEBTN_CROSS | DIVA_IO_GAMEBTN_SQUARE; + case 27000: return DIVA_IO_GAMEBTN_SQUARE; + case 31500: return DIVA_IO_GAMEBTN_SQUARE | DIVA_IO_GAMEBTN_TRIANGLE; + default: return 0; + } +} + +void diva_di_get_auto_status(bool* left, bool* right) { + assert(left != NULL); + assert(right != NULL); + + *left = false; + *right = false; + + if (diva_di_auto_button_left && state.st.rgbButtons[diva_di_auto_button_left - 1]) { + *left = true; + } + if (diva_di_auto_button_right && state.st.rgbButtons[diva_di_auto_button_right - 1]) { + *right = true; + } + + // hardcoded deadzone is kinda ugly + if (diva_di_auto_axis_left != NULL) { + bool l = *(LONG*)&state.bytes[diva_di_auto_axis_left->off] < 0x5FFF; + bool r = *(LONG*)&state.bytes[diva_di_auto_axis_left->off] > 0x9FFF; + if (diva_di_auto_left_invert) { + *left |= r; + *right |= l; + } else { + *left |= l; + *right |= r; + } + } + if (diva_di_auto_axis_right != NULL) { + bool l = *(LONG*)&state.bytes[diva_di_auto_axis_right->off] < 0x5FFF; + bool r = *(LONG*)&state.bytes[diva_di_auto_axis_right->off] > 0x9FFF; + if (diva_di_auto_right_invert) { + *left |= r; + *right |= l; + } else { + *left |= l; + *right |= r; + } + } +} + +static void diva_di_get_opbtns(uint8_t* opbtn_out) { + uint8_t opbtn = 0; + + if (diva_di_service && state.st.rgbButtons[diva_di_service - 1]) { + opbtn |= DIVA_IO_OPBTN_SERVICE; + } + + if (diva_di_test && state.st.rgbButtons[diva_di_test - 1]) { + opbtn |= DIVA_IO_OPBTN_TEST; + } + + *opbtn_out = opbtn; +} diff --git a/games/divaio/input/di.h b/games/divaio/input/di.h new file mode 100644 index 0000000..692b8bf --- /dev/null +++ b/games/divaio/input/di.h @@ -0,0 +1,7 @@ +#pragma once + +#include "divaio/config.h" +#include "divaio/input/backend.h" + +HRESULT diva_di_init(const struct diva_di_config *cfg, HINSTANCE inst, + const struct diva_io_backend **backend); diff --git a/games/divaio/input/kb.c b/games/divaio/input/kb.c new file mode 100644 index 0000000..818b616 --- /dev/null +++ b/games/divaio/input/kb.c @@ -0,0 +1,156 @@ +#include + +#include +#include + +#include "divaio/config.h" +#include "divaio/divaio.h" +#include "divaio/input/backend.h" +#include "divaio/input/kb.h" + +#include "util/dprintf.h" + +static uint8_t diva_kb_start; +static uint8_t diva_kb_circle; +static uint8_t diva_kb_cross; +static uint8_t diva_kb_square; +static uint8_t diva_kb_triangle; +static uint8_t diva_kb_auto_left; +static uint8_t diva_kb_auto_right; +static uint8_t diva_kb_slider[DIVA_SLIDER_CELL_COUNT]; + +static void diva_kb_get_gamebtns(struct diva_button_states* states); +static void diva_kb_get_auto_status(bool* l, bool* r); +static void diva_kb_get_slider(uint32_t* slider); + +static HRESULT diva_kb_config_apply(const struct diva_kb_config* cfg); + +static const struct diva_io_backend diva_kb_backend = { + .get_gamebtns = diva_kb_get_gamebtns, + .get_slider = diva_kb_get_slider, + .get_auto_status = diva_kb_get_auto_status, +}; + +HRESULT diva_kb_init(const struct diva_kb_config* cfg, + const struct diva_io_backend** backend) { + assert(cfg != NULL); + assert(backend != NULL); + + *backend = NULL; + + HRESULT hr = diva_kb_config_apply(cfg); + + if (FAILED(hr)) { + return hr; + } + + dprintf("Diva IO: Using keyboard input\n"); + *backend = &diva_kb_backend; + + return S_OK; +} + +static HRESULT diva_kb_config_apply(const struct diva_kb_config* cfg) { + if (cfg->vk_start > 255) { + dprintf("Diva IO: Invalid start key configuration: %u\n", cfg->vk_start); + return E_INVALIDARG; + } + + if (cfg->vk_cross > 255) { + dprintf("Diva IO: Invalid cross key configuration: %u\n", cfg->vk_cross); + return E_INVALIDARG; + } + + if (cfg->vk_circle > 255) { + dprintf("Diva IO: Invalid circle key configuration: %u\n", cfg->vk_circle); + return E_INVALIDARG; + } + + if (cfg->vk_square > 255) { + dprintf("Diva IO: Invalid square key configuration: %u\n", cfg->vk_square); + return E_INVALIDARG; + } + + if (cfg->vk_triangle > 255) { + dprintf("Diva IO: Invalid triangle key configuration: %u\n", cfg->vk_triangle); + return E_INVALIDARG; + } + + if (cfg->vk_auto_left > 255) { + dprintf("Diva IO: Invalid auto slide left key configuration: %u\n", cfg->vk_auto_left); + return E_INVALIDARG; + } + + if (cfg->vk_auto_right > 255) { + dprintf("Diva IO: Invalid auto slide right key configuration: %u\n", cfg->vk_auto_right); + return E_INVALIDARG; + } + + for (int i = 0; i < DIVA_SLIDER_CELL_COUNT; i++) { + if (cfg->vk_slider[i] > 255) { + dprintf("Diva IO: Invalid auto slider cell %d key configuration: %u\n", i+ 1, cfg->vk_slider[i]); + return E_INVALIDARG; + } + } + + /* Apply the configuration */ + diva_kb_start = cfg->vk_start; + diva_kb_circle = cfg->vk_circle; + diva_kb_cross = cfg->vk_cross; + diva_kb_triangle = cfg->vk_triangle; + diva_kb_square = cfg->vk_square; + diva_kb_auto_left = cfg->vk_auto_left; + diva_kb_auto_right = cfg->vk_auto_right; + + for (int i = 0; i < DIVA_SLIDER_CELL_COUNT; i++) { + diva_kb_slider[i] = cfg->vk_slider[i]; + } + + /* Print some debug output to make sure config works... */ + dprintf("Diva IO: --- Begin configuration ---\n"); + dprintf("Diva IO: Start key . . . : %u\n", diva_kb_start); + dprintf("Diva IO: Square key . . . : %u\n", diva_kb_square); + dprintf("Diva IO: Triangle key . . : %u\n", diva_kb_triangle); + dprintf("Diva IO: Cross key . . . : %u\n", diva_kb_cross); + dprintf("Diva IO: Circle key . . . : %u\n", diva_kb_circle); + dprintf("Diva IO: Left slide key . : %u\n", diva_kb_auto_left); + dprintf("Diva IO: Right slide key : %u\n", diva_kb_auto_right); + + /* Print the configuration for all 8 buttons */ + for (int i = 0; i < DIVA_SLIDER_CELL_COUNT; i++) { + dprintf("Diva IO: Slider %02i . . . : %u\n", i + 1, diva_kb_slider[i]); + } + + dprintf("Diva IO: --- End configuration ---\n"); + + return S_OK; +} + +static void diva_kb_get_gamebtns(struct diva_button_states* states) { + assert(states != NULL); + + states->start = GetAsyncKeyState(diva_kb_start) & 0x8000; + states->cross.primary = GetAsyncKeyState(diva_kb_cross) & 0x8000; + states->square.primary = GetAsyncKeyState(diva_kb_square) & 0x8000; + states->triangle.primary = GetAsyncKeyState(diva_kb_triangle) & 0x8000; + states->circle.primary = GetAsyncKeyState(diva_kb_circle) & 0x8000; + +} + +static void diva_kb_get_auto_status(bool* l, bool* r) { + assert(l != NULL); + assert(r != NULL); + + *l = GetAsyncKeyState(diva_kb_auto_left) & 0x8000; + *r = GetAsyncKeyState(diva_kb_auto_right) & 0x8000; +} + +void diva_kb_get_slider(uint32_t* slider) { + assert(slider != NULL); + + for (int i = 0; i < DIVA_SLIDER_CELL_COUNT; i++) { + if (GetAsyncKeyState(diva_kb_slider[i]) & 0x8000) { + *slider |= 1 << i; + } + } +} diff --git a/games/divaio/input/kb.h b/games/divaio/input/kb.h new file mode 100644 index 0000000..295ea4a --- /dev/null +++ b/games/divaio/input/kb.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +#include "divaio/config.h" +#include "divaio/input/backend.h" + +HRESULT diva_kb_init(const struct diva_kb_config *cfg, + const struct diva_io_backend **backend); diff --git a/games/divaio/input/wnd.c b/games/divaio/input/wnd.c new file mode 100644 index 0000000..c342bc9 --- /dev/null +++ b/games/divaio/input/wnd.c @@ -0,0 +1,86 @@ +#include + +#include +#include + +#include "util/dprintf.h" + +/* DirectInput requires a window for correct initialization (and also force + feedback), so this source file provides some utilities for creating a + generic message-only window. */ + +static LRESULT WINAPI diva_io_wnd_proc( + HWND hwnd, + UINT msg, + WPARAM wparam, + LPARAM lparam); + +HRESULT diva_io_wnd_create(HINSTANCE inst, HWND *out) +{ + HRESULT hr; + WNDCLASSEXW wcx; + ATOM atom; + HWND hwnd; + + assert(inst != NULL); /* We are not an EXE */ + assert(out != NULL); + + *out = NULL; + + memset(&wcx, 0, sizeof(wcx)); + wcx.cbSize = sizeof(wcx); + wcx.lpfnWndProc = diva_io_wnd_proc; + wcx.hInstance = inst; + wcx.lpszClassName = L"divaIO"; + + atom = RegisterClassExW(&wcx); + + if (atom == 0) { + hr = HRESULT_FROM_WIN32(GetLastError()); + dprintf("divaIO: RegisterClassExW failed: %08x\n", (int) hr); + + goto fail; + } + + hwnd = CreateWindowExW( + 0, + (wchar_t *) (intptr_t) atom, + L"", + 0, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + HWND_MESSAGE, + NULL, + inst, + NULL); + + if (hwnd == NULL) { + hr = HRESULT_FROM_WIN32(GetLastError()); + dprintf("divaIO: CreateWindowExW failed: %08x\n", (int) hr); + + goto fail; + } + + *out = hwnd; + + return S_OK; + +fail: + UnregisterClassW((wchar_t *) (intptr_t) atom, inst); + + return hr; +} + +static LRESULT WINAPI diva_io_wnd_proc( + HWND hwnd, + UINT msg, + WPARAM wparam, + LPARAM lparam) +{ + switch (msg) { + default: + return DefWindowProcW(hwnd, msg, wparam, lparam); + } +} diff --git a/games/divaio/input/wnd.h b/games/divaio/input/wnd.h new file mode 100644 index 0000000..29ff763 --- /dev/null +++ b/games/divaio/input/wnd.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +HRESULT diva_io_wnd_create(HINSTANCE inst, HWND *out); diff --git a/games/divaio/input/xi.c b/games/divaio/input/xi.c new file mode 100644 index 0000000..8f8ec33 --- /dev/null +++ b/games/divaio/input/xi.c @@ -0,0 +1,128 @@ +#include +#include + +#include +#include +#include + +#include "divaio/config.h" +#include "divaio/input/backend.h" +#include "divaio/input/xi.h" + +#include "util/dprintf.h" + +static void diva_xi_get_gamebtns(struct diva_button_states* states); +static void diva_xi_get_opbtns(uint8_t* opbtn); +static void diva_xi_get_auto_status(bool* left, bool* right); + +static HRESULT diva_xi_config_apply(const struct diva_xi_config* cfg); + + +static const struct diva_io_backend diva_xi_backend = { + .get_gamebtns = diva_xi_get_gamebtns, + .get_opbtns = diva_xi_get_opbtns, + .get_auto_status = diva_xi_get_auto_status, +}; + +static bool diva_xi_test_enabled; + +static XINPUT_STATE xi = {0}; + +HRESULT diva_xi_init(const struct diva_xi_config* cfg, const struct diva_io_backend** backend) { + assert(cfg != NULL); + assert(backend != NULL); + + HRESULT hr = diva_xi_config_apply(cfg); + + if (FAILED(hr)) { + return hr; + } + + dprintf("XInput: Using XInput controller\n"); + *backend = &diva_xi_backend; + + return S_OK; +} + +static HRESULT diva_xi_config_apply(const struct diva_xi_config* cfg) { + dprintf("XInput: --- Begin configuration ---\n"); + dprintf("XInput: Test Button : %i\n", cfg->use_select_as_test); + dprintf("XInput: --- End configuration ---\n"); + + diva_xi_test_enabled = cfg->use_select_as_test; + + return S_OK; +} + +static void diva_xi_get_gamebtns(struct diva_button_states* states) { + + assert(states != NULL); + + memset(&xi, 0, sizeof(xi)); + XInputGetState(0, &xi); + + const WORD xb = xi.Gamepad.wButtons; + + states->start = xb & XINPUT_GAMEPAD_START; + states->cross.primary = xb & XINPUT_GAMEPAD_A; + states->cross.secondary = xb & XINPUT_GAMEPAD_DPAD_DOWN; + states->circle.primary = xb & XINPUT_GAMEPAD_B; + states->circle.secondary = xb & XINPUT_GAMEPAD_DPAD_RIGHT; + states->triangle.primary = xb & XINPUT_GAMEPAD_Y; + states->triangle.secondary = xb & XINPUT_GAMEPAD_DPAD_UP; + states->square.primary = xb & XINPUT_GAMEPAD_X; + states->square.secondary = xb & XINPUT_GAMEPAD_DPAD_LEFT; +} + +void diva_xi_get_opbtns(uint8_t* opbtn) { + assert(opbtn != NULL); + + uint8_t opbtn_out = 0; + + if (diva_xi_test_enabled && xi.Gamepad.wButtons & XINPUT_GAMEPAD_BACK) { + opbtn_out |= DIVA_IO_OPBTN_TEST; + } + + *opbtn = opbtn_out; +} + +void diva_xi_get_auto_status(bool* left, bool* right) { + assert(left != NULL); + assert(right != NULL); + + *left = false; + *right = false; + + const int left_x = xi.Gamepad.sThumbLX; + const int right_x = xi.Gamepad.sThumbRX; + + if (left_x < -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE * 2) { + *left |= true; + } else if (left_x > XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE * 2) { + *right |= true; + } + + if (right_x < -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE * 2) { + *left |= true; + } else if (right_x > XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE * 2) { + *right |= true; + } + + const WORD xb = xi.Gamepad.wButtons; + + if (xb & XINPUT_GAMEPAD_LEFT_SHOULDER) { + *left |= true; + } + + if (xb & XINPUT_GAMEPAD_RIGHT_SHOULDER) { + *right |= true; + } + + if (xi.Gamepad.bLeftTrigger > 64) { + *left |= true; + } + + if (xi.Gamepad.bRightTrigger > 64) { + *right |= true; + } +} diff --git a/games/divaio/input/xi.h b/games/divaio/input/xi.h new file mode 100644 index 0000000..cd5c7c8 --- /dev/null +++ b/games/divaio/input/xi.h @@ -0,0 +1,11 @@ +#pragma once + +/* Can't call this xinput.h or it will conflict with */ + +#include + +#include "divaio/config.h" +#include "divaio/input/backend.h" + +HRESULT diva_xi_init(const struct diva_xi_config *cfg, + const struct diva_io_backend **backend); diff --git a/games/divaio/meson.build b/games/divaio/meson.build index 3d4f611..5df85ba 100644 --- a/games/divaio/meson.build +++ b/games/divaio/meson.build @@ -3,10 +3,34 @@ divaio_lib = static_library( name_prefix : '', include_directories : inc, implicit_include_directories : false, + dependencies : [ + dinput8_lib, + dxguid_lib, + xinput_lib, + ], + link_with : [ + util_lib, + ], sources : [ 'divaio.c', 'divaio.h', 'config.c', 'config.h', + 'input/backend.h', + 'input/di.c', + 'input/di.h', + 'input/di-dev.c', + 'input/di-dev.h', + 'input/kb.c', + 'input/kb.h', + 'input/wnd.c', + 'input/wnd.h', + 'input/xi.c', + 'input/xi.h', + 'touch/backend.h', + 'touch/mouse.c', + 'touch/mouse.h', + 'touch/wintouch.c', + 'touch/wintouch.h', ], ) diff --git a/games/divaio/touch/backend.h b/games/divaio/touch/backend.h new file mode 100644 index 0000000..ace6a15 --- /dev/null +++ b/games/divaio/touch/backend.h @@ -0,0 +1,8 @@ +#pragma once + +#include "divaio/divaio.h" + +struct diva_touch_backend { + HRESULT (*init)(); + void (*update)(diva_io_touch_callback_t callback); +}; diff --git a/games/divaio/touch/mouse.c b/games/divaio/touch/mouse.c new file mode 100644 index 0000000..6669f30 --- /dev/null +++ b/games/divaio/touch/mouse.c @@ -0,0 +1,79 @@ +#include "backend.h" + +static void diva_mouse_update(diva_io_touch_callback_t callback); + +static const struct diva_touch_backend diva_mouse_backend = { + .update = diva_mouse_update +}; + +static int diva_io_m1 = VK_LBUTTON; + +HRESULT diva_touch_mouse_init(const struct diva_touch_backend** backend) { + diva_io_m1 = GetSystemMetrics(SM_SWAPBUTTON) ? VK_RBUTTON : VK_LBUTTON; + + *backend = &diva_mouse_backend; + + return S_OK; +} + +static uint8_t status = 0; +static uint16_t x = 0; +static uint16_t y = 0; +static uint16_t last_x = 0; +static uint16_t last_y = 0; +static uint8_t id = 0; +static bool touch = false; + +void diva_mouse_update(diva_io_touch_callback_t callback) { + if (GetAsyncKeyState(diva_io_m1) & 0x8000) { + POINT point; + + /* Get cursor location and map to window size */ + BOOL ok = GetCursorPos(&point); + + if (!ok) { + return; + } + + HWND hwnd = GetForegroundWindow(); + + ok = ScreenToClient(hwnd, &point); + + if (!ok) { + return; + } + + /* Set status */ + if (!touch) { + status = DIVA_IO_TOUCH_DOWN; + touch = true; + } else { + status = DIVA_IO_TOUCH_STREAM; + } + + /* Set coordinates */ + if (point.x < 0) point.x = 0; + if (point.y < 0) point.y = 0; + x = (uint16_t)point.x; + y = (uint16_t)point.y; + last_x = x; + last_y = y; + } else { + if (touch) { + status = DIVA_IO_TOUCH_LIFTOFF; + x = last_x; + y = last_y; + touch = false; + } else { + /* No touch event */ + status = 0; + x = 0; + y = 0; + } + } + + /* Mouse always acts as single-touch */ + id = 1; + + callback(status, x, y, id); +} diff --git a/games/divaio/touch/mouse.h b/games/divaio/touch/mouse.h new file mode 100644 index 0000000..df0922a --- /dev/null +++ b/games/divaio/touch/mouse.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +#include "backend.h" + +HRESULT diva_touch_mouse_init(const struct diva_touch_backend **backend); diff --git a/games/divaio/touch/wintouch.c b/games/divaio/touch/wintouch.c new file mode 100644 index 0000000..64c60d6 --- /dev/null +++ b/games/divaio/touch/wintouch.c @@ -0,0 +1,164 @@ +#include "backend.h" +#include "util/dprintf.h" + +#include +#include +#include + +#define DEBUG 0 + +static HRESULT diva_wintouch_init(); +static void diva_wintouch_update(diva_io_touch_callback_t callback); +static void diva_handle_touch(UINT cInputs, PTOUCHINPUT pInputs); + +static const struct diva_touch_backend diva_wintouch_backend = { + .init = diva_wintouch_init, + .update = diva_wintouch_update +}; + +static HWND window_handle = NULL; +static const wchar_t* window_title = L"Hatsune Miku Project DIVA Arcade Future Tone"; +static WNDPROC original_wndproc = NULL; + +static uint8_t status = 0; +static uint16_t x = 0; +static uint16_t y = 0; +static bool touch = false; + +struct handle_data { + unsigned long process_id; + HWND window_handle; +}; + +BOOL is_main_window(HWND handle) { + return GetWindow(handle, GW_OWNER) == (HWND)0 && IsWindowVisible(handle); +} + +BOOL CALLBACK enum_windows_callback(HWND handle, LPARAM lParam) { + struct handle_data* data = (struct handle_data*)lParam; + unsigned long process_id = 0; + GetWindowThreadProcessId(handle, &process_id); + if (data->process_id != process_id || !is_main_window(handle)) + return TRUE; + data->window_handle = handle; + return FALSE; +} + +HWND find_main_window(unsigned long process_id) { + struct handle_data data = {0}; + data.process_id = process_id; + data.window_handle = 0; + EnumWindows(enum_windows_callback, (LPARAM)&data); + return data.window_handle; +} + +static HWND get_window() { + if (window_handle != NULL) { + return window_handle; + } + + HWND hwnd = find_main_window(GetCurrentProcessId()); + if (hwnd == NULL) { + return NULL; + } + + dprintf("Diva IO: Touch: Program window detected\n"); + window_handle = hwnd; + return window_handle; +} + +HRESULT diva_touch_wintouch_init(const struct diva_touch_backend** backend) { + if (!(GetSystemMetrics(SM_DIGITIZER) & NID_READY)) { + dprintf("Diva IO: Touch: No touchscreen connected!\n"); + return E_FAIL; + } + + *backend = &diva_wintouch_backend; + + return S_OK; +} + +LRESULT CALLBACK diva_touch_wndproc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { + if (msg == WM_TOUCH) { + const UINT cInputs = LOWORD(wParam); + const PTOUCHINPUT pInputs = (PTOUCHINPUT)malloc(sizeof(TOUCHINPUT) * cInputs); + + if (pInputs == NULL) { + dprintf("Diva IO: Touch: Allocation error\n"); + return 0; + } + + if (GetTouchInputInfo((HTOUCHINPUT)lParam, cInputs, pInputs, sizeof(TOUCHINPUT))) { + diva_handle_touch(cInputs, pInputs); + } + + free(pInputs); + CloseTouchInputHandle((HTOUCHINPUT)lParam); + + return 0; + } else if (original_wndproc != NULL) { + return CallWindowProcW(original_wndproc, hWnd, msg, wParam, lParam); + } else { + return DefWindowProcW(hWnd, msg, wParam, lParam); + } +} + +HRESULT diva_wintouch_init() { + window_handle = get_window(); + if (window_handle == NULL) { + dprintf("Diva IO: Touch: Failed to find main window\n"); + return E_FAIL; + } + + if (!RegisterTouchWindow(window_handle, 0)) { + dprintf("Diva IO: Touch: Failed to register touch window: %lx\n", GetLastError()); + return E_FAIL; + } + + original_wndproc = (WNDPROC)GetWindowLongPtrW(window_handle, GWLP_WNDPROC); + if (original_wndproc == NULL) { + dprintf("Diva IO: Touch: Failed to retrieve original window procedure: %lx\n", GetLastError()); + return E_FAIL; + } + + if (!SetWindowLongPtrW(window_handle, GWLP_WNDPROC, (LONG_PTR)&diva_touch_wndproc)) { + dprintf("Diva IO: Touch: Failed to set new window procedure: %lx\n", GetLastError()); + return E_FAIL; + } + + dprintf("Diva IO: Touch: WinTouch initialized\n"); + return S_OK; +} + +void diva_wintouch_update(diva_io_touch_callback_t callback) { + if (status == DIVA_IO_TOUCH_LIFTOFF) { + status = 0; + touch = false; + } else if (!touch) { + status = 0; + x = 0; + y = 0; + } + + callback(status, x, y, 1); +} + +static void diva_handle_touch(const UINT cInputs, const PTOUCHINPUT pInputs) { + if (cInputs == 0) { + return; + } + + // The Elo touchscreen is single touch, so we ignore everything except the first touch. + const TOUCHINPUT ti = pInputs[0]; + + if (ti.dwFlags & (TOUCHEVENTF_DOWN | TOUCHEVENTF_MOVE)) { + touch = true; + status = ti.dwFlags & TOUCHEVENTF_DOWN ? DIVA_IO_TOUCH_DOWN : DIVA_IO_TOUCH_STREAM; + } else if (ti.dwFlags & TOUCHEVENTF_UP) { + status = DIVA_IO_TOUCH_LIFTOFF; + } + + x = max(0, ti.x / 100); + y = max(0, ti.y / 100); + +} diff --git a/games/divaio/touch/wintouch.h b/games/divaio/touch/wintouch.h new file mode 100644 index 0000000..56fcb2a --- /dev/null +++ b/games/divaio/touch/wintouch.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +#include "backend.h" + +HRESULT diva_touch_wintouch_init(const struct diva_touch_backend **backend);