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);