diff --git a/thcrap/src/global.h b/thcrap/src/global.h index 60fd42d8..158c59b5 100644 --- a/thcrap/src/global.h +++ b/thcrap/src/global.h @@ -42,6 +42,8 @@ void TH_CDECL thcrap_free(void *mem); // Release builds. #ifdef _DEBUG # define DEBUG_OR_RELEASE "_d" +# define DEBUG_OR_RELEASE_W L"_d" #else # define DEBUG_OR_RELEASE +# define DEBUG_OR_RELEASE_W #endif diff --git a/thcrap/src/shelllink.cpp b/thcrap/src/shelllink.cpp index 49cc8dd0..1f368e3f 100644 --- a/thcrap/src/shelllink.cpp +++ b/thcrap/src/shelllink.cpp @@ -15,21 +15,12 @@ #include #include -typedef enum { - LINK_FN = 3, // Slots 1 and 2 are used by thcrap_configure, that needs them for run_cfg_fn - LINK_ARGS, -} shelllink_slot_t; - -#define LINK_MACRO_EXPAND(macro) \ - macro(link_fn); \ - macro(target_cmd); \ - macro(target_args); \ - macro(work_path); \ - macro(icon_fn) - HRESULT CreateLink( - const char *link_fn, const char *target_cmd, const char *target_args, - const char *work_path, const char *icon_fn + const std::filesystem::path& link_fn, + const std::filesystem::path& target_cmd, + const std::wstring& target_args, + const std::filesystem::path& work_path, + const std::filesystem::path& icon_fn ) { HRESULT hres; @@ -41,14 +32,11 @@ HRESULT CreateLink( if (SUCCEEDED(hres)) { IPersistFile* ppf; - LINK_MACRO_EXPAND(WCHAR_T_DEC); - LINK_MACRO_EXPAND(WCHAR_T_CONV); - // Set the path to the shortcut target and add the description. - psl->SetPath(target_cmd_w); - psl->SetArguments(target_args_w); - psl->SetWorkingDirectory(work_path_w); - psl->SetIconLocation(icon_fn_w, 0); + psl->SetPath(target_cmd.wstring().c_str()); + psl->SetArguments(target_args.c_str()); + psl->SetWorkingDirectory(work_path.wstring().c_str()); + psl->SetIconLocation(icon_fn.wstring().c_str(), 0); // Query IShellLink for the IPersistFile interface, used for saving the // shortcut in persistent storage. @@ -56,19 +44,143 @@ HRESULT CreateLink( if (SUCCEEDED(hres)) { // Save the link by calling IPersistFile::Save. - hres = ppf->Save(link_fn_w, FALSE); + hres = ppf->Save(link_fn.wstring().c_str(), FALSE); if (FAILED(hres)) { hres = GetLastError(); } ppf->Release(); } psl->Release(); - LINK_MACRO_EXPAND(WCHAR_T_FREE); } return hres; } -std::string get_link_dir(ShortcutsDestination destination, const char *self_path) +void ReplaceStringTable(HANDLE hUpdate, std::vector strings) +{ + if (strings.size() > 16) { + throw std::runtime_error("ReplaceStringTable supports at most 16 strings"); + } + + std::vector newStringTable; + for (auto& str : strings) { + size_t pos = newStringTable.size(); + newStringTable.resize(newStringTable.size() + 1 + str.length()); + newStringTable[pos] = static_cast(str.length()); + std::copy(str.begin(), str.end(), newStringTable.begin() + pos + 1); + } + + for (size_t i = strings.size(); i < 16; i++) { + newStringTable.push_back(0); + } + + UpdateResourceW(hUpdate, RT_STRING, MAKEINTRESOURCE(1), + MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL), newStringTable.data(), newStringTable.size() * sizeof(wchar_t)); +} + +std::pair GetResource(HMODULE hMod, LPCWSTR resourceId, LPCWSTR resourceType) +{ + HRSRC hRes = FindResourceW(hMod, resourceId, resourceType); + if (hRes == nullptr) { + return {}; + } + + HGLOBAL hResLoad = LoadResource(hMod, hRes); + if (hResLoad == nullptr) { + return {}; + } + + LPVOID lpResLock = LockResource(hResLoad); + if (lpResLock == nullptr) { + return {}; + } + + DWORD size = SizeofResource(hMod, hRes); + if (size == 0) { + return {}; + } + + return std::make_pair(lpResLock, size); +} + +// This function doesn't work. TODO: fix +void ReplaceIcon(HANDLE hUpdate, const std::filesystem::path& icon_path) +{ + HMODULE hIconExe = LoadLibraryExW(icon_path.wstring().c_str(), nullptr, LOAD_LIBRARY_AS_DATAFILE); + if (hIconExe == nullptr) { + return; + } + + auto& [iconData, iconSize ] = GetResource(hIconExe, MAKEINTRESOURCE(1), RT_ICON); + auto& [iconGroupData, iconGroupSize] = GetResource(hIconExe, MAKEINTRESOURCE(1), RT_GROUP_ICON); + if (!iconData || !iconGroupData) { + FreeLibrary(hIconExe); + return; + } + + UpdateResourceW(hUpdate, RT_ICON, MAKEINTRESOURCE(1), + MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL), iconData, iconSize); + UpdateResourceW(hUpdate, RT_GROUP_ICON, MAKEINTRESOURCE(1), + MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL), iconGroupData, iconGroupSize); + + FreeLibrary(hIconExe); +} + +bool CreateWrapper( + const std::filesystem::path& link_path, + const std::filesystem::path& thcrap_dir, + const std::wstring& loader_exe, + const std::wstring& target_args, + const std::filesystem::path& icon_path, + ShortcutsType shortcut_type +) +{ + auto wrapper_path = thcrap_dir / loader_exe; + if (!CopyFileW(wrapper_path.wstring().c_str(), link_path.wstring().c_str(), FALSE)) { + log_printf("Could not copy %s to %s (%u)\n", wrapper_path.string().c_str(), + link_path.string().c_str(), GetLastError()); + return false; + } + + HANDLE hUpdate = BeginUpdateResourceW(link_path.wstring().c_str(), FALSE); + if (!hUpdate) { + DeleteFileW(link_path.wstring().c_str()); + return false; + } + + auto target_dir = thcrap_dir / "bin"; + if (shortcut_type == SHTYPE_WRAPPER_RELPATH) { + auto link_dir = link_path; + link_dir.remove_filename(); + target_dir = target_dir.lexically_proximate(link_dir); + } + ReplaceStringTable(hUpdate, { + target_dir.wstring(), + target_args, + loader_exe + }); + ReplaceIcon(hUpdate, icon_path); + + EndUpdateResourceW(hUpdate, FALSE /* Don't discard changes */); + return true; +} + +std::filesystem::path GetThcrapDir() +{ + size_t self_fn_len = GetModuleFileNameU(NULL, NULL, 0) + 1; + VLA(char, self_fn, self_fn_len); + + GetModuleFileNameU(NULL, self_fn, self_fn_len); + + auto thcrap_dir = std::filesystem::u8path(self_fn); + thcrap_dir.remove_filename(); + thcrap_dir /= ".."; + thcrap_dir = thcrap_dir.lexically_normal(); + + VLA_FREE(self_fn); + return thcrap_dir; +} + +std::filesystem::path get_link_dir(ShortcutsDestination destination, const std::filesystem::path& self_path) { switch (destination) { @@ -80,16 +192,16 @@ std::string get_link_dir(ShortcutsDestination destination, const char *self_path return self_path; case SHDESTINATION_DESKTOP: { - char szPath[MAX_PATH]; - if (SHGetFolderPathU(NULL, CSIDL_DESKTOPDIRECTORY, NULL, SHGFP_TYPE_CURRENT, szPath) != S_OK) { + wchar_t szPath[MAX_PATH]; + if (SHGetFolderPathW(NULL, CSIDL_DESKTOPDIRECTORY, NULL, SHGFP_TYPE_CURRENT, szPath) != S_OK) { return ""; } return szPath; } case SHDESTINATION_START_MENU: { - char szPath[MAX_PATH]; - if (SHGetFolderPathU(NULL, CSIDL_PROGRAMS, NULL, SHGFP_TYPE_CURRENT, szPath) != S_OK) { + wchar_t szPath[MAX_PATH]; + if (SHGetFolderPathW(NULL, CSIDL_PROGRAMS, NULL, SHGFP_TYPE_CURRENT, szPath) != S_OK) { return ""; } @@ -98,7 +210,7 @@ std::string get_link_dir(ShortcutsDestination destination, const char *self_path if (!std::filesystem::is_directory(path)) { std::filesystem::create_directory(path); } - return path.generic_u8string(); + return path; } case SHDESTINATION_GAMES_DIRECTORY: @@ -107,77 +219,90 @@ std::string get_link_dir(ShortcutsDestination destination, const char *self_path } } -std::string GetIconPath(const char *icon_path_, const char *game_id) +std::filesystem::path GetIconPath(const char *icon_path_, const char *game_id) { auto icon_path = std::filesystem::u8path(icon_path_); if (icon_path.filename() != "vpatch.exe") - return icon_path_; + return icon_path; - icon_path.replace_filename(std::string(game_id) + ".exe"); - if (std::filesystem::is_regular_file(icon_path)) - return icon_path.u8string(); + auto new_path = icon_path; + new_path.replace_filename(std::string(game_id) + ".exe"); + if (std::filesystem::is_regular_file(new_path)) + return new_path; // Special case - EoSD if (strcmp(game_id, "th06") == 0) { - icon_path.replace_filename(L"東方紅魔郷.exe"); - if (std::filesystem::is_regular_file(icon_path)) - return icon_path.u8string(); + new_path = icon_path; + new_path.replace_filename(L"東方紅魔郷.exe"); + if (std::filesystem::is_regular_file(new_path)) + return new_path; } - return icon_path_; + return icon_path; } -int CreateShortcuts(const char *run_cfg_fn, games_js_entry *games, ShortcutsDestination destination) +int CreateShortcuts(const char *run_cfg_fn, games_js_entry *games, ShortcutsDestination destination, ShortcutsType shortcut_type) { - constexpr stringref_t loader_exe = "thcrap_loader" DEBUG_OR_RELEASE ".exe"; + LPCWSTR loader_exe = L"thcrap_loader" DEBUG_OR_RELEASE_W L".exe"; + auto thcrap_dir = GetThcrapDir(); + auto self_path = thcrap_dir / L"bin" / loader_exe; + auto link_dir = get_link_dir(destination, thcrap_dir); int ret = 0; - size_t self_fn_len = GetModuleFileNameU(NULL, NULL, 0) + 1; - VLA(char, self_fn, self_fn_len); - GetModuleFileNameU(NULL, self_fn, self_fn_len); - PathRemoveFileSpec(self_fn); - PathAppendU(self_fn, ".."); - PathAddBackslashU(self_fn); + if (shortcut_type != SHTYPE_SHORTCUT && + shortcut_type != SHTYPE_WRAPPER_ABSPATH && + shortcut_type != SHTYPE_WRAPPER_RELPATH) { + log_print("Error creating shortcuts: invalid parameter for shortcut_type. Please report this error to the developpers.\n"); + return 1; + } // Yay, COM. - HRESULT com_init_succeded = CoInitializeEx(NULL, COINIT_MULTITHREADED); - { - VLA(char, self_path, self_fn_len + loader_exe.length()); - strcpy(self_path, self_fn); + HRESULT com_init_succeded = E_FAIL; + if (shortcut_type == SHTYPE_SHORTCUT) { + com_init_succeded = CoInitializeEx(NULL, COINIT_MULTITHREADED); + } - strcat(self_fn, "bin\\"); - strcat(self_fn, loader_exe.data()); + log_printf("Creating shortcuts"); - std::string link_dir = get_link_dir(destination, self_path); + for (size_t i = 0; games[i].id; i++) { + log_printf("."); - log_printf("Creating shortcuts"); + if (destination == SHDESTINATION_GAMES_DIRECTORY) { + link_dir = std::filesystem::absolute(games[i].path).remove_filename(); + } + + auto icon_path = GetIconPath(games[i].path, games[i].id); + auto link_path = link_dir / (std::string(games[i].id) + " (" + run_cfg_fn + ").ext"); + std::string link_args = std::string("\"") + run_cfg_fn + ".js\" " + games[i].id; + auto link_args_w = std::make_unique(link_args.length() + 1); + StringToUTF16(link_args_w.get(), link_args.c_str(), -1); - for (size_t i = 0; games[i].id; i++) { - if (destination == SHDESTINATION_GAMES_DIRECTORY) { - link_dir = std::filesystem::u8path(games[i].path).remove_filename().generic_u8string(); + if (shortcut_type == SHTYPE_SHORTCUT) { + link_path.replace_extension("lnk"); + if (CreateLink(link_path, self_path, link_args_w.get(), thcrap_dir, icon_path)) { + ret = 1; } - const char *link_fn = strings_sprintf(LINK_FN, "%s\\%s (%s).lnk", link_dir.c_str(), games[i].id, run_cfg_fn); - const char *link_args = strings_sprintf(LINK_ARGS, "\"%s.js\" %s", run_cfg_fn, games[i].id); - - log_printf("."); - - std::string icon_path = GetIconPath(games[i].path, games[i].id); - if (CreateLink(link_fn, self_fn, link_args, self_path, icon_path.c_str())) { - log_printf( - "\n" - "Error writing to %s!\n" - "You probably do not have the permission to write to the current directory,\n" - "or the file itself is write-protected.\n", - link_fn - ); + } + else if (shortcut_type == SHTYPE_WRAPPER_ABSPATH || shortcut_type == SHTYPE_WRAPPER_RELPATH) { + link_path.replace_extension("exe"); + auto exe_args = std::wstring(loader_exe) + L" " + link_args_w.get(); + if (!CreateWrapper(link_path, thcrap_dir, loader_exe, exe_args, icon_path, shortcut_type)) { ret = 1; - break; } } - VLA_FREE(self_path); + if (ret != 0) { + log_printf( + "\n" + "Error writing to %s!\n" + "You probably do not have the permission to write to its directory,\n" + "or the file itself is write-protected.\n", + link_path.string().c_str() + ); + break; + } } - VLA_FREE(self_fn); + if (com_init_succeded == S_OK) { CoUninitialize(); } diff --git a/thcrap/src/shelllink.h b/thcrap/src/shelllink.h index 6afa1b92..b253c323 100644 --- a/thcrap/src/shelllink.h +++ b/thcrap/src/shelllink.h @@ -17,6 +17,13 @@ enum ShortcutsDestination SHDESTINATION_GAMES_DIRECTORY = 4, }; +enum ShortcutsType +{ + SHTYPE_SHORTCUT = 1, + SHTYPE_WRAPPER_ABSPATH = 2, + SHTYPE_WRAPPER_RELPATH = 3, +}; + // Create a shortcut with the given parameters. // It is assumed that CoInitialize has already been called. HRESULT CreateLink( @@ -25,4 +32,4 @@ HRESULT CreateLink( ); // Create shortcuts for the given games -int CreateShortcuts(const char *run_cfg_fn, games_js_entry *games, enum ShortcutsDestination destination); +int CreateShortcuts(const char *run_cfg_fn, games_js_entry *games, enum ShortcutsDestination destination, enum ShortcutsType shortcut_type); diff --git a/thcrap_configure/src/configure.cpp b/thcrap_configure/src/configure.cpp index 2e4d72bb..43f15a33 100644 --- a/thcrap_configure/src/configure.cpp +++ b/thcrap_configure/src/configure.cpp @@ -341,7 +341,7 @@ int TH_CDECL win32_utf8_main(int argc, const char *argv[]) games_js_entry *gamesArray = nullptr; if (console_ask_yn(_A("Create shortcuts? (required for first run)")) != 'n') { gamesArray = games_js_to_array(games); - if (CreateShortcuts(run_cfg_fn.c_str(), gamesArray, SHDESTINATION_THCRAP_DIR) != 0) { + if (CreateShortcuts(run_cfg_fn.c_str(), gamesArray, SHDESTINATION_THCRAP_DIR, SHTYPE_SHORTCUT) != 0) { goto end; } } diff --git a/thcrap_configure_v3/Page5.xaml.cs b/thcrap_configure_v3/Page5.xaml.cs index df25c62f..12df5f58 100644 --- a/thcrap_configure_v3/Page5.xaml.cs +++ b/thcrap_configure_v3/Page5.xaml.cs @@ -25,6 +25,9 @@ public partial class Page5 : UserControl { bool? isUTLPresent = null; GlobalConfig config = null; + ThcrapDll.ShortcutsType shortcutsType = ThcrapDll.ShortcutsType.SHTYPE_SHORTCUT; + const long configDestinationMask = 0xFFFF; + const int configTypeOffset = 16; [Flags] public enum ShortcutDestinations @@ -51,12 +54,17 @@ public void Enter() if (config == null) { config = new GlobalConfig(); - ShortcutDestinations dest = config.default_shortcut_destinations; + ShortcutDestinations dest = (ShortcutDestinations)(config.default_shortcut_destinations & configDestinationMask); checkboxDesktop.IsChecked = (dest & ShortcutDestinations.Desktop) != 0; checkboxStartMenu.IsChecked = (dest & ShortcutDestinations.StartMenu) != 0; checkboxGamesFolder.IsChecked = (dest & ShortcutDestinations.GamesFolder) != 0; checkboxThcrapFolder.IsChecked = (dest & ShortcutDestinations.ThcrapFolder) != 0; + shortcutsType = (ThcrapDll.ShortcutsType)(config.default_shortcut_destinations >> configTypeOffset); + if (shortcutsType != ThcrapDll.ShortcutsType.SHTYPE_SHORTCUT && + shortcutsType != ThcrapDll.ShortcutsType.SHTYPE_WRAPPER_ABSPATH && + shortcutsType != ThcrapDll.ShortcutsType.SHTYPE_WRAPPER_RELPATH) + shortcutsType = ThcrapDll.ShortcutsType.SHTYPE_SHORTCUT; } } @@ -93,16 +101,17 @@ private void CreateShortcuts(string configName, IEnumerable games) { - config.default_shortcut_destinations = + ShortcutDestinations shortcutDestinations = (checkboxDesktop.IsChecked == true ? ShortcutDestinations.Desktop : 0) | (checkboxStartMenu.IsChecked == true ? ShortcutDestinations.StartMenu : 0) | (checkboxGamesFolder.IsChecked == true ? ShortcutDestinations.GamesFolder : 0) | (checkboxThcrapFolder.IsChecked == true ? ShortcutDestinations.ThcrapFolder : 0); + config.default_shortcut_destinations = (long)shortcutDestinations | ((long)shortcutsType << configTypeOffset); config.Save(); if (checkboxDesktop.IsChecked == true) diff --git a/thcrap_configure_v3/Runconfig.cs b/thcrap_configure_v3/Runconfig.cs index 9ef879a9..3db13f68 100644 --- a/thcrap_configure_v3/Runconfig.cs +++ b/thcrap_configure_v3/Runconfig.cs @@ -41,7 +41,7 @@ class GlobalConfig public bool console { get; set; } public long exception_detail { get; set; } public long codepage { get; set; } - public Page5.ShortcutDestinations default_shortcut_destinations { get; set; } + public long default_shortcut_destinations { get; set; } public GlobalConfig() { @@ -52,7 +52,7 @@ public GlobalConfig() console = ThcrapDll.globalconfig_get_boolean("console", false); exception_detail = ThcrapDll.globalconfig_get_integer("exception_detail", 1); codepage = ThcrapDll.globalconfig_get_integer("codepage", 932); - default_shortcut_destinations = (Page5.ShortcutDestinations)ThcrapDll.globalconfig_get_integer("default_shortcut_destinations", + default_shortcut_destinations = ThcrapDll.globalconfig_get_integer("default_shortcut_destinations", (long)(Page5.ShortcutDestinations.Desktop | Page5.ShortcutDestinations.StartMenu)); } public void Save() diff --git a/thcrap_configure_v3/ThcrapDll.cs b/thcrap_configure_v3/ThcrapDll.cs index 2262b5ff..422d23c0 100644 --- a/thcrap_configure_v3/ThcrapDll.cs +++ b/thcrap_configure_v3/ThcrapDll.cs @@ -194,6 +194,13 @@ public enum ShortcutsDestination SHDESTINATION_GAMES_DIRECTORY = 4, } + public enum ShortcutsType + { + SHTYPE_SHORTCUT = 1, + SHTYPE_WRAPPER_ABSPATH = 2, + SHTYPE_WRAPPER_RELPATH = 3, + } + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void log_print_cb([MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(ThcrapHelper.UTF8StringMarshaler))] string text); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] @@ -269,7 +276,7 @@ public static extern patch_t patch_init( [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)] public static extern int CreateShortcuts( [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(ThcrapHelper.UTF8StringMarshaler))] string run_cfg_fn, - games_js_entry[] games, ShortcutsDestination destination); + games_js_entry[] games, ShortcutsDestination destination, ShortcutsType type); } class ThcrapUpdateDll diff --git a/thcrap_wrapper/src/thcrap_wrapper.c b/thcrap_wrapper/src/thcrap_wrapper.c index d2b55aeb..c0f0468a 100644 --- a/thcrap_wrapper/src/thcrap_wrapper.c +++ b/thcrap_wrapper/src/thcrap_wrapper.c @@ -65,6 +65,9 @@ int main() if (rcApplicationPath == NULL) { PathAppendW(ApplicationPath, L"bin\\"); } + else if (!PathIsRelative(rcApplicationPath)) { + my_strcpy(ApplicationPath, rcApplicationPath); + } else { PathAppendW(ApplicationPath, rcApplicationPath); }