From e2c15afc68ec88677051176764846d65efe7dd22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radek=20Krzy=C5=9Bk=C3=B3w?= <46760021+Flower35@users.noreply.github.com> Date: Sun, 30 Jun 2024 06:46:14 +0200 Subject: [PATCH] Windows platform enhancements (#84) * Enabling portable paths for Windows (custom user preferences dir, drag'n'drop paths) * Updated the windows messages loop for DXGI window API * Fixed international keyboard layouts text input in DXGI --- src/pc/fs/fs.c | 2 +- src/pc/gfx/gfx_dxgi.cpp | 219 ++++++++++++++++++++++----------------- src/pc/gfx/gfx_sdl2.c | 15 ++- src/pc/mods/mods_utils.c | 9 +- src/pc/pc_main.c | 21 +++- src/pc/platform.c | 120 +++++++++++++++++++-- src/pc/platform.h | 4 + 7 files changed, 275 insertions(+), 115 deletions(-) diff --git a/src/pc/fs/fs.c b/src/pc/fs/fs.c index 34de1d60..4e0746bf 100644 --- a/src/pc/fs/fs.c +++ b/src/pc/fs/fs.c @@ -52,7 +52,7 @@ bool fs_init(const char *writepath) { #endif // we shall not progress any further if the path is inaccessible - if ('\0' == fs_writepath[0]) { + if (('\0' == fs_writepath[0]) || !fs_sys_dir_exists(fs_writepath)) { sys_fatal("Could not access the User Preferences directory."); } diff --git a/src/pc/gfx/gfx_dxgi.cpp b/src/pc/gfx/gfx_dxgi.cpp index 0aa56897..e6fbad35 100644 --- a/src/pc/gfx/gfx_dxgi.cpp +++ b/src/pc/gfx/gfx_dxgi.cpp @@ -24,15 +24,13 @@ #include "gfx_dxgi.h" extern "C" { -#include "pc/mods/mod_import.h" -#ifdef DISCORD_SDK -#include "pc/discord/discord.h" -#endif -#include "pc/network/version.h" + #include "pc/mods/mod_import.h" + #include "pc/rom_checker.h" + #include "pc/network/version.h" + #include "pc/configfile.h" } -#include "../configfile.h" -#include "../pc_main.h" +#include "pc/pc_main.h" #include "gfx_window_manager_api.h" #include "gfx_rendering_api.h" @@ -91,7 +89,6 @@ static struct { bool sync_interval_means_frames_to_wait; UINT length_in_vsync_frames; - void (*run_one_game_iter)(void); bool (*on_key_down)(int scancode); bool (*on_key_up)(int scancode); void (*on_all_keys_up)(void); @@ -244,100 +241,122 @@ static void gfx_dxgi_on_resize(void) { } } -static void onkeydown(WPARAM w_param, LPARAM l_param) { +static void gfx_dxgi_on_key_down(WPARAM w_param, LPARAM l_param) { int key = ((l_param >> 16) & 0x1ff); - if (inTextInput) { - const int keyboardScanCode = (l_param >> 16) & 0x00ff; - const int virtualKey = w_param; - - BYTE keyboardState[256]; - GetKeyboardState(keyboardState); - - WORD ascii = 0; - const int len = ToAscii(virtualKey, keyboardScanCode, keyboardState, &ascii, 0); - if (len > 0) { - dxgi.on_text_input((char*)&ascii); - } - } - if (dxgi.on_key_down != nullptr) { - dxgi.on_key_down(key); - } + if (dxgi.on_key_down != nullptr) { dxgi.on_key_down(key); } } -static void onkeyup(WPARAM w_param, LPARAM l_param) { +static void gfx_dxgi_on_key_up(WPARAM w_param, LPARAM l_param) { int key = ((l_param >> 16) & 0x1ff); - if (dxgi.on_key_up != nullptr) { - dxgi.on_key_up(key); + if (dxgi.on_key_up != nullptr) { dxgi.on_key_up(key); } +} + +static void gfx_dxgi_on_text_input(wchar_t code_unit) { + if (inTextInput && (!IS_HIGH_SURROGATE(code_unit)) && (!IS_LOW_SURROGATE(code_unit))) { + char utf8_buffer[3 + 1]; + if (code_unit >= 0x0800) { // 3-byte encoding + utf8_buffer[0] = 0xe0 | ((code_unit >> 12) & 0x0f); + utf8_buffer[1] = 0x80 | ((code_unit >> 6) & 0x3f); + utf8_buffer[2] = 0x80 | (code_unit & 0x3f); + utf8_buffer[3] = '\0'; + } else if (code_unit >= 0x0080) { // 2-byte encoding + utf8_buffer[0] = 0xc0 | ((code_unit >> 6) & 0x1f); + utf8_buffer[1] = 0x80 | (code_unit & 0x3f); + utf8_buffer[2] = '\0'; + } else { // 1-byte encoding + if (code_unit < ' ') { return; } // skipping control chars + utf8_buffer[0] = (char)code_unit; + utf8_buffer[1] = '\0'; + } + + dxgi.on_text_input(utf8_buffer); } } static LRESULT CALLBACK gfx_dxgi_wnd_proc(HWND h_wnd, UINT message, WPARAM w_param, LPARAM l_param) { + WCHAR wcsFileName[MAX_PATH]; + char szFileName[MAX_PATH]; + switch (message) { - case WM_SIZE: + case WM_SIZE: { gfx_dxgi_on_resize(); - break; - case WM_DESTROY: + return 0; + } + case WM_CLOSE: { + DestroyWindow(h_wnd); + return 0; + } + case WM_DESTROY: { game_exit(); - break; - case WM_PAINT: - if (dxgi.showing_error) { - return DefWindowProcW(h_wnd, message, w_param, l_param); - } else { - if (dxgi.run_one_game_iter != nullptr) { - dxgi.run_one_game_iter(); - } - } - break; - case WM_ACTIVATEAPP: + PostQuitMessage(0); + return 0; + } + case WM_ACTIVATEAPP: { if (dxgi.on_all_keys_up != nullptr) { dxgi.on_all_keys_up(); + return 0; } break; - case WM_KEYDOWN: - onkeydown(w_param, l_param); - break; - case WM_KEYUP: - onkeyup(w_param, l_param); - break; - case WM_SYSKEYDOWN: + } + case WM_KEYDOWN: { + gfx_dxgi_on_key_down(w_param, l_param); + return 0; + } + case WM_KEYUP: { + gfx_dxgi_on_key_up(w_param, l_param); + return 0; + } + case WM_CHAR: { + // some keyboard input translated to a single UTF-16LE code unit + gfx_dxgi_on_text_input((wchar_t)w_param); + return 0; + } + case WM_SYSKEYDOWN: { if ((w_param == VK_RETURN) && ((l_param & 1 << 30) == 0)) { toggle_borderless_window_full_screen(!dxgi.is_full_screen); - break; - } else { - return DefWindowProcW(h_wnd, message, w_param, l_param); - } - case WM_DROPFILES: { - HDROP hDrop = (HDROP)w_param; - UINT nFiles = DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0); - for (UINT i = 0; i < nFiles; i++) - { - char szFileName[MAX_PATH] = { 0 }; - DragQueryFile(hDrop, i, szFileName, MAX_PATH); - mod_import_file(szFileName); - } - DragFinish(hDrop); + return 0; } break; - default: - return DefWindowProcW(h_wnd, message, w_param, l_param); + } + case WM_LBUTTONDOWN: { + if (!gRomIsValid) { + OPENFILENAMEW ofn; + ZeroMemory(&ofn, sizeof(ofn)); + ofn.lStructSize = sizeof(ofn); + ofn.hwndOwner = h_wnd; + ofn.lpstrFilter = L"N64 ROM files (*.z64)\0*.z64\0"; + ofn.lpstrFile = wcsFileName; + ofn.nMaxFile = MAX_PATH; + ofn.lpstrTitle = L"Select the \"Super Mario 64 (U) [!]\" ROM file.."; + ofn.Flags = (OFN_DONTADDTORECENT | OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST); + + wcsFileName[0] = L'\0'; + if (GetOpenFileNameW(&ofn) && sys_windows_short_path_from_wcs(szFileName, MAX_PATH, wcsFileName)) { + rom_on_drop_file(szFileName); + } + return 0; + } + break; + } + case WM_DROPFILES: { + HDROP hDrop = (HDROP)w_param; + UINT nFiles = DragQueryFileW(hDrop, 0xFFFFFFFF, NULL, 0); + for (UINT i = 0; i < nFiles; i++) { + if (0 != DragQueryFileW(hDrop, i, wcsFileName, MAX_PATH)) { + if (sys_windows_short_path_from_wcs(szFileName, MAX_PATH, wcsFileName)) { + if (!gRomIsValid) { + rom_on_drop_file(szFileName); + } else if (gGameInited) { + mod_import_file(szFileName); + } + } + } + } + DragFinish(hDrop); + return 0; + } } - if (configWindow.reset) { - dxgi.last_maximized_state = false; - configWindow.reset = false; - configWindow.x = WAPI_WIN_CENTERPOS; - configWindow.y = WAPI_WIN_CENTERPOS; - configWindow.w = DESIRED_SCREEN_WIDTH; - configWindow.h = DESIRED_SCREEN_HEIGHT; - configWindow.fullscreen = false; - configWindow.settings_changed = true; - } - - if (configWindow.settings_changed) { - configWindow.settings_changed = false; - update_screen_settings(); - } - - return 0; + return DefWindowProcW(h_wnd, message, w_param, l_param); } static void gfx_dxgi_init(const char *window_title) { @@ -406,16 +425,7 @@ static void gfx_dxgi_set_keyboard_callbacks(bool (*on_key_down)(int scancode), b } static void gfx_dxgi_main_loop(void (*run_one_game_iter)(void)) { - dxgi.run_one_game_iter = run_one_game_iter; - - MSG msg; - while (GetMessage(&msg, nullptr, 0, 0)) { - TranslateMessage(&msg); - DispatchMessage(&msg); -#ifdef DISCORD_SDK - discord_update(); -#endif - } + run_one_game_iter(); } static void gfx_dxgi_get_dimensions(uint32_t *width, uint32_t *height) { @@ -424,11 +434,28 @@ static void gfx_dxgi_get_dimensions(uint32_t *width, uint32_t *height) { } static void gfx_dxgi_handle_events(void) { - /*MSG msg; - while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { + MSG msg; + while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE) != 0) { TranslateMessage(&msg); - DispatchMessage(&msg); - }*/ + DispatchMessageW(&msg); + if (msg.message == WM_QUIT) { break; } + } + + if (configWindow.reset) { + dxgi.last_maximized_state = false; + configWindow.reset = false; + configWindow.x = WAPI_WIN_CENTERPOS; + configWindow.y = WAPI_WIN_CENTERPOS; + configWindow.w = DESIRED_SCREEN_WIDTH; + configWindow.h = DESIRED_SCREEN_HEIGHT; + configWindow.fullscreen = false; + configWindow.settings_changed = true; + } + + if (configWindow.settings_changed) { + configWindow.settings_changed = false; + update_screen_settings(); + } } static uint64_t qpc_to_us(uint64_t qpc) { diff --git a/src/pc/gfx/gfx_sdl2.c b/src/pc/gfx/gfx_sdl2.c index 10a96d37..78739576 100644 --- a/src/pc/gfx/gfx_sdl2.c +++ b/src/pc/gfx/gfx_sdl2.c @@ -186,13 +186,22 @@ static void gfx_sdl_onkeyup(int scancode) { } static void gfx_sdl_ondropfile(char* path) { +#ifdef _WIN32 + char portable_path[SYS_MAX_PATH]; + if (sys_windows_short_path_from_mbs(portable_path, SYS_MAX_PATH, path)) { + if (!gRomIsValid) { + rom_on_drop_file(portable_path); + } else if (gGameInited) { + mod_import_file(portable_path); + } + } +#else if (!gRomIsValid) { rom_on_drop_file(path); - return; - } - if (gGameInited) { + } else if (gGameInited) { mod_import_file(path); } +#endif } static void gfx_sdl_handle_events(void) { diff --git a/src/pc/mods/mods_utils.c b/src/pc/mods/mods_utils.c index f78d36c7..73444265 100644 --- a/src/pc/mods/mods_utils.c +++ b/src/pc/mods/mods_utils.c @@ -168,7 +168,14 @@ bool str_ends_with(const char* string, const char* suffix) { if (suffixLength > stringLength) { return false; } - return !strcmp(&string[stringLength - suffixLength], suffix); +#ifdef _WIN32 + // Paths on Windows are case-insensitive and might have + // upper-case or mixed-case endings. + return (0 == _stricmp(&(string[stringLength - suffixLength]), suffix)); +#else + // Always expecting lower-case file paths and extensions + return (0 == strcmp(&(string[stringLength - suffixLength]), suffix)); +#endif } ////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/pc/pc_main.c b/src/pc/pc_main.c index b03895e5..ec4e94a6 100644 --- a/src/pc/pc_main.c +++ b/src/pc/pc_main.c @@ -353,17 +353,28 @@ int main(int argc, char *argv[]) { // handle terminal arguments if (!parse_cli_opts(argc, argv)) { return 0; } -#if defined(_WIN32) || defined(_WIN64) +#ifdef _WIN32 // handle Windows console - if (!gCLIOpts.console) { + if (gCLIOpts.console) { + SetConsoleOutputCP(CP_UTF8); + } else { FreeConsole(); freopen("NUL", "w", stdout); } - #endif - const char* userPath = gCLIOpts.savePath[0] ? gCLIOpts.savePath : sys_user_path(); - fs_init(userPath); +#ifdef _WIN32 + if (gCLIOpts.savePath[0]) { + char portable_path[SYS_MAX_PATH] = {}; + sys_windows_short_path_from_mbs(portable_path, SYS_MAX_PATH, gCLIOpts.savePath); + fs_init(portable_path); + } else { + fs_init(sys_user_path()); + } +#else + fs_init(gCLIOpts.savePath[0] ? gCLIOpts.savePath : sys_user_path()); +#endif + configfile_load(); legacy_folder_handler(); diff --git a/src/pc/platform.c b/src/pc/platform.c index e81f8052..0e83ec46 100644 --- a/src/pc/platform.c +++ b/src/pc/platform.c @@ -5,7 +5,7 @@ #include #include -#if defined(_WIN32) || defined(_WIN64) +#ifdef _WIN32 #include #include #include @@ -82,18 +82,120 @@ void sys_fatal(const char *fmt, ...) { sys_fatal_impl(msg); } -#if defined(_WIN32) || defined(_WIN64) +#ifdef _WIN32 -static BOOL sys_windows_short_path(LPSTR destPath, SIZE_T destSize, LPWSTR wideLongPath) +static bool sys_windows_pathname_is_portable(const wchar_t *name, size_t size) { - WCHAR wideShortPath[SYS_MAX_PATH]; + for (size_t i = 0; i < size; i++) { + wchar_t c = name[i]; + + // character outside the ASCII printable range + if ((c < L' ') || (c > L'~')) { return false; } + + // characters unallowed in filenames + switch (c) { + // skipping ':', as it will appear with the drive specifier + case L'<': case L'>': case L'/': case L'\\': + case L'"': case L'|': case L'?': case L'*': + return false; + } + } + return true; +} + +static wchar_t *sys_windows_pathname_get_delim(const wchar_t *name) +{ + const wchar_t *sep1 = wcschr(name, L'/'); + const wchar_t *sep2 = wcschr(name, L'\\'); + + if (NULL == sep1) { return (wchar_t*)sep2; } + if (NULL == sep2) { return (wchar_t*)sep1; } + + return (sep1 < sep2) ? (wchar_t*)sep1 : (wchar_t*)sep2; +} + +bool sys_windows_short_path_from_wcs(char *destPath, size_t destSize, const wchar_t *wcsLongPath) +{ + wchar_t wcsShortPath[SYS_MAX_PATH]; // converted with WinAPI + wchar_t wcsPortablePath[SYS_MAX_PATH]; // non-unicode parts replaced back with long forms // Convert the Long Path in Wide Format to the alternate short form. - // It will still point to already existing directory. - if (0 == GetShortPathNameW(wideLongPath, wideShortPath, SYS_MAX_PATH)) { return FALSE; } + // It will still point to already existing directory or file. + if (0 == GetShortPathNameW(wcsLongPath, wcsShortPath, SYS_MAX_PATH)) { return FALSE; } + + // Scanning the paths side-by-side, to keep the portable (ASCII) + // parts of the absolute path unchanged (in the long form) + wcsPortablePath[0] = L'\0'; + const wchar_t *longPart = wcsLongPath; + wchar_t *shortPart = wcsShortPath; + + while (true) { + int longLength; + int shortLength; + const wchar_t *sourcePart; + int sourceLength; + int bufferLength; + + const wchar_t *longDelim = sys_windows_pathname_get_delim(longPart); + wchar_t *shortDelim = sys_windows_pathname_get_delim(shortPart); + + if (NULL == longDelim) { + longLength = wcslen(longPart); // final part of the scanned path + } else { + longLength = longDelim - longPart; // ptr diff measured in WCHARs + } + + if (NULL == shortDelim) { + shortLength = wcslen(shortPart); // final part of the scanned path + } else { + shortLength = shortDelim - shortPart; // ptr diff measured in WCHARs + } + + if (sys_windows_pathname_is_portable(longPart, longLength)) { + // take the original name (subdir or filename) + sourcePart = longPart; + sourceLength = longLength; + } else { + // take the converted alternate (short) name + sourcePart = shortPart; + sourceLength = shortLength; + } + + // take into account the slash-or-backslash separator + if (L'\0' != sourcePart[sourceLength]) { sourceLength++; } + + // how many WCHARs are still left in the buffer + bufferLength = (SYS_MAX_PATH - 1) - wcslen(wcsPortablePath); + if (sourceLength > bufferLength) { return false; } + + wcsncat(wcsPortablePath, sourcePart, sourceLength); + + // path end reached? + if ((NULL == longDelim) || (NULL == shortDelim)) { break; } + + // compare the next name + longPart = longDelim + 1; + shortPart = shortDelim + 1; + } // Short Path can be safely represented by the US-ASCII Charset. - return (WideCharToMultiByte(CP_ACP, 0, wideShortPath, (-1), destPath, destSize, NULL, NULL) > 0); + return (WideCharToMultiByte(CP_ACP, 0, wcsPortablePath, (-1), destPath, destSize, NULL, NULL) > 0); +} + +bool sys_windows_short_path_from_mbs(char *destPath, size_t destSize, const char *mbsLongPath) +{ + // Converting the absolute path in UTF-8 format (MultiByte String) + // to an alternate (portable) format usable on Windows. + // Assuming the given paths points to an already existing file or folder. + + wchar_t wcsWidePath[SYS_MAX_PATH]; + + if (MultiByteToWideChar(CP_UTF8, 0, mbsLongPath, (-1), wcsWidePath, SYS_MAX_PATH) > 0) + { + return sys_windows_short_path_from_wcs(destPath, destSize, wcsWidePath); + } + + return false; } const char *sys_user_path(void) @@ -142,7 +244,7 @@ const char *sys_user_path(void) if (ERROR_ALREADY_EXISTS != GetLastError()) { return NULL; } } - return sys_windows_short_path(shortPath, SYS_MAX_PATH, widePath) ? shortPath : NULL; + return sys_windows_short_path_from_wcs(shortPath, SYS_MAX_PATH, widePath) ? shortPath : NULL; } const char *sys_exe_path(void) @@ -157,7 +259,7 @@ const char *sys_exe_path(void) if (NULL != lastBackslash) { *lastBackslash = L'\0'; } else { return NULL; } - return sys_windows_short_path(shortPath, SYS_MAX_PATH, widePath) ? shortPath : NULL; + return sys_windows_short_path_from_wcs(shortPath, SYS_MAX_PATH, widePath) ? shortPath : NULL; } static void sys_fatal_impl(const char *msg) { diff --git a/src/pc/platform.h b/src/pc/platform.h index 0c49dbea..bde57d21 100644 --- a/src/pc/platform.h +++ b/src/pc/platform.h @@ -15,6 +15,10 @@ char *sys_strlwr(char *src); int sys_strcasecmp(const char *s1, const char *s2); // path stuff +#ifdef _WIN32 +bool sys_windows_short_path_from_wcs(char *destPath, size_t destSize, const wchar_t *wcsLongPath); +bool sys_windows_short_path_from_mbs(char* destPath, size_t destSize, const char *mbsLongPath); +#endif const char *sys_user_path(void); const char *sys_exe_path(void); const char *sys_file_extension(const char *fpath);