From f06e884373c7fc98d9945d1e47b0965d5494888b Mon Sep 17 00:00:00 2001 From: aemony Date: Fri, 6 Oct 2023 21:24:46 +0200 Subject: [PATCH] Library: Various restructuring and changes - Cached data is now stored and handled separately for more flexibility - Solved various issues related to incorrectly cached data - Added a Refresh option for official covers - Various asset labels were shortened --- include/tabs/library.h | 58 + src/tabs/library.cpp | 3765 ++++++++++++++++++++-------------------- 2 files changed, 1897 insertions(+), 1926 deletions(-) diff --git a/include/tabs/library.h b/include/tabs/library.h index 9e440639..45f3bbfb 100644 --- a/include/tabs/library.h +++ b/include/tabs/library.h @@ -22,5 +22,63 @@ #pragma once +#include +#include + void SKIF_UI_Tab_DrawLibrary (void); + +// Cached struct used to hold calculated data across frames +struct SKIF_Lib_SummaryCache +{ + struct { + std::string type; + std::string type_version; + struct { + std::string text; + ImColor color; + ImColor color_hover; + } status; + std::string hover_text; + } injection; + + std::string config_repo; + + struct { + std::wstring shorthandW; + std::string shorthand; // Converted to utf-8 from utf-16 + std::wstring root_dirW; + std::string root_dir; // Converted to utf-8 from utf-16 + std::wstring full_pathW; + std::string full_path; // Converted to utf-8 from utf-16 + } config; + + struct { + std::wstring shorthandW; + std::string shorthand; // Converted to utf-8 from utf-16 + std::wstring versionW; + std::string version; // Converted to utf-8 from utf-16 + std::wstring full_pathW; + std::string full_path; // Converted to utf-8 from utf-16 + } dll; + + AppId_t app_id = 0; + DWORD running = 0; + bool service = false; + bool autostop = false; + + void Refresh (app_record_s* pApp); + + // Functions + static SKIF_Lib_SummaryCache& GetInstance (void) + { + static SKIF_Lib_SummaryCache instance; + return instance; + } + + SKIF_Lib_SummaryCache (SKIF_Lib_SummaryCache const&) = delete; // Delete copy constructor + SKIF_Lib_SummaryCache (SKIF_Lib_SummaryCache&&) = delete; // Delete move constructor + +private: + SKIF_Lib_SummaryCache (void) { }; // Do nothing +}; \ No newline at end of file diff --git a/src/tabs/library.cpp b/src/tabs/library.cpp index 9086b78f..e582c0e2 100644 --- a/src/tabs/library.cpp +++ b/src/tabs/library.cpp @@ -65,7 +65,7 @@ const int SKIF_STEAM_APPID = 1157970; bool SKIF_STEAM_OWNER = false; bool loadCover = false; -bool tryingToLoadCover = true; +bool tryingToLoadCover = false; std::atomic gameCoverLoading = false; static bool clickedGameLaunch, @@ -120,11 +120,11 @@ ApplySRGBAlpha (float a) 1.055f * std::pow ( a, 1.0f / 2.4f ) - 0.55f ); } +#pragma region Trie Keyboard Hint Search + // define character size #define CHAR_SIZE 128 -#pragma region Trie Keyboard Hint Search - // A Class representing a Trie node class Trie { @@ -265,2120 +265,2232 @@ Trie labels; #pragma endregion +#pragma region SKIF_Lib_SummaryCache void -SKIF_UI_Tab_DrawLibrary (void) +SKIF_Lib_SummaryCache::Refresh (app_record_s* pApp) { -#if 0 - SKIF_GamesCollection& _games = SKIF_GamesCollection::GetInstance(); + static SKIF_InjectionContext& _inject = SKIF_InjectionContext::GetInstance ( ); - // Always read from the last written index - int nowReading = _games.snapshot_idx_written.load ( ); - _games.snapshot_idx_reading.store (nowReading); + app_id = pApp->id; + running = pApp->_status.running; + autostop = _inject.bAckInj; - if (RepopulateGames) - _games.RefreshGames ( ); + service = (pApp->specialk.injection.injection.bitness == InjectionBitness::ThirtyTwo && _inject.pid32) || + (pApp->specialk.injection.injection.bitness == InjectionBitness::SixtyFour && _inject.pid64) || + (pApp->specialk.injection.injection.bitness == InjectionBitness::Unknown && (_inject.pid32 && + _inject.pid64)); - /* - std::vector > &apps_new = - _games.GetGames ( ); - */ + sk_install_state_s& sk_install = + pApp->specialk.injection; - std::vector >* apps_new = _games.GetGames ( ); + wchar_t wszDLLPath [MAX_PATH]; + wcsncpy_s ( wszDLLPath, MAX_PATH, + sk_install.injection.dll_path.c_str (), + _TRUNCATE ); - if (apps_new != nullptr && ! apps_new->empty() && RepopulateGames) - { - PLOG_VERBOSE << "New library backend discovered the following games:"; - for (auto const& app : *apps_new) { - PLOG_VERBOSE << app->names.normal; - //OutputDebugString(SK_UTF8ToWideChar(app->names.normal).c_str()); - //OutputDebugString(L"\n"); - } - } -#endif + dll.full_pathW = wszDLLPath; + dll.full_path = SK_WideCharToUTF8 (dll.full_pathW); - /* - if (! sshot_file.empty ()) - { - SKIF_GameManagement_ShowScreenshot (sshot_file); - } - */ + PathStripPathW (wszDLLPath); + dll.shorthandW = wszDLLPath; + dll.shorthand = SK_WideCharToUTF8 (dll.shorthandW); + dll.versionW = sk_install.injection.dll_ver; + dll.version = SK_WideCharToUTF8 (dll.versionW); - static SKIF_CommonPathsCache& _path_cache = SKIF_CommonPathsCache::GetInstance ( ); - static SKIF_RegistrySettings& _registry = SKIF_RegistrySettings::GetInstance ( ); - static SKIF_InjectionContext& _inject = SKIF_InjectionContext::GetInstance ( ); + wchar_t wszConfigPath [MAX_PATH]; + wcsncpy_s ( wszConfigPath, MAX_PATH, + sk_install.config.file.c_str (), + _TRUNCATE ); - static SKIF_DirectoryWatch SKIF_Epic_ManifestWatch; - - //static CComPtr pTex2D; - static CComPtr pTexSRV; - //static ImVec2 vecTex2D; + config.root_dirW = sk_install.config.dir; + config.root_dir = SK_WideCharToUTF8 (config.root_dirW); + config.full_pathW = wszConfigPath; + config.full_path = SK_WideCharToUTF8 (config.full_pathW); + PathStripPathW (wszConfigPath); + config.shorthandW = wszConfigPath; + config.shorthand = SK_WideCharToUTF8 (config.shorthandW); - static ImVec2 vecCoverUv0 = ImVec2 (0, 0), - vecCoverUv1 = ImVec2 (1, 1); - - static DirectX::TexMetadata meta = { }; - static DirectX::ScratchImage img = { }; - - static - std::wstring appinfo_path ( - SK_GetSteamDir () - ); + //if (! PathFileExistsW (sk_install.config.file.c_str ())) + // config.shorthand.clear (); - SK_RunOnce ( - appinfo_path.append ( - LR"(\appcache\appinfo.vdf)" - ) - ); + //if (! PathFileExistsA (dll.full_path.c_str ())) + // dll.shorthand.clear (); - SK_RunOnce ( - appinfo = - std::make_unique ( - appinfo_path - ) - ); + injection.type = "None"; + injection.status.text.clear (); + injection.hover_text.clear (); - auto& io = - ImGui::GetIO (); + switch (sk_install.injection.type) + { + case sk_install_state_s::Injection::Type::Local: + injection.type = "Local"; + injection.type_version = SK_FormatString (R"(%s v %s (%s))", injection.type.c_str(), dll.version.c_str(), dll.shorthand.c_str()); + break; - //static volatile LONG icon_thread = 1; - //static volatile LONG need_sort = 0; - bool sort_changed = false; + case sk_install_state_s::Injection::Type::Global: + default: // Unknown injection strategy, but let's assume global would work -#if 0 - if (InterlockedCompareExchange (&need_sort, 0, 1)) - { - std::sort ( apps.begin (), - apps.end (), - []( const std::pair & a, - const std::pair & b ) -> int + if ( _inject.bHasServlet ) { - return a.second.names.all_upper_alnum.compare( - b.second.names.all_upper_alnum - ) < 0; + injection.type = "Global"; + injection.type_version = SK_FormatString (R"(%s v %s)", injection.type.c_str(), dll.version.c_str()); + injection.status.text = + (service) ? (_inject.bAckInj) ? "Waiting for game..." : "Running" + : " "; //"Service Status"; + + injection.status.color = + (service) ? ImGui::GetStyleColorVec4(ImGuiCol_SKIF_Success) // HSV (0.3F, 0.99F, 1.F) + : ImGui::GetStyleColorVec4(ImGuiCol_SKIF_Info); // HSV (0.08F, 0.99F, 1.F); + injection.status.color_hover = + (service) ? ImGui::GetStyleColorVec4(ImGuiCol_SKIF_Success) * ImVec4(0.8f, 0.8f, 0.8f, 1.0f) + : ImGui::GetStyleColorVec4(ImGuiCol_SKIF_Info) * ImVec4(0.8f, 0.8f, 0.8f, 1.0f); + injection.hover_text = + (service) ? "Click to stop the service" + : "Click to start the service"; } - ); - - sort_changed = true; + break; } -#endif - auto _SortApps = [&](void) -> void + switch (sk_install.config.type) { - std::sort ( apps.begin (), - apps.end (), - []( const std::pair & a, - const std::pair & b ) -> int - { - return a.second.names.all_upper_alnum.compare( - b.second.names.all_upper_alnum - ) < 0; - } - ); + case ConfigType::Centralized: + config_repo = "Centralized"; break; + case ConfigType::Localized: + config_repo = "Localized"; break; + default: + config_repo = "Unknown"; + config.shorthand.clear (); break; + } +} - sort_changed = true; - PLOG_INFO << "Apps were sorted!"; - }; +#pragma endregion - static bool update = true; - static bool updateInjStrat = false; - struct { - uint32_t appid = SKIF_STEAM_APPID; - std::string store = "Steam"; - SKIF_DirectoryWatch dir_watch; +#pragma region PrintInjectionSummary - void reset() - { - appid = SKIF_STEAM_APPID; - store = "Steam"; - dir_watch.reset(); - } - } static selection; +void +GetInjectionSummary (app_record_s* pApp) +{ + if (pApp == nullptr || pApp->id == SKIF_STEAM_APPID) + return; - struct { - uint32_t appid = 0; - std::string store = ""; + static SKIF_CommonPathsCache& _path_cache = SKIF_CommonPathsCache::GetInstance ( ); + static SKIF_RegistrySettings& _registry = SKIF_RegistrySettings::GetInstance ( ); + static SKIF_InjectionContext& _inject = SKIF_InjectionContext::GetInstance ( ); + static SKIF_Lib_SummaryCache& _cache = SKIF_Lib_SummaryCache::GetInstance ( ); + + +#pragma region _IsLocalDLLFileOutdated - void reset() + auto _IsLocalDLLFileOutdated = [&](void) -> bool + { + bool ret = false; + + if ((pApp->store != "Steam") || + (pApp->store == "Steam" && // Exclude the check for games with known older versions + _cache.app_id != 405900 && // Disgaea PC + _cache.app_id != 359870 && // FFX/X-2 HD Remaster + //_cache.app_id != 578330 && // LEGO City Undercover // Do not exclude from the updater as its a part of mainline SK + _cache.app_id != 429660 && // Tales of Berseria + _cache.app_id != 372360 && // Tales of Symphonia + _cache.app_id != 738540 && // Tales of Vesperia DE + _cache.app_id != 351970 // Tales of Zestiria: + )) { - appid = 0; - store = ""; + if (SKIF_Util_CompareVersionStrings (SK_UTF8ToWideChar(_inject.SKVer32), SK_UTF8ToWideChar(_cache.dll.version)) > 0) + { + ret = true; + } } - } static lastCover; + + return ret; + }; - static bool populated = false; +#pragma endregion - if (! ImGui::IsAnyMouseDown ( ) || ! SKIF_ImGui_IsFocused ( )) +#pragma region _UpdateLocalDLLFile + + auto _UpdateLocalDLLFile = [&](void) -> void { - // Temporarily disabled since this gets triggered on game launch/shutdown as well... - // And generally also breaks SKIF's library view on occasion - //if (SKIF_Steam_isLibrariesSignaled ()) - // RepopulateGames = true; + int iBinaryType = SKIF_Util_GetBinaryType (SK_UTF8ToWideChar (_cache.dll.full_path).c_str()); + if (iBinaryType > 0) + { + wchar_t wszPathToGlobalDLL [MAX_PATH + 2] = { }; + GetModuleFileNameW (nullptr, wszPathToGlobalDLL, MAX_PATH); + PathRemoveFileSpecW ( wszPathToGlobalDLL); + PathAppendW ( wszPathToGlobalDLL, (iBinaryType == 2) ? L"SpecialK64.dll" : L"SpecialK32.dll"); - if (! _registry.bDisableEpicLibrary && SKIF_Epic_ManifestWatch.isSignaled (SKIF_Epic_AppDataPath, true)) - RepopulateGames = true; + if (CopyFile (wszPathToGlobalDLL, SK_UTF8ToWideChar (_cache.dll.full_path).c_str(), FALSE)) + { + PLOG_INFO << "Successfully updated " << SK_UTF8ToWideChar (_cache.dll.full_path) << " from v " << SK_UTF8ToWideChar (_cache.dll.version) << " to v " << SK_UTF8ToWideChar (_inject.SKVer32); + } - if (! _registry.bDisableXboxLibrary && SKIF_Xbox_hasInstalledGamesChanged ( )) - RepopulateGames = true; - - if (! _registry.bDisableGOGLibrary) - { - if (SKIF_GOG_hasInstalledGamesChanged ( )) - RepopulateGames = true; + else { + PLOG_ERROR << "Failed to copy " << wszPathToGlobalDLL << " to " << SK_UTF8ToWideChar (_cache.dll.full_path); + PLOG_ERROR << SKIF_Util_GetErrorAsWStr(); + } + } - if (SKIF_GOG_hasGalaxySettingsChanged ( )) - SKIF_GOG_UpdateGalaxyUserID ( ); + else { + PLOG_ERROR << "Failed to retrieve binary type from " << SK_UTF8ToWideChar (_cache.dll.full_path) << " -- returned: " << iBinaryType; + PLOG_ERROR << SKIF_Util_GetErrorAsWStr(); } - } + }; - if (RepopulateGames) - { - RepopulateGames = false; +#pragma endregion - // Clear cached lists - apps.clear (); + if (_cache.app_id != pApp->id || + _cache.service != _inject.bCurrentState || + _cache.running != pApp->_status.running || + _cache.autostop != _inject.bAckInj ) + { + _cache.Refresh (pApp); + } - // Reset selection to Special K, but only if set to something else than -1 - if (selection.appid != 0) - selection.reset(); + static constexpr float + num_lines = 4.0f; + auto line_ht = + ImGui::GetTextLineHeightWithSpacing (); - update = true; + auto frame_id = + ImGui::GetID ("###Injection_Summary_Frame"); - populated = false; - } + SKIF_ImGui_BeginChildFrame ( frame_id, + ImVec2 ( _WIDTH - ImGui::GetStyle ().FrameBorderSize * 2.0f, + num_lines * line_ht ), + ImGuiWindowFlags_NavFlattened | + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoBackground + ); - if (! populated) + ImGui::BeginGroup (); + + // Column 1 + ImGui::BeginGroup (); + ImGui::PushStyleColor (ImGuiCol_Text, ImVec4 (0.5f, 0.5f, 0.5f, 1.f)); + //ImGui::NewLine (); + ImGui::TextUnformatted ("Injection:"); + ImGui::TextUnformatted ("Config Root:"); + ImGui::TextUnformatted ("Config File:"); + ImGui::TextUnformatted ("Platform:"); + ImGui::PopStyleColor (); + ImGui::ItemSize (ImVec2 (110.f * SKIF_ImGui_GlobalDPIScale, + 0.f) + ); // Column should have min-width 130px (scaled with the DPI) + ImGui::EndGroup (); + + ImGui::SameLine (); + + // Column 2 + ImGui::BeginGroup (); + + // Injection + if (!_cache.dll.shorthand.empty ()) { - //InterlockedExchange (&icon_thread, 1); - - PLOG_INFO << "Populating library list..."; + //ImGui::TextUnformatted (cache.dll.shorthand.c_str ()); + ImGuiSelectableFlags flags = ImGuiSelectableFlags_AllowItemOverlap; - apps = SKIF_Steam_GetInstalledAppIDs (); + if (_cache.injection.type._Equal("Global")) + flags |= ImGuiSelectableFlags_Disabled; - for (auto& app : apps) - if (app.second.id == SKIF_STEAM_APPID) - SKIF_STEAM_OWNER = true; + bool openLocalMenu = false; - if ( ! SKIF_STEAM_OWNER ) + ImGui::PushStyleColor(ImGuiCol_TextDisabled, ImGui::GetStyleColorVec4(ImGuiCol_SKIF_TextCaption)); + if (ImGui::Selectable (_cache.injection.type_version.c_str(), false, flags)) { - app_record_s SKIF_record (SKIF_STEAM_APPID); - - SKIF_record.id = SKIF_STEAM_APPID; - SKIF_record.names.normal = "Special K"; - SKIF_record.names.all_upper = "SPECIAL K"; - SKIF_record.install_dir = _path_cache.specialk_install; - SKIF_record.store = "Steam"; - SKIF_record.ImGuiLabelAndID = SK_FormatString("%s###%s%i", SKIF_record.names.normal.c_str(), SKIF_record.store.c_str(), SKIF_record.id); - SKIF_record.ImGuiPushID = SK_FormatString("%s%i", SKIF_record.store.c_str(), SKIF_record.id); + openLocalMenu = true; + } + ImGui::PopStyleColor(); - std::pair - SKIF ( "Special K", SKIF_record ); + if (_cache.injection.type._Equal("Local")) + { + SKIF_ImGui_SetMouseCursorHand ( ); - apps.emplace_back (SKIF); + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) + openLocalMenu = true; } - // Load GOG titles from registry - if (! _registry.bDisableGOGLibrary) - SKIF_GOG_GetInstalledAppIDs (&apps); + SKIF_ImGui_SetHoverText (_cache.dll.full_path.c_str ()); + + if (openLocalMenu && ! ImGui::IsPopupOpen ("LocalDLLMenu")) + ImGui::OpenPopup ("LocalDLLMenu"); - // Load Epic titles from disk - if (! _registry.bDisableEpicLibrary) - SKIF_Epic_GetInstalledAppIDs (&apps); - - if (! _registry.bDisableXboxLibrary) - SKIF_Xbox_GetInstalledAppIDs (&apps); + if (ImGui::BeginPopup ("LocalDLLMenu", ImGuiWindowFlags_NoMove)) + { + if (_IsLocalDLLFileOutdated( )) + { + if (ImGui::Selectable (("Update to v " + _inject.SKVer32).c_str( ))) + { + _UpdateLocalDLLFile ( ); + } - // Load custom SKIF titles from registry - SKIF_GetCustomAppIDs (&apps); + ImGui::Separator ( ); + } - // Set to last selected if it can be found - if (selection.appid == SKIF_STEAM_APPID) - { - for (auto& app : apps) + if (ImGui::Selectable ("Uninstall")) { - if (app.second.id == _registry.iLastSelectedGame && - app.second.store == SK_WideCharToUTF8 (_registry.wsLastSelectedStore)) + + if (DeleteFile (_cache.dll.full_pathW.c_str())) { - selection.appid = app.second.id; - selection.store = app.second.store; - manual_selection.id = selection.appid; - //manual_selection.store = selection.store; - update = true; + PLOG_INFO << "Successfully uninstalled local DLL v " << _cache.dll.version << " from " << _cache.dll.full_path; } } + + ImGui::EndPopup ( ); } + } - PLOG_INFO << "Loading game names synchronously..."; + else + ImGui::TextUnformatted ("N/A"); - // Clear any existing trie - labels = Trie { }; + // Config Root + // Config File + if (!_cache.config.shorthand.empty ()) + { + // Config Root + if (ImGui::Selectable (_cache.config_repo.c_str ())) + { + std::wstring wsRootDir = _cache.config.root_dirW; - // Handle names first - for ( auto& app : apps ) + std::error_code ec; + // Create any missing directories + if (! std::filesystem::exists ( wsRootDir, ec)) + std::filesystem::create_directories (wsRootDir, ec); + + SKIF_Util_ExplorePath (wsRootDir); + } + SKIF_ImGui_SetMouseCursorHand (); + SKIF_ImGui_SetHoverText (_cache.config.root_dir.c_str ()); + + // Config File + if (ImGui::Selectable (_cache.config.shorthand.c_str ())) { - //PLOG_DEBUG << "Working on " << app.second.id << " (" << app.second.store << ")"; + std::wstring wsRootDir = _cache.config.root_dirW; + + std::error_code ec; + // Create any missing directories + if (! std::filesystem::exists ( wsRootDir, ec)) + std::filesystem::create_directories (wsRootDir, ec); + + HANDLE h = CreateFile ( _cache.config.full_pathW.c_str(), + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, + CREATE_NEW, + FILE_ATTRIBUTE_NORMAL, + NULL ); + + // We need to close the handle as well, as otherwise Notepad will think the file + // is still in use (trigger Save As dialog on Save) until SKIF gets closed + if (h != INVALID_HANDLE_VALUE) + CloseHandle (h); + + SKIF_Util_OpenURI (_cache.config.full_pathW.c_str(), SW_SHOWNORMAL, NULL); + } + SKIF_ImGui_SetMouseCursorHand (); + SKIF_ImGui_SetHoverText (_cache.config.full_path.c_str ()); - // Special handling for non-Steam owners of Special K / SKIF - if ( app.second.id == SKIF_STEAM_APPID ) - app.first = "Special K"; - // Regular handling for the remaining Steam games - else if (app.second.store == "Steam") { - app.first.clear (); + if ( ! ImGui::IsPopupOpen ("ConfigFileMenu") && + ImGui::IsItemClicked (ImGuiMouseButton_Right)) + ImGui::OpenPopup ("ConfigFileMenu"); - app.second._status.refresh (&app.second); - } + if (ImGui::BeginPopup ("ConfigFileMenu", ImGuiWindowFlags_NoMove)) + { + ImGui::TextColored ( + ImColor::HSV (0.11F, 1.F, 1.F), + "Troubleshooting:" + ); - // Only bother opening the application manifest - // and looking for a name if the client claims - // the app is installed. - if (app.second._status.installed) + ImGui::Separator ( ); + + struct Preset { - if (! app.second.names.normal.empty ()) - { - app.first = app.second.names.normal; - } + std::string Name; + std::wstring Path; - // Some games have an install state but no name, - // for those we have to consult the app manifest - else if (app.second.store == "Steam") + Preset (std::wstring n, std::wstring p) { - app.first = - SK_UseManifestToGetAppName ( - app.second.id ); - } + Name = SK_WideCharToUTF8 (n); + Path = p; + }; + }; - // Corrupted app manifest / not known to Steam client; SKIP! - if (app.first.empty ()) - { - PLOG_DEBUG << "App ID " << app.second.id << " (" << app.second.store << ") has no name; ignoring!"; + // Static stuff :D + static SKIF_DirectoryWatch SKIF_GlobalWatch; + static SKIF_DirectoryWatch SKIF_CustomWatch; + static std::vector DefaultPresets; + static std::vector CustomPresets; + static bool runOnceDefaultPresets = true; + static bool runOnceCustomPresets = true; - app.second.id = 0; - continue; - } + // Directory watches -- updates the vectors automatically + if (SKIF_GlobalWatch.isSignaled (LR"(Global)", false) || runOnceDefaultPresets) + { + runOnceDefaultPresets = false; - std::string original_name = app.first; - - // Some games use weird Unicode character combos that ImGui can't handle, - // so let's replace those with the normal ones. - - // Replace RIGHT SINGLE QUOTATION MARK (Code: 2019 | UTF-8: E2 80 99) - // with a APOSTROPHE (Code: 0027 | UTF-8: 27) - app.first = std::regex_replace(app.first, std::regex("\xE2\x80\x99"), "\x27"); - - // Replace LATIN SMALL LETTER O (Code: 006F | UTF-8: 6F) and COMBINING DIAERESIS (Code: 0308 | UTF-8: CC 88) - // with a LATIN SMALL LETTER O WITH DIAERESIS (Code: 00F6 | UTF-8: C3 B6) - app.first = std::regex_replace(app.first, std::regex("\x6F\xCC\x88"), "\xC3\xB6"); - - // Strip game names from special symbols (disabled due to breaking some Chinese characters) - //const char* chars = (const char *)u8"\u00A9\u00AE\u2122"; // Copyright (c), Registered (R), Trademark (TM) - //for (unsigned int i = 0; i < strlen(chars); ++i) - //app.first.erase(std::remove(app.first.begin(), app.first.end(), chars[i]), app.first.end()); - - // Remove COPYRIGHT SIGN (Code: 00A9 | UTF-8: C2 A9) - app.first = std::regex_replace(app.first, std::regex("\xC2\xA9"), ""); + HANDLE hFind = INVALID_HANDLE_VALUE; + WIN32_FIND_DATA ffd; + std::vector tmpPresets; + std::wstring PresetFolder = SK_FormatStringW (LR"(%ws\Global\)", _path_cache.specialk_userdata); - // Remove REGISTERED SIGN (Code: 00AE | UTF-8: C2 AE) - app.first = std::regex_replace(app.first, std::regex("\xC2\xAE"), ""); - - // Remove TRADE MARK SIGN (Code: 2122 | UTF-8: E2 84 A2) - app.first = std::regex_replace(app.first, std::regex("\xE2\x84\xA2"), ""); + hFind = FindFirstFile((PresetFolder + L"default_*.ini").c_str(), &ffd); - if (original_name != app.first) + if (INVALID_HANDLE_VALUE != hFind) { - PLOG_DEBUG << "Game title was changed:"; - PLOG_DEBUG << "Old: " << SK_UTF8ToWideChar(original_name.c_str()) << " (" << original_name << ")"; - PLOG_DEBUG << "New: " << SK_UTF8ToWideChar(app.first.c_str()) << " (" << app.first << ")"; - } - - // Strip any remaining null terminators - app.first.erase(std::find(app.first.begin(), app.first.end(), '\0'), app.first.end()); + do { + Preset newPreset = { PathFindFileName(ffd.cFileName), SK_FormatStringW (LR"(%ws\Global\%ws)", _path_cache.specialk_userdata, ffd.cFileName) }; + tmpPresets.push_back(newPreset); + } while (FindNextFile (hFind, &ffd)); - // Trim leftover spaces - app.first.erase(app.first.begin(), std::find_if(app.first.begin(), app.first.end(), [](unsigned char ch) { return !std::isspace(ch); })); - app.first.erase(std::find_if(app.first.rbegin(), app.first.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), app.first.end()); - - // Update ImGuiLabelAndID and ImGuiPushID - app.second.ImGuiLabelAndID = SK_FormatString("%s###%s%i", app.first.c_str(), app.second.store.c_str(), app.second.id); - app.second.ImGuiPushID = SK_FormatString("%s%i", app.second.store.c_str(), app.second.id); + DefaultPresets = tmpPresets; + FindClose (hFind); + } } - // Check if install folder exists (but not for SKIF) - if (app.second.id != SKIF_STEAM_APPID && app.second.store != "Xbox") + if (SKIF_CustomWatch.isSignaled (LR"(Global\Custom)", false) || runOnceCustomPresets) { - std::wstring install_dir; + runOnceCustomPresets = false; - if (app.second.store == "Steam") - install_dir = SK_UseManifestToGetInstallDir(app.second.id); - else - install_dir = app.second.install_dir; - - if (! PathFileExists(install_dir.c_str())) - { - PLOG_DEBUG << "App ID " << app.second.id << " (" << app.second.store << ") has non-existent install folder; ignoring!"; + HANDLE hFind = INVALID_HANDLE_VALUE; + WIN32_FIND_DATA ffd; + std::vector tmpPresets; + std::wstring PresetFolder = SK_FormatStringW (LR"(%ws\Global\Custom\)", _path_cache.specialk_userdata); - app.second.id = 0; - continue; - } - } + hFind = FindFirstFile((PresetFolder + L"*.ini").c_str(), &ffd); - // Prepare for the keyboard hint / search/filter functionality - if ( app.second._status.installed || app.second.id == SKIF_STEAM_APPID) - { - std::string all_upper = SKIF_Util_ToUpper (app.first), - all_upper_alnum; - - for (const char c : app.first) + if (INVALID_HANDLE_VALUE != hFind) { - if (! ( isalnum (c) || isspace (c) )) - continue; + do { + Preset newPreset = { PathFindFileName(ffd.cFileName), SK_FormatStringW (LR"(%ws\Global\Custom\%ws)", _path_cache.specialk_userdata, ffd.cFileName) }; + tmpPresets.push_back(newPreset); + } while (FindNextFile (hFind, &ffd)); - all_upper_alnum += (char)toupper (c); + CustomPresets = tmpPresets; + FindClose (hFind); } - - size_t stripped = 0; - - if (_registry.bLibraryIgnoreArticles) + } + + if ((! DefaultPresets.empty() || ! CustomPresets.empty())) + { + if (ImGui::BeginMenu("Apply Preset")) { - static const - std::string toSkip [] = + // Default Presets + if (! DefaultPresets.empty()) + { + for (auto& preset : DefaultPresets) { - std::string ("A "), - std::string ("AN "), - std::string ("THE ") - }; + if (ImGui::Selectable (preset.Name.c_str())) + { + CopyFile (preset.Path.c_str(), _cache.config.full_pathW.c_str(), FALSE); + PLOG_VERBOSE << "Copying " << preset.Path << " over to " << _cache.config.full_path << ", overwriting any existing file in the process."; + } + } - for ( auto& skip_ : toSkip ) + if (! CustomPresets.empty()) + ImGui::Separator ( ); + } + + // Custom Presets + if (! CustomPresets.empty()) { - if (all_upper_alnum.find (skip_) == 0) + for (auto& preset : CustomPresets) { - all_upper_alnum = - all_upper_alnum.substr ( - skip_.length () - ); - - stripped = skip_.length (); - break; + if (ImGui::Selectable (preset.Name.c_str())) + { + CopyFile (preset.Path.c_str(), _cache.config.full_pathW.c_str(), FALSE); + PLOG_VERBOSE << "Copying " << preset.Path << " over to " << _cache.config.full_path << ", overwriting any existing file in the process."; + } } } + + ImGui::EndMenu ( ); } - std::string trie_builder; + ImGui::Separator ( ); + } - for ( const char c : all_upper_alnum) + if (ImGui::Selectable ("Apply Compatibility Config")) + { + std::wofstream config_file(_cache.config.full_pathW.c_str()); + + if (config_file.is_open()) { - trie_builder += c; + // Static const as this profile never changes + static const std::wstring out_text = +LR"([SpecialK.System] +ShowEULA=false +GlobalInjectDelay=0.0 - labels.insert (trie_builder); - } - - app.second.names.normal = app.first; - app.second.names.all_upper = all_upper; - app.second.names.all_upper_alnum = all_upper_alnum; - app.second.names.pre_stripped = stripped; - } - } +[API.Hook] +d3d9=true +d3d9ex=true +d3d11=true +OpenGL=true +d3d12=true +Vulkan=true - PLOG_INFO << "Finished loading game names synchronously..."; +[Steam.Log] +Silent=true - _SortApps ( ); +[Input.libScePad] +Enable=false - PLOG_INFO << "Finished populating the library list."; +[Input.XInput] +Enable=false - // We're going to stream game icons asynchronously on this thread - _beginthread ([](void*)->void - { - SKIF_Util_SetThreadDescription (GetCurrentThread (), L"SKIF_LibRefreshWorker"); +[Input.Gamepad] +EnableDirectInput7=false +EnableDirectInput8=false +EnableHID=false +EnableNativePS4=false +EnableRawInput=true +AllowHapticUI=false - CoInitializeEx (nullptr, 0x0); +[Input.Keyboard] +CatchAltF4=false +BypassAltF4Handler=false - PLOG_INFO << "Thread started!"; - - PLOG_INFO << "Loading the embedded Patreon texture..."; - ImVec2 dontCare1, dontCare2; - if (pPatTexSRV.p == nullptr) - LoadLibraryTexture (LibraryTexture::Patreon, SKIF_STEAM_APPID, pPatTexSRV, L"(patreon.png)", dontCare1, dontCare2); - if (pSKLogoTexSRV.p == nullptr) - LoadLibraryTexture (LibraryTexture::Cover, SKIF_STEAM_APPID, pSKLogoTexSRV, L"(sk_boxart.png)", dontCare1, dontCare2); +[Textures.D3D11] +Cache=false)"; - // FIX: This causes the whole apps vector to be resorted by the main thread - // which then causes textures being loaded below to end up being assigned to the wrong game - //InterlockedExchange (&need_sort, 1); + config_file.write(out_text.c_str(), + out_text.length()); - // Force a refresh when the game names have finished being streamed - PostMessage (SKIF_Notify_hWnd, WM_SKIF_ICON, 0x0, 0x0); - - // Load icons last - for ( auto& app : apps ) - { - if (app.second.id == 0) - continue; + config_file.close(); + } + } - std::wstring load_str; - - if (app.second.id == SKIF_STEAM_APPID) // SKIF - load_str = L"_icon.jpg"; - else if (app.second.store == "Other") // SKIF Custom - load_str = L"icon"; - else if (app.second.store == "Epic") // Epic - load_str = L"icon"; - else if (app.second.store == "GOG") // GOG - load_str = app.second.install_dir + L"\\goggame-" + std::to_wstring(app.second.id) + L".ico"; - else if (app.second.store == "Steam") // STEAM - load_str = SK_FormatStringW(LR"(%ws\appcache\librarycache\%i_icon.jpg)", SK_GetSteamDir(), app.second.id); //L"_icon.jpg" + SKIF_ImGui_SetHoverTip ("Known as the \"sledgehammer\" config within the community as it disables\n" + "various features of Special K in an attempt to improve compatibility."); - LoadLibraryTexture ( LibraryTexture::Icon, - app.second.id, - app.second.tex_icon.texture, - load_str, - dontCare1, - dontCare2, - &app.second ); + if (ImGui::Selectable ("Reset")) + { + HANDLE h = CreateFile ( _cache.config.full_pathW.c_str(), + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, + TRUNCATE_EXISTING, + FILE_ATTRIBUTE_NORMAL, + NULL ); + + // We need to close the handle as well, as otherwise Notepad will think the file + // is still in use (trigger Save As dialog on Save) until SKIF gets closed + if (h != INVALID_HANDLE_VALUE) + CloseHandle (h); } - PLOG_INFO << "Finished streaming game icons asynchronously..."; - - //InterlockedExchange (&icon_thread, 0); + ImGui::EndPopup ( ); + } + } - // Force a refresh when the game icons have finished being streamed - PostMessage (SKIF_Notify_hWnd, WM_SKIF_ICON, 0x0, 0x0); + else + { + ImGui::TextUnformatted (_cache.config_repo.c_str ()); + ImGui::TextUnformatted ("N/A"); + } - PLOG_INFO << "Thread stopped!"; - }, 0x0, NULL); + // Platform + ImGui::TextUnformatted (pApp->store.c_str()); - populated = true; - } + // Column should have min-width 100px (scaled with the DPI) + ImGui::ItemSize ( + ImVec2 ( 130.0f * SKIF_ImGui_GlobalDPIScale, + 0.0f + ) ); + ImGui::EndGroup ( ); + ImGui::SameLine ( ); - extern bool coverFadeActive; - static int tmp_iDimCovers = _registry.iDimCovers; - - static - app_record_s* pApp = nullptr; + // Column 3 + ImGui::BeginGroup ( ); - for (auto& app : apps) - if (app.second.id == selection.appid && app.second.store == selection.store) - pApp = &app.second; + static bool quickServiceHover = false; - // Apply changes when the selected game changes - if (update) + // Service quick toogle / Waiting for game... + if (_cache.injection.type._Equal ("Global") && ! _inject.isPending()) { - fTint = (_registry.iDimCovers == 0) ? 1.0f : fTintMin; - - if (pApp != nullptr) + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImColor(0, 0, 0, 0).Value); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImColor(0, 0, 0, 0).Value); + ImGui::PushStyleColor(ImGuiCol_Text, (quickServiceHover) ? _cache.injection.status.color_hover.Value + : _cache.injection.status.color.Value); + + if (ImGui::Selectable (_cache.injection.status.text.c_str(), false, ImGuiSelectableFlags_SpanAllColumns)) { - if ( pApp->install_dir != selection.dir_watch._path) - selection.dir_watch.reset ( ); + _inject._StartStopInject (_cache.service, _registry.bStopOnInjection, pApp->launch_configs[0].isElevated(pApp->id)); - if (! pApp->install_dir.empty()) - selection.dir_watch.isSignaled (pApp->install_dir, true); + _cache.app_id = 0; } + + ImGui::PopStyleColor (3); + + quickServiceHover = ImGui::IsItemHovered (); + + SKIF_ImGui_SetMouseCursorHand (); + SKIF_ImGui_SetHoverTip ( + _cache.injection.hover_text.c_str () + ); + + if ( ! ImGui::IsPopupOpen ("ServiceMenu") && + ImGui::IsItemClicked (ImGuiMouseButton_Right)) + ServiceMenu = PopupState_Open; } - // Apply changes when the _registry.iDimCovers var has been changed in the Settings tab - else if (tmp_iDimCovers != _registry.iDimCovers) + else { + ImGui::NewLine ( ); + } + + + if (_cache.injection.type._Equal ("Local")) { - fTint = (_registry.iDimCovers == 0) ? 1.0f : fTintMin; + if (_IsLocalDLLFileOutdated ( )) + { + ImGui::SameLine ( ); - tmp_iDimCovers = _registry.iDimCovers; + ImGui::PushStyleColor (ImGuiCol_Button, ImVec4 (.1f, .1f, .1f, .5f)); + if (ImGui::SmallButton (ICON_FA_ARROW_UP)) + { + _UpdateLocalDLLFile ( ); + } + ImGui::PopStyleColor ( ); + + SKIF_ImGui_SetHoverTip (("The local DLL file is outdated.\n" + "Click to update it to v " + _inject.SKVer32 + ".")); + } } - ImGui::BeginGroup ( ); + ImGui::EndGroup (); - static int queuePosGameCover = 0; - static char cstrLabelLoading[] = "..."; - static char cstrLabelMissing[] = "Missing cover :("; - static char cstrLabelGOGUser[] = "Please sign in to GOG Galaxy to\n" - "allow the cover to be populated :)"; + // End of columns + ImGui::EndGroup (); - ImVec2 vecPosCoverImage = ImGui::GetCursorPos ( ); - vecPosCoverImage.x -= 1.0f * SKIF_ImGui_GlobalDPIScale; + ImGui::EndChildFrame (); - if (tryingToLoadCover) - { - ImGui::SetCursorPos (ImVec2 ( - vecPosCoverImage.x + 300.0F * SKIF_ImGui_GlobalDPIScale - ImGui::CalcTextSize (cstrLabelLoading).x / 2, - vecPosCoverImage.y + 450.0F * SKIF_ImGui_GlobalDPIScale - ImGui::CalcTextSize (cstrLabelLoading).y / 2)); - ImGui::TextDisabled ( cstrLabelLoading); - } - - else if (textureLoadQueueLength.load() == queuePosGameCover && pTexSRV.p == nullptr) - { - extern std::wstring GOGGalaxy_UserID; - if (pApp != nullptr && pApp->store == "GOG" && GOGGalaxy_UserID.empty()) - { - ImGui::SetCursorPos (ImVec2 ( - vecPosCoverImage.x + 300.0F * SKIF_ImGui_GlobalDPIScale - ImGui::CalcTextSize (cstrLabelGOGUser).x / 2, - vecPosCoverImage.y + 450.0F * SKIF_ImGui_GlobalDPIScale - ImGui::CalcTextSize (cstrLabelGOGUser).y / 2)); - ImGui::TextDisabled ( cstrLabelGOGUser); - } - else { - ImGui::SetCursorPos (ImVec2 ( - vecPosCoverImage.x + 300.0F * SKIF_ImGui_GlobalDPIScale - ImGui::CalcTextSize (cstrLabelMissing).x / 2, - vecPosCoverImage.y + 450.0F * SKIF_ImGui_GlobalDPIScale - ImGui::CalcTextSize (cstrLabelMissing).y / 2)); - ImGui::TextDisabled ( cstrLabelMissing); - } - } + ImGui::Separator (); - ImGui::SetCursorPos (vecPosCoverImage); + auto frame_id2 = + ImGui::GetID ("###Injection_Play_Button_Frame"); -#if 0 - extern bool SKIF_bHDREnabled; + ImGui::PushStyleVar ( + ImGuiStyleVar_FramePadding, + ImVec2 ( 120.0f * SKIF_ImGui_GlobalDPIScale, + 40.0f * SKIF_ImGui_GlobalDPIScale) + ); - float fGammaCorrectedTint = - ((! SKIF_bHDREnabled && _registry.iSDRMode == 2) || - ( SKIF_bHDREnabled && _registry.iHDRMode == 2)) - ? ApplySRGBAlpha (fTint) - : fTint; -#endif - - // Display game cover image - SKIF_ImGui_OptImage (pTexSRV.p, - ImVec2 (600.0F * SKIF_ImGui_GlobalDPIScale, - 900.0F * SKIF_ImGui_GlobalDPIScale), - vecCoverUv0, // Top Left coordinates - vecCoverUv1, // Bottom Right coordinates - ImVec4 (fTint, fTint, fTint, fAlpha), - (_registry.bUIBorders) ? ImGui::GetStyleColorVec4 (ImGuiCol_Border) : ImVec4 (0.0f, 0.0f, 0.0f, 0.0f) // Border + SKIF_ImGui_BeginChildFrame ( + frame_id2, ImVec2 ( 0.0f, + 110.f * SKIF_ImGui_GlobalDPIScale ), + ImGuiWindowFlags_NavFlattened | + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoBackground ); - if (ImGui::IsItemClicked (ImGuiMouseButton_Right)) - ImGui::OpenPopup ("CoverMenu"); + ImGui::PopStyleVar (); - ImGui::SetCursorPos (vecPosCoverImage); + std::string buttonLabel = ICON_FA_GAMEPAD " Launch ";// + pApp->type; + ImGuiButtonFlags buttonFlags = ImGuiButtonFlags_None; - if (selection.appid == SKIF_STEAM_APPID && pSKLogoTexSRV != nullptr) + if (pApp->_status.running) { - ImGui::Image (pSKLogoTexSRV.p, - ImVec2 (600.0F * SKIF_ImGui_GlobalDPIScale, - 900.0F * SKIF_ImGui_GlobalDPIScale), - ImVec2 (0.0f, 0.0f), // Top Left coordinates - ImVec2 (1.0f, 1.0f), // Bottom Right coordinates - ImVec4 (1.0f, 1.0f, 1.0f, 1.0f), // Tint for Special K's logo (always full strength) - ImVec4 (0.0f, 0.0f, 0.0f, 0.0f) // Border - ); + buttonLabel = "Running..."; + buttonFlags = ImGuiButtonFlags_Disabled; + ImGui::PushStyleColor (ImGuiCol_Button, ImGui::GetStyleColorVec4 (ImGuiCol_Button) * ImVec4 (0.75f, 0.75f, 0.75f, 1.0f)); } - // Every >15 ms, increase/decrease the cover fade effect (makes it frame rate independent) - static DWORD timeLastTick; - DWORD timeCurr = SKIF_Util_timeGetTime(); - bool isHovered = ImGui::IsItemHovered(); - bool incTick = false; - extern int startupFadeIn; - - if (startupFadeIn == 0 && pTexSRV.p != nullptr) - startupFadeIn = 1; - - if (startupFadeIn == 1) + // Disable the button for global injection types if the servlets are missing + if ( ! _inject.bHasServlet && !_cache.injection.type._Equal ("Local") ) + SKIF_ImGui_PushDisableState ( ); + + // This captures two events -- launching through context menu + large button + if ( ImGui::ButtonEx ( + buttonLabel.c_str (), + ImVec2 ( 150.0f * SKIF_ImGui_GlobalDPIScale, + 50.0f * SKIF_ImGui_GlobalDPIScale ), buttonFlags ) + || + clickedGameLaunch + || + clickedGameLaunchWoSK ) { - if (fAlpha < 1.0f && pTexSRV.p != nullptr) + + if ( pApp->store != "Steam" && pApp->store != "Epic" && + pApp->launch_configs[0].getExecutableFullPath(pApp->id).find(L"InvalidPath") != std::wstring::npos ) { - if (timeCurr - timeLastTick > 15) + confirmPopupText = "Could not launch game due to missing executable:\n\n" + SK_WideCharToUTF8(pApp->launch_configs[0].getExecutableFullPath(pApp->id, false)); + ConfirmPopup = PopupState_Open; + } + + else { + bool usingSK = (_cache.injection.type._Equal("Local")); + + // Check if Global injection should be used + if (! usingSK) { - fAlpha += 0.05f; - incTick = true; + std::string fullPath = SK_WideCharToUTF8(pApp->launch_configs[0].getExecutableFullPath (pApp->id)); + bool isLocalBlacklisted = pApp->launch_configs[0].isBlacklisted (pApp->id), + isGlobalBlacklisted = _inject._TestUserList (fullPath.c_str (), false); + + usingSK = ! clickedGameLaunchWoSK && + ! isLocalBlacklisted && + ! isGlobalBlacklisted; + + if (usingSK) + { + // Whitelist the path if it haven't been already + if (pApp->store == "Xbox") + { + if (! _inject._TestUserList (SK_WideCharToUTF8 (pApp->Xbox_AppDirectory).c_str(), true)) + { + if (_inject.WhitelistPattern (pApp->Xbox_PackageName)) + _inject.SaveWhitelist ( ); + } + } + + else + { + if (_inject.WhitelistPath (fullPath)) + _inject.SaveWhitelist ( ); + } + + // Disable the first service notification + if (_registry.bMinimizeOnGameLaunch) + _registry._SuppressServiceNotification = true; + } + + // Kickstart service if it is currently not running + if (! _inject.bCurrentState && usingSK ) + _inject._StartStopInject (false, true, pApp->launch_configs[0].isElevated(pApp->id)); + + // Stop the service if the user attempts to launch without SK + else if ( clickedGameLaunchWoSK && _inject.bCurrentState ) + _inject._StartStopInject (true); } - } - if (fAlpha >= 1.0f) - startupFadeIn = 2; - } + // Create the injection acknowledge events in case of a local injection + else { + _inject.SetInjectAckEx (true); + _inject.SetInjectExitAckEx (true); + } - if (_registry.iDimCovers == 2) - { - if (isHovered && fTint < 1.0f) - { - if (timeCurr - timeLastTick > 15) + // Launch game + if (pApp->store == "GOG" && GOGGalaxy_Installed && _registry.bPreferGOGGalaxyLaunch && ! clickedGameLaunch && ! clickedGameLaunchWoSK) { - fTint = fTint + 0.01f; - incTick = true; + extern std::wstring GOGGalaxy_Path; + + // "D:\Games\GOG Galaxy\GalaxyClient.exe" /command=runGame /gameId=1895572517 /path="D:\Games\GOG Games\AI War 2" + + std::wstring launchOptions = SK_FormatStringW(LR"(/command=runGame /gameId=%d /path="%ws")", pApp->id, pApp->install_dir.c_str()); + + SKIF_Util_OpenURI (GOGGalaxy_Path, SW_SHOWDEFAULT, L"OPEN", launchOptions.c_str()); + + /* + SHELLEXECUTEINFOW + sexi = { }; + sexi.cbSize = sizeof (SHELLEXECUTEINFOW); + sexi.lpVerb = L"OPEN"; + sexi.lpFile = GOGGalaxy_Path.c_str(); + sexi.lpParameters = launchOptions.c_str(); + //sexi.lpDirectory = NULL; + sexi.nShow = SW_SHOWDEFAULT; + sexi.fMask = SEE_MASK_FLAG_NO_UI | + SEE_MASK_ASYNCOK | SEE_MASK_NOZONECHECKS; + + ShellExecuteExW (&sexi); + */ } - coverFadeActive = true; - } - else if (! isHovered && fTint > fTintMin) - { - if (timeCurr - timeLastTick > 15) + else if (pApp->store == "Epic") { - fTint = fTint - 0.01f; - incTick = true; + // com.epicgames.launcher://apps/CatalogNamespace%3ACatalogItemId%3AAppName?action=launch&silent=true + SKIF_Util_OpenURI ((L"com.epicgames.launcher://apps/" + pApp->launch_configs[0].launch_options + L"?action=launch&silent=true").c_str()); } - coverFadeActive = true; + else if (pApp->store == "Steam") { + //SKIF_Util_OpenURI_Threaded ((L"steam://run/" + std::to_wstring(pApp->id)).c_str()); // This is seemingly unreliable + SKIF_Util_OpenURI ((L"steam://run/" + std::to_wstring(pApp->id)).c_str()); + pApp->_status.invalidate(); + } + + else { // SKIF Custom, GOG without Galaxy, Xbox + + std::wstring wszPath = (pApp->store == "Xbox") + ? pApp->launch_configs[0].executable_helper + : pApp->launch_configs[0].getExecutableFullPath(pApp->id); + + SKIF_Util_OpenURI (wszPath, SW_SHOWDEFAULT, L"OPEN", pApp->launch_configs[0].launch_options.c_str(), pApp->launch_configs[0].working_dir.c_str()); + + /* + SHELLEXECUTEINFOW + sexi = { }; + sexi.cbSize = sizeof (SHELLEXECUTEINFOW); + sexi.lpVerb = L"OPEN"; + sexi.lpFile = wszPath.c_str(); + sexi.lpParameters = pApp->launch_configs[0].launch_options.c_str(); + sexi.lpDirectory = pApp->launch_configs[0].working_dir .c_str(); + sexi.nShow = SW_SHOWDEFAULT; + sexi.fMask = SEE_MASK_FLAG_NO_UI | SEE_MASK_NOZONECHECKS; // SEE_MASK_ASYNCOK cannot be used since we are removing the environmental variable + + ShellExecuteExW (&sexi); + */ + } + + // Fallback for minimizing SKIF when not using SK if configured as such + if (_registry.bMinimizeOnGameLaunch && ! usingSK && SKIF_ImGui_hWnd != NULL) + ShowWindowAsync (SKIF_ImGui_hWnd, SW_SHOWMINNOACTIVE); } + + clickedGameLaunch = clickedGameLaunchWoSK = false; } + + // Disable the button for global injection types if the servlets are missing + if ( ! _inject.bHasServlet && !_cache.injection.type._Equal ("Local") ) + SKIF_ImGui_PopDisableState ( ); - // Increment the tick - if (incTick) - timeLastTick = timeCurr; + if (pApp->_status.running) + ImGui::PopStyleColor ( ); - if (ImGui::BeginPopup ("CoverMenu", ImGuiWindowFlags_NoMove)) + if (ImGui::IsItemClicked(ImGuiMouseButton_Right) && + ! openedGameContextMenu) { - //static - // app_record_s* pApp = nullptr; + openedGameContextMenu = true; + } - //for (auto& app : apps) - // if (app.second.id == appid) - // pApp = &app.second; + ImGui::EndChildFrame (); +} - if (pApp != nullptr) - { - // Column 1: Icons +#pragma endregion - ImGui::BeginGroup ( ); - ImVec2 iconPos = ImGui::GetCursorPos(); +#pragma region UpdateInjectionStrategy - ImGui::ItemSize ( ImVec2 (ImGui::CalcTextSize (ICON_FA_FILE_IMAGE ).x, ImGui::GetTextLineHeight())); - if (pApp->tex_cover.isCustom) - ImGui::ItemSize (ImVec2 (ImGui::CalcTextSize (ICON_FA_ROTATE_LEFT ).x, ImGui::GetTextLineHeight())); - else if (pApp->tex_cover.isManaged) - ImGui::ItemSize (ImVec2 (ImGui::CalcTextSize (ICON_FA_ROTATE ).x, ImGui::GetTextLineHeight())); - ImGui::PushStyleColor (ImGuiCol_Separator, ImVec4(0, 0, 0, 0)); - ImGui::Separator ( ); - ImGui::PopStyleColor ( ); - ImGui::ItemSize (ImVec2 (ImGui::CalcTextSize (ICON_FA_UP_RIGHT_FROM_SQUARE).x, ImGui::GetTextLineHeight())); +void +UpdateInjectionStrategy (app_record_s* pApp) +{ + if (pApp == nullptr || pApp->id == SKIF_STEAM_APPID) + return; - ImGui::EndGroup ( ); + static SKIF_CommonPathsCache& _path_cache = SKIF_CommonPathsCache::GetInstance ( ); + static SKIF_RegistrySettings& _registry = SKIF_RegistrySettings::GetInstance ( ); + static SKIF_InjectionContext& _inject = SKIF_InjectionContext::GetInstance ( ); + + // Handle Steam games + if (pApp->store == "Steam") + { + pApp->specialk.injection = + SKIF_InstallUtils_GetInjectionStrategy (pApp->id); - ImGui::SameLine ( ); + // Scan Special K configuration, etc. + if (pApp->specialk.profile_dir.empty ()) + { + pApp->specialk.profile_dir = pApp->specialk.injection.config.dir; - // Column 2: Items - ImGui::BeginGroup ( ); - bool dontCare = false; - if (ImGui::Selectable ("Change", dontCare, ImGuiSelectableFlags_SpanAllColumns)) + if (! pApp->specialk.profile_dir.empty ()) { - LPWSTR pwszFilePath = NULL; - if (SK_FileOpenDialog(&pwszFilePath, COMDLG_FILTERSPEC{ L"Images", L"*.jpg;*.png" }, 1, FOS_FILEMUSTEXIST, FOLDERID_Pictures)) - { - std::wstring targetPath = L""; - std::wstring ext = std::filesystem::path(pwszFilePath).extension().wstring(); + SK_VirtualFS profile_vfs; - if (pApp->id == SKIF_STEAM_APPID) - targetPath = SK_FormatStringW (LR"(%ws\Assets\)", _path_cache.specialk_userdata); - else if (pApp->store == "Other") - targetPath = SK_FormatStringW (LR"(%ws\Assets\Custom\%i\)", _path_cache.specialk_userdata, pApp->id); - else if (pApp->store == "Epic") - targetPath = SK_FormatStringW (LR"(%ws\Assets\EGS\%ws\)", _path_cache.specialk_userdata, SK_UTF8ToWideChar(pApp->Epic_AppName).c_str()); - else if (pApp->store == "GOG") - targetPath = SK_FormatStringW (LR"(%ws\Assets\GOG\%i\)", _path_cache.specialk_userdata, pApp->id); - else if (pApp->store == "Xbox") - targetPath = SK_FormatStringW (LR"(%ws\Assets\Xbox\%ws\)", _path_cache.specialk_userdata, SK_UTF8ToWideChar(pApp->Xbox_PackageName).c_str()); - else if (pApp->store == "Steam") - targetPath = SK_FormatStringW (LR"(%ws\Assets\Steam\%i\)", _path_cache.specialk_userdata, pApp->id); + int files = + SK_VFS_ScanTree ( profile_vfs, + pApp->specialk.profile_dir.data (), 2 ); - if (targetPath != L"") - { - std::error_code ec; - // Create any missing directories - if (! std::filesystem::exists ( targetPath, ec)) - std::filesystem::create_directories (targetPath, ec); + UNREFERENCED_PARAMETER (files); + } + } + } + + // Handle GOG, Epic, and SKIF Custom games + else { + DWORD dwBinaryType = MAXDWORD; + if ( GetBinaryTypeW (pApp->launch_configs[0].getExecutableFullPath(pApp->id).c_str (), &dwBinaryType) ) + { + if (dwBinaryType == SCS_32BIT_BINARY) + pApp->specialk.injection.injection.bitness = InjectionBitness::ThirtyTwo; + else if (dwBinaryType == SCS_64BIT_BINARY) + pApp->specialk.injection.injection.bitness = InjectionBitness::SixtyFour; + } - targetPath += L"cover"; + std::wstring test_paths[] = { + pApp->launch_configs[0].getExecutableDir(pApp->id, false), + pApp->launch_configs[0].working_dir + }; - if (ext == L".jpg") - DeleteFile((targetPath + L".png").c_str()); + if (test_paths[0] == test_paths[1]) + test_paths[1] = L""; - CopyFile(pwszFilePath, (targetPath + ext).c_str(), false); + struct { + InjectionBitness bitness; + InjectionPoint entry_pt; + std::wstring name; + std::wstring path; + } test_dlls [] = { + { pApp->specialk.injection.injection.bitness, InjectionPoint::D3D9, L"d3d9", L"" }, + { pApp->specialk.injection.injection.bitness, InjectionPoint::DXGI, L"dxgi", L"" }, + { pApp->specialk.injection.injection.bitness, InjectionPoint::D3D11, L"d3d11", L"" }, + { pApp->specialk.injection.injection.bitness, InjectionPoint::OpenGL, L"OpenGL32", L"" }, + { pApp->specialk.injection.injection.bitness, InjectionPoint::DInput8, L"dinput8", L"" } + }; + + // Assume Global 32-bit if we don't know otherwise + bool bIs64Bit = + ( pApp->specialk.injection.injection.bitness == + InjectionBitness::SixtyFour ); + + pApp->specialk.injection.config.type = + ConfigType::Centralized; + + wchar_t wszPathToSelf [MAX_PATH] = { }; + GetModuleFileNameW (0, wszPathToSelf, MAX_PATH); + PathRemoveFileSpecW ( wszPathToSelf); + PathAppendW ( wszPathToSelf, + bIs64Bit ? L"SpecialK64.dll" + : L"SpecialK32.dll" ); + + pApp->specialk.injection.injection.dll_path = wszPathToSelf; + pApp->specialk.injection.injection.dll_ver = + SKIF_GetSpecialKDLLVersion ( wszPathToSelf); + + pApp->specialk.injection.injection.type = + InjectionType::Global; + pApp->specialk.injection.injection.entry_pt = + InjectionPoint::CBTHook; + pApp->specialk.injection.config.file = + L"SpecialK.ini"; + + bool breakOuterLoop = false; + for ( auto& test_path : test_paths) + { + if (test_path.empty()) + continue; - update = true; - lastCover.reset(); // Needed as otherwise SKIF would not reload the cover - } - } - } - else + for ( auto& dll : test_dlls ) { - SKIF_ImGui_SetMouseCursorHand ( ); - } + dll.path = + ( test_path + LR"(\)" ) + + ( dll.name + L".dll" ); - // Only show option if: - // - We use a custom cover - // - We use an official cover as long as: - // * We do not have SKIF selected (has no official cover background to remove) - // * We do not have a GOG title selected (uses GOG Galaxy artwork) - if (pApp->tex_cover.isCustom || pApp->tex_cover.isManaged) - { - if (ImGui::Selectable ((pApp->tex_cover.isCustom) ? ((pApp->store == "Other") ? "Clear" : "Reset") : "Refresh", dontCare, ImGuiSelectableFlags_SpanAllColumns)) + if (PathFileExistsW (dll.path.c_str ())) { - std::wstring targetPath = L""; - - if (pApp->id == SKIF_STEAM_APPID) - targetPath = SK_FormatStringW (LR"(%ws\Assets\)", _path_cache.specialk_userdata); - else if (pApp->store == "Other") - targetPath = SK_FormatStringW (LR"(%ws\Assets\Custom\%i\)", _path_cache.specialk_userdata, pApp->id); - else if (pApp->store == "Epic") - targetPath = SK_FormatStringW (LR"(%ws\Assets\EGS\%ws\)", _path_cache.specialk_userdata, SK_UTF8ToWideChar(pApp->Epic_AppName).c_str()); - else if (pApp->store == "GOG") - targetPath = SK_FormatStringW (LR"(%ws\Assets\GOG\%i\)", _path_cache.specialk_userdata, pApp->id); - else if (pApp->store == "Xbox") - targetPath = SK_FormatStringW (LR"(%ws\Assets\Xbox\%ws\)", _path_cache.specialk_userdata, SK_UTF8ToWideChar(pApp->Xbox_PackageName).c_str()); - else if (pApp->store == "Steam") - targetPath = SK_FormatStringW (LR"(%ws\Assets\Steam\%i\)", _path_cache.specialk_userdata, pApp->id); + std::wstring dll_ver = + SKIF_GetSpecialKDLLVersion (dll.path.c_str ()); - if (PathFileExists (targetPath.c_str())) + if (! dll_ver.empty ()) { - std::wstring fileName = (pApp->tex_cover.isCustom) ? L"cover" : L"cover-original"; - - bool d1 = DeleteFile ((targetPath + fileName + L".png").c_str()), - d2 = DeleteFile ((targetPath + fileName + L".jpg").c_str()), - d3 = false, - d4 = false; + pApp->specialk.injection.injection = { + dll.bitness, + dll.entry_pt, InjectionType::Local, + dll.path, dll_ver + }; - // For Xbox titles we also store a fallback cover that we must reset - if (! pApp->tex_cover.isCustom && pApp->store == "Xbox") + if (PathFileExistsW ((test_path + LR"(\SpecialK.Central)").c_str ())) { - fileName = L"cover-fallback.png"; - d3 = DeleteFile ((targetPath + fileName + L".png").c_str()), - d4 = DeleteFile ((targetPath + fileName + L".jpg").c_str()); + pApp->specialk.injection.config.type = + ConfigType::Centralized; } - // If any file was removed - if (d1 || d2 || d3 || d4) + else { - update = true; - lastCover.reset(); // Needed as otherwise SKIF would not reload the cover + pApp->specialk.injection.config = { + ConfigType::Localized, + test_path + }; } + + pApp->specialk.injection.config.file = + dll.name + L".ini"; + + breakOuterLoop = true; + break; } - } - else - { - SKIF_ImGui_SetMouseCursorHand ( ); + + else + PLOG_VERBOSE << "Local wrapper was not detected as being Special K: " << dll.path; } } - ImGui::Separator ( ); + if (breakOuterLoop) + break; + } - auto _GetSteamGridDBLink = [&](void) -> std::string - { - // Strip (recently added) from the game name - std::string name = pApp->names.normal; - try { - name = std::regex_replace(name, std::regex(R"( \(recently added\))"), ""); - } - catch (const std::exception& e) - { - UNREFERENCED_PARAMETER(e); - } + if (pApp->specialk.injection.config.type == ConfigType::Centralized) + { + pApp->specialk.injection.config.dir = + SK_FormatStringW(LR"(%ws\Profiles\%ws)", + _path_cache.specialk_userdata, + pApp->specialk.profile_dir.c_str()); + } - return (pApp->store == "Steam") - ? SK_FormatString("https://www.steamgriddb.com/steam/%lu/grids", pApp->id) - : SK_FormatString("https://www.steamgriddb.com/search/grids?term=%s", name.c_str()); + pApp->specialk.injection.config.file = + ( pApp->specialk.injection.config.dir + LR"(\)" ) + + pApp->specialk.injection.config.file; + } - }; +} - if (ImGui::Selectable ("Browse SteamGridDB", dontCare, ImGuiSelectableFlags_SpanAllColumns)) - { - SKIF_Util_OpenURI (SK_UTF8ToWideChar(_GetSteamGridDBLink()).c_str()); - } - else - { - SKIF_ImGui_SetMouseCursorHand ( ); - SKIF_ImGui_SetHoverText (_GetSteamGridDBLink()); - } +#pragma endregion - ImGui::EndGroup ( ); - ImGui::SetCursorPos (iconPos); - ImGui::TextColored ( - (_registry.iStyle == 2) ? ImColor (0, 0, 0) : ImColor (255, 255, 255), - ICON_FA_FILE_IMAGE - ); +void +SKIF_UI_Tab_DrawLibrary (void) +{ +#if 0 + SKIF_GamesCollection& _games = SKIF_GamesCollection::GetInstance(); - if (pApp->tex_cover.isCustom) - ImGui::TextColored ( - (_registry.iStyle == 2) ? ImColor (0, 0, 0) : ImColor (255, 255, 255), - ICON_FA_ROTATE_LEFT - ); - else if (pApp->tex_cover.isManaged) - ImGui::TextColored ( - (_registry.iStyle == 2) ? ImColor (0, 0, 0) : ImColor (255, 255, 255), - ICON_FA_ROTATE - ); + // Always read from the last written index + int nowReading = _games.snapshot_idx_written.load ( ); + _games.snapshot_idx_reading.store (nowReading); - ImGui::Separator ( ); + if (RepopulateGames) + _games.RefreshGames ( ); - ImGui::TextColored ( - ImGui::GetStyleColorVec4(ImGuiCol_SKIF_Info), - ICON_FA_UP_RIGHT_FROM_SQUARE - ); + /* + std::vector > &apps_new = + _games.GetGames ( ); + */ + + std::vector >* apps_new = _games.GetGames ( ); + if (apps_new != nullptr && ! apps_new->empty() && RepopulateGames) + { + PLOG_VERBOSE << "New library backend discovered the following games:"; + for (auto const& app : *apps_new) { + PLOG_VERBOSE << app->names.normal; + //OutputDebugString(SK_UTF8ToWideChar(app->names.normal).c_str()); + //OutputDebugString(L"\n"); } + } +#endif - ImGui::EndPopup ( ); + static SKIF_CommonPathsCache& _path_cache = SKIF_CommonPathsCache::GetInstance ( ); + static SKIF_RegistrySettings& _registry = SKIF_RegistrySettings::GetInstance ( ); + static SKIF_InjectionContext& _inject = SKIF_InjectionContext::GetInstance ( ); + static SKIF_Lib_SummaryCache& _cache = SKIF_Lib_SummaryCache::GetInstance ( ); + + static SKIF_DirectoryWatch SKIF_Epic_ManifestWatch; + + //static CComPtr pTex2D; + static CComPtr pTexSRV; + //static ImVec2 vecTex2D; + + static ImVec2 vecCoverUv0 = ImVec2 (0, 0), + vecCoverUv1 = ImVec2 (1, 1); + + static DirectX::TexMetadata meta = { }; + static DirectX::ScratchImage img = { }; + + // Initialize the Steam appinfo.vdf Reader + if (! _registry.bDisableSteamLibrary) + { + SK_RunOnce ( + appinfo = std::make_unique (std::wstring(_path_cache.steam_install) + LR"(\appcache\appinfo.vdf)"); + ); } - float fY = - ImGui::GetCursorPosY ( ); + auto& io = + ImGui::GetIO (); - ImGui::EndGroup ( ); - ImGui::SameLine (0.0f, 3.0f * SKIF_ImGui_GlobalDPIScale); // 0.0f, 0.0f + //static volatile LONG icon_thread = 1; + //static volatile LONG need_sort = 0; - float fZ = - ImGui::GetCursorPosX ( ); + bool sort_changed = false; + static bool update = true; + static bool populated = false; - if (update) + auto _SortApps = [&](void) -> void { - //SKIF_GameManagement_ShowScreenshot (L""); - update = false; + std::sort ( apps.begin (), + apps.end (), + []( const std::pair & a, + const std::pair & b ) -> int + { + return a.second.names.all_upper_alnum.compare( + b.second.names.all_upper_alnum + ) < 0; + } + ); - // Ensure we aren't already loading this cover - if (lastCover.appid != pApp->id || lastCover.store != pApp->store) + sort_changed = true; + PLOG_INFO << "Apps were sorted!"; + }; + + struct { + uint32_t appid = SKIF_STEAM_APPID; + std::string store = "Steam"; + SKIF_DirectoryWatch dir_watch; + bool reset_to_skif = true; + + void reset () { - loadCover = true; - lastCover.appid = pApp->id; - lastCover.store = pApp->store; + appid = (reset_to_skif) ? SKIF_STEAM_APPID : 0; + store = (reset_to_skif) ? "Steam" : ""; + + if (dir_watch._hChangeNotification != INVALID_HANDLE_VALUE) + dir_watch.reset(); } - } + } static selection, item_clicked, lastCover; - if (loadCover && populated) // && ! InterlockedExchangeAdd (&icon_thread, 0)) /* && (ImGui::GetCurrentWindowRead()->HiddenFramesCannotSkipItems == 0) */ - { // Load cover first after the window has been shown -- to fix one copy leaking of the cover - // 2023-03-24: Is this even needed any longer after fixing the double-loading that was going on? - // 2023-03-25: Disabled HiddenFramesCannotSkipItems check to see if it's solved. - // 2023-10-05: Disabled waiting for the icon thread as well - loadCover = false; + // We need to ensure the lastCover isn't set to SKIF's app ID as that would prevent the cover from loading on launch + SK_RunOnce (lastCover.reset_to_skif = false; lastCover.reset()); - // Reset variables used to track whether we're still loading a game cover, or if we're missing one - gameCoverLoading.store (true); - tryingToLoadCover = true; - queuePosGameCover = textureLoadQueueLength.load() + 1; + if (! ImGui::IsAnyMouseDown ( ) || ! SKIF_ImGui_IsFocused ( )) + { + // Temporarily disabled since this gets triggered on game launch/shutdown as well... + // And generally also breaks SKIF's library view on occasion + //if (SKIF_Steam_isLibrariesSignaled ()) + // RepopulateGames = true; -//#define _WRITE_APPID_INI -#ifdef _WRITE_APPID_INI - if ( appinfo != nullptr && pApp->store == "Steam") + if (! _registry.bDisableEpicLibrary && SKIF_Epic_ManifestWatch.isSignaled (SKIF_Epic_AppDataPath, true)) + RepopulateGames = true; + + if (! _registry.bDisableXboxLibrary && SKIF_Xbox_hasInstalledGamesChanged ( )) + RepopulateGames = true; + + if (! _registry.bDisableGOGLibrary) { - skValveDataFile::appinfo_s *pAppInfo = - appinfo->getAppInfo ( pApp->id ); + if (SKIF_GOG_hasInstalledGamesChanged ( )) + RepopulateGames = true; - DBG_UNREFERENCED_LOCAL_VARIABLE (pAppInfo); + if (SKIF_GOG_hasGalaxySettingsChanged ( )) + SKIF_GOG_UpdateGalaxyUserID ( ); } -#endif + } - //PLOG_VERBOSE << "ImGui Frame Counter: " << ImGui::GetFrameCount(); + if (RepopulateGames) + { + RepopulateGames = false; - // We're going to stream the cover in asynchronously on this thread - _beginthread ([](void*)->void - { - CoInitializeEx (nullptr, 0x0); + // Clear cached lists + apps.clear (); - SKIF_Util_SetThreadDescription (GetCurrentThread (), L"SKIF_LibCoverWorker"); + // Reset selection to Special K, but only if set to something else than -1 + if (selection.appid != 0) + selection.reset(); - PLOG_INFO << "Thread started!"; - PLOG_INFO << "Streaming game cover asynchronously..."; + update = true; - if (pApp == nullptr) - { - PLOG_ERROR << "Aborting due to pApp being a nullptr!"; - return; - } + populated = false; + } - app_record_s* _pApp = pApp; + if (! populated) + { + //InterlockedExchange (&icon_thread, 1); - int queuePos = getTextureLoadQueuePos(); - //PLOG_VERBOSE << "queuePos = " << queuePos; + PLOG_INFO << "Populating library list..."; - static ImVec2 _vecCoverUv0(vecCoverUv0); - static ImVec2 _vecCoverUv1(vecCoverUv1); - static CComPtr _pTexSRV (pTexSRV.p); + apps = SKIF_Steam_GetInstalledAppIDs (); - // Most textures are pushed to be released by LoadLibraryTexture(), - // however the current cover pointer is only updated to the new one - // *after* the old cover has been pushed to be released. - // - // This means there's a short thread race where the main thread - // can still reference a texture that has already been released. - // - // As a result, we preface the whole loading of the new cover texture - // by explicitly changing the current cover texture to point to nothing. - // - // The only downside is that the cover transition is not seemless; - // a black/non-existent cover will be displayed in-between. - // - // But at least SKIF does not run the risk of crashing as often :) - pTexSRV = nullptr; + for (auto& app : apps) + if (app.second.id == SKIF_STEAM_APPID) + SKIF_STEAM_OWNER = true; - std::wstring load_str; + if ( ! SKIF_STEAM_OWNER ) + { + app_record_s SKIF_record (SKIF_STEAM_APPID); - // SKIF - if (_pApp->id == SKIF_STEAM_APPID) - { - // No need to change the string in any way - } + SKIF_record.id = SKIF_STEAM_APPID; + SKIF_record.names.normal = "Special K"; + SKIF_record.names.all_upper = "SPECIAL K"; + SKIF_record.install_dir = _path_cache.specialk_install; + SKIF_record.store = "Steam"; + SKIF_record.ImGuiLabelAndID = SK_FormatString("%s###%s%i", SKIF_record.names.normal.c_str(), SKIF_record.store.c_str(), SKIF_record.id); + SKIF_record.ImGuiPushID = SK_FormatString("%s%i", SKIF_record.store.c_str(), SKIF_record.id); - // SKIF Custom - else if (_pApp->store == "Other") - { - load_str = L"cover"; - } + std::pair + SKIF ( "Special K", SKIF_record ); - // GOG - else if (_pApp->store == "GOG") - { - load_str = L"*_glx_vertical_cover.webp"; - } + apps.emplace_back (SKIF); + } - // Epic - else if (_pApp->store == "Epic") - { - load_str = - SK_FormatStringW (LR"(%ws\Assets\EGS\%ws\cover-original.jpg)", _path_cache.specialk_userdata, SK_UTF8ToWideChar(_pApp->Epic_AppName).c_str()); + // Load GOG titles from registry + if (! _registry.bDisableGOGLibrary) + SKIF_GOG_GetInstalledAppIDs (&apps); - if ( ! PathFileExistsW (load_str. c_str ()) ) + // Load Epic titles from disk + if (! _registry.bDisableEpicLibrary) + SKIF_Epic_GetInstalledAppIDs (&apps); + + if (! _registry.bDisableXboxLibrary) + SKIF_Xbox_GetInstalledAppIDs (&apps); + + // Load custom SKIF titles from registry + SKIF_GetCustomAppIDs (&apps); + + // Set to last selected if it can be found + if (selection.appid == SKIF_STEAM_APPID) + { + for (auto& app : apps) + { + if (app.second.id == _registry.iLastSelectedGame && + app.second.store == SK_WideCharToUTF8 (_registry.wsLastSelectedStore)) { - SKIF_Epic_IdentifyAssetNew (_pApp->Epic_CatalogNamespace, _pApp->Epic_CatalogItemId, _pApp->Epic_AppName, _pApp->Epic_DisplayName); + selection.appid = app.second.id; + selection.store = app.second.store; + manual_selection.id = selection.appid; + manual_selection.store = selection.store; + update = true; } - - else { - // If the file exist, load the metadata from the local image, but only if low bandwidth mode is not enabled - if ( ! _registry.bLowBandwidthMode && - SUCCEEDED ( - DirectX::GetMetadataFromWICFile ( - load_str.c_str (), - DirectX::WIC_FLAGS_FILTER_POINT, - meta - ) - ) - ) - { - // If the image is in reality 600 in width or 900 in height, which indicates a low-res cover, - // download the full-size cover and replace the existing one. - if (meta.width == 600 || - meta.height == 900) - { - SKIF_Epic_IdentifyAssetNew (_pApp->Epic_CatalogNamespace, _pApp->Epic_CatalogItemId, _pApp->Epic_AppName, _pApp->Epic_DisplayName); - } - } + } + } + + PLOG_INFO << "Loading game names synchronously..."; + + // Clear any existing trie + labels = Trie { }; + + // Handle names first + for ( auto& app : apps ) + { + //PLOG_DEBUG << "Working on " << app.second.id << " (" << app.second.store << ")"; + + // Special handling for non-Steam owners of Special K / SKIF + if ( app.second.id == SKIF_STEAM_APPID ) + app.first = "Special K"; + + // Regular handling for the remaining Steam games + else if (app.second.store == "Steam") { + app.first.clear (); + + app.second._status.refresh (&app.second); + } + + // Only bother opening the application manifest + // and looking for a name if the client claims + // the app is installed. + if (app.second._status.installed) + { + if (! app.second.names.normal.empty ()) + { + app.first = app.second.names.normal; + } + + // Some games have an install state but no name, + // for those we have to consult the app manifest + else if (app.second.store == "Steam") + { + app.first = + SK_UseManifestToGetAppName ( + app.second.id ); + } + + // Corrupted app manifest / not known to Steam client; SKIP! + if (app.first.empty ()) + { + PLOG_DEBUG << "App ID " << app.second.id << " (" << app.second.store << ") has no name; ignoring!"; + + app.second.id = 0; + continue; + } + + std::string original_name = app.first; + + // Some games use weird Unicode character combos that ImGui can't handle, + // so let's replace those with the normal ones. + + // Replace RIGHT SINGLE QUOTATION MARK (Code: 2019 | UTF-8: E2 80 99) + // with a APOSTROPHE (Code: 0027 | UTF-8: 27) + app.first = std::regex_replace(app.first, std::regex("\xE2\x80\x99"), "\x27"); + + // Replace LATIN SMALL LETTER O (Code: 006F | UTF-8: 6F) and COMBINING DIAERESIS (Code: 0308 | UTF-8: CC 88) + // with a LATIN SMALL LETTER O WITH DIAERESIS (Code: 00F6 | UTF-8: C3 B6) + app.first = std::regex_replace(app.first, std::regex("\x6F\xCC\x88"), "\xC3\xB6"); + + // Strip game names from special symbols (disabled due to breaking some Chinese characters) + //const char* chars = (const char *)u8"\u00A9\u00AE\u2122"; // Copyright (c), Registered (R), Trademark (TM) + //for (unsigned int i = 0; i < strlen(chars); ++i) + //app.first.erase(std::remove(app.first.begin(), app.first.end(), chars[i]), app.first.end()); + + // Remove COPYRIGHT SIGN (Code: 00A9 | UTF-8: C2 A9) + app.first = std::regex_replace(app.first, std::regex("\xC2\xA9"), ""); + + // Remove REGISTERED SIGN (Code: 00AE | UTF-8: C2 AE) + app.first = std::regex_replace(app.first, std::regex("\xC2\xAE"), ""); + + // Remove TRADE MARK SIGN (Code: 2122 | UTF-8: E2 84 A2) + app.first = std::regex_replace(app.first, std::regex("\xE2\x84\xA2"), ""); + + if (original_name != app.first) + { + PLOG_DEBUG << "Game title was changed:"; + PLOG_DEBUG << "Old: " << SK_UTF8ToWideChar(original_name.c_str()) << " (" << original_name << ")"; + PLOG_DEBUG << "New: " << SK_UTF8ToWideChar(app.first.c_str()) << " (" << app.first << ")"; } + + // Strip any remaining null terminators + app.first.erase(std::find(app.first.begin(), app.first.end(), '\0'), app.first.end()); + + // Trim leftover spaces + app.first.erase(app.first.begin(), std::find_if(app.first.begin(), app.first.end(), [](unsigned char ch) { return !std::isspace(ch); })); + app.first.erase(std::find_if(app.first.rbegin(), app.first.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), app.first.end()); + + // Update ImGuiLabelAndID and ImGuiPushID + app.second.ImGuiLabelAndID = SK_FormatString("%s###%s%i", app.first.c_str(), app.second.store.c_str(), app.second.id); + app.second.ImGuiPushID = SK_FormatString("%s%i", app.second.store.c_str(), app.second.id); } - // Xbox - else if (_pApp->store == "Xbox") + // Check if install folder exists (but not for SKIF) + if (app.second.id != SKIF_STEAM_APPID && app.second.store != "Xbox") { - load_str = - SK_FormatStringW (LR"(%ws\Assets\Xbox\%ws\cover-original.png)", _path_cache.specialk_userdata, SK_UTF8ToWideChar(_pApp->Xbox_PackageName).c_str()); + std::wstring install_dir; - if ( ! PathFileExistsW (load_str. c_str ()) ) + if (app.second.store == "Steam") + install_dir = SK_UseManifestToGetInstallDir(app.second.id); + else + install_dir = app.second.install_dir; + + if (! PathFileExists(install_dir.c_str())) { - SKIF_Xbox_IdentifyAssetNew (_pApp->Xbox_PackageName, _pApp->Xbox_StoreId); + PLOG_DEBUG << "App ID " << app.second.id << " (" << app.second.store << ") has non-existent install folder; ignoring!"; + + app.second.id = 0; + continue; } - - else { - // If the file exist, load the metadata from the local image, but only if low bandwidth mode is not enabled - if ( ! _registry.bLowBandwidthMode && - SUCCEEDED ( - DirectX::GetMetadataFromWICFile ( - load_str.c_str (), - DirectX::WIC_FLAGS_FILTER_POINT, - meta - ) - ) - ) + } + + // Prepare for the keyboard hint / search/filter functionality + if ( app.second._status.installed || app.second.id == SKIF_STEAM_APPID) + { + std::string all_upper = SKIF_Util_ToUpper (app.first), + all_upper_alnum; + + for (const char c : app.first) + { + if (! ( isalnum (c) || isspace (c) )) + continue; + + all_upper_alnum += (char)toupper (c); + } + + size_t stripped = 0; + + if (_registry.bLibraryIgnoreArticles) + { + static const + std::string toSkip [] = + { + std::string ("A "), + std::string ("AN "), + std::string ("THE ") + }; + + for ( auto& skip_ : toSkip ) { - // If the image is in reality 600 in width or 900 in height, which indicates a low-res cover, - // download the full-size cover and replace the existing one. - if (meta.width == 600 || - meta.height == 900) + if (all_upper_alnum.find (skip_) == 0) { - SKIF_Xbox_IdentifyAssetNew (_pApp->Xbox_PackageName, _pApp->Xbox_StoreId); + all_upper_alnum = + all_upper_alnum.substr ( + skip_.length () + ); + + stripped = skip_.length (); + break; } } } + + std::string trie_builder; + + for ( const char c : all_upper_alnum) + { + trie_builder += c; + + labels.insert (trie_builder); + } + + app.second.names.normal = app.first; + app.second.names.all_upper = all_upper; + app.second.names.all_upper_alnum = all_upper_alnum; + app.second.names.pre_stripped = stripped; } + } - // Steam - else if (_pApp->store == "Steam") + PLOG_INFO << "Finished loading game names synchronously..."; + + _SortApps ( ); + + PLOG_INFO << "Finished populating the library list."; + + // We're going to stream game icons asynchronously on this thread + _beginthread ([](void*)->void + { + SKIF_Util_SetThreadDescription (GetCurrentThread (), L"SKIF_LibRefreshWorker"); + + CoInitializeEx (nullptr, 0x0); + + PLOG_INFO << "Thread started!"; + + PLOG_INFO << "Loading the embedded Patreon texture..."; + ImVec2 dontCare1, dontCare2; + if (pPatTexSRV.p == nullptr) + LoadLibraryTexture (LibraryTexture::Patreon, SKIF_STEAM_APPID, pPatTexSRV, L"(patreon.png)", dontCare1, dontCare2); + if (pSKLogoTexSRV.p == nullptr) + LoadLibraryTexture (LibraryTexture::Cover, SKIF_STEAM_APPID, pSKLogoTexSRV, L"(sk_boxart.png)", dontCare1, dontCare2); + + // FIX: This causes the whole apps vector to be resorted by the main thread + // which then causes textures being loaded below to end up being assigned to the wrong game + //InterlockedExchange (&need_sort, 1); + + // Force a refresh when the game names have finished being streamed + PostMessage (SKIF_Notify_hWnd, WM_SKIF_ICON, 0x0, 0x0); + + // Load icons last + for ( auto& app : apps ) { - std::wstring load_str_2x ( - SK_FormatStringW (LR"(%ws\Assets\Steam\%i\)", _path_cache.specialk_userdata, _pApp->id) - ); + if (app.second.id == 0) + continue; - std::error_code ec; - // Create any missing directories - if (! std::filesystem::exists ( load_str_2x, ec)) - std::filesystem::create_directories (load_str_2x, ec); + std::wstring load_str; + + if (app.second.id == SKIF_STEAM_APPID) // SKIF + load_str = L"_icon.jpg"; + else if (app.second.store == "Other") // SKIF Custom + load_str = L"icon"; + else if (app.second.store == "Epic") // Epic + load_str = L"icon"; + else if (app.second.store == "GOG") // GOG + load_str = app.second.install_dir + L"\\goggame-" + std::to_wstring(app.second.id) + L".ico"; + else if (app.second.store == "Steam") // STEAM + load_str = SK_FormatStringW(LR"(%ws\appcache\librarycache\%i_icon.jpg)", _path_cache.steam_install, app.second.id); //L"_icon.jpg" - load_str_2x += L"cover-original.jpg"; + LoadLibraryTexture ( LibraryTexture::Icon, + app.second.id, + app.second.tex_icon.texture, + load_str, + dontCare1, + dontCare2, + &app.second ); + } + + PLOG_INFO << "Finished streaming game icons asynchronously..."; - load_str = SK_GetSteamDir (); + //InterlockedExchange (&icon_thread, 0); - load_str += LR"(/appcache/librarycache/)" + - std::to_wstring (_pApp->id) + - L"_library_600x900.jpg"; + // Force a refresh when the game icons have finished being streamed + PostMessage (SKIF_Notify_hWnd, WM_SKIF_ICON, 0x0, 0x0); - std::wstring load_str_final = load_str; + PLOG_INFO << "Thread stopped!"; + }, 0x0, NULL); - // Get UNIX-style time - time_t ltime; - time (<ime); + populated = true; + } - std::wstring url = L"https://steamcdn-a.akamaihd.net/steam/apps/"; - url += std::to_wstring (_pApp->id); - url += L"/library_600x900_2x.jpg"; - url += L"?t="; - url += std::to_wstring (ltime); // Add UNIX-style timestamp to ensure we don't get anything cached + extern bool coverFadeActive; + static int tmp_iDimCovers = _registry.iDimCovers; + + static + app_record_s* pApp = nullptr; - // If 600x900 exists but 600x900_x2 cannot be found - if ( PathFileExistsW (load_str. c_str ()) && - ! PathFileExistsW (load_str_2x.c_str ()) ) - { - // Load the metadata from 600x900, but only if low bandwidth mode is not enabled - if ( ! _registry.bLowBandwidthMode && - SUCCEEDED ( - DirectX::GetMetadataFromWICFile ( - load_str.c_str (), - DirectX::WIC_FLAGS_FILTER_POINT, - meta - ) - ) - ) - { - // If the image is in reality 300x450, which indicates a real cover, - // download the real 600x900 cover and store it in _x2 - if (meta.width == 300 && - meta.height == 450) - { - SKIF_Util_GetWebResource (url, load_str_2x); - load_str_final = load_str_2x; - } - } - } + for (auto& app : apps) + if (app.second.id == selection.appid && app.second.store == selection.store) + pApp = &app.second; + + // Apply changes when the selected game changes + if (update) + { + fTint = (_registry.iDimCovers == 0) ? 1.0f : fTintMin; + + if (pApp != nullptr) + { + if ( pApp->install_dir != selection.dir_watch._path) + selection.dir_watch.reset ( ); + + if (! pApp->install_dir.empty()) + selection.dir_watch.isSignaled (pApp->install_dir, true); + + //UpdateInjectionStrategy (pApp); + //_cache.Refresh (pApp); + } + } + + // Apply changes when the _registry.iDimCovers var has been changed in the Settings tab + else if (tmp_iDimCovers != _registry.iDimCovers) + { + fTint = (_registry.iDimCovers == 0) ? 1.0f : fTintMin; + + tmp_iDimCovers = _registry.iDimCovers; + } + + ImGui::BeginGroup ( ); + + static int queuePosGameCover = 0; + static char cstrLabelLoading[] = "..."; + static char cstrLabelMissing[] = "Missing cover :("; + static char cstrLabelGOGUser[] = "Please sign in to GOG Galaxy to\n" + "allow the cover to be populated :)"; + + ImVec2 vecPosCoverImage = ImGui::GetCursorPos ( ); + vecPosCoverImage.x -= 1.0f * SKIF_ImGui_GlobalDPIScale; + + if (tryingToLoadCover) + { + ImGui::SetCursorPos (ImVec2 ( + vecPosCoverImage.x + 300.0F * SKIF_ImGui_GlobalDPIScale - ImGui::CalcTextSize (cstrLabelLoading).x / 2, + vecPosCoverImage.y + 450.0F * SKIF_ImGui_GlobalDPIScale - ImGui::CalcTextSize (cstrLabelLoading).y / 2)); + ImGui::TextDisabled ( cstrLabelLoading); + } + + else if (textureLoadQueueLength.load() == queuePosGameCover && pTexSRV.p == nullptr) + { + extern std::wstring GOGGalaxy_UserID; + if (pApp != nullptr && pApp->store == "GOG" && GOGGalaxy_UserID.empty()) + { + ImGui::SetCursorPos (ImVec2 ( + vecPosCoverImage.x + 300.0F * SKIF_ImGui_GlobalDPIScale - ImGui::CalcTextSize (cstrLabelGOGUser).x / 2, + vecPosCoverImage.y + 450.0F * SKIF_ImGui_GlobalDPIScale - ImGui::CalcTextSize (cstrLabelGOGUser).y / 2)); + ImGui::TextDisabled ( cstrLabelGOGUser); + } + else { + ImGui::SetCursorPos (ImVec2 ( + vecPosCoverImage.x + 300.0F * SKIF_ImGui_GlobalDPIScale - ImGui::CalcTextSize (cstrLabelMissing).x / 2, + vecPosCoverImage.y + 450.0F * SKIF_ImGui_GlobalDPIScale - ImGui::CalcTextSize (cstrLabelMissing).y / 2)); + ImGui::TextDisabled ( cstrLabelMissing); + } + } + + ImGui::SetCursorPos (vecPosCoverImage); + +#if 0 + extern bool SKIF_bHDREnabled; + + float fGammaCorrectedTint = + ((! SKIF_bHDREnabled && _registry.iSDRMode == 2) || + ( SKIF_bHDREnabled && _registry.iHDRMode == 2)) + ? ApplySRGBAlpha (fTint) + : fTint; +#endif + + // Display game cover image + SKIF_ImGui_OptImage (pTexSRV.p, + ImVec2 (600.0F * SKIF_ImGui_GlobalDPIScale, + 900.0F * SKIF_ImGui_GlobalDPIScale), + vecCoverUv0, // Top Left coordinates + vecCoverUv1, // Bottom Right coordinates + ImVec4 (fTint, fTint, fTint, fAlpha), + (_registry.bUIBorders) ? ImGui::GetStyleColorVec4 (ImGuiCol_Border) : ImVec4 (0.0f, 0.0f, 0.0f, 0.0f) // Border + ); - // If 600x900_x2 exists, check the last modified time stamps - else { - WIN32_FILE_ATTRIBUTE_DATA faX1{}, faX2{}; + if (ImGui::IsItemClicked (ImGuiMouseButton_Right)) + ImGui::OpenPopup ("CoverMenu"); - // ... but only if low bandwidth mode is disabled - if (! _registry.bLowBandwidthMode && - GetFileAttributesEx (load_str .c_str (), GetFileExInfoStandard, &faX1) && - GetFileAttributesEx (load_str_2x.c_str (), GetFileExInfoStandard, &faX2)) - { - // If 600x900 has been edited after 600_900_x2, - // download new copy of the 600_900_x2 cover - if (CompareFileTime (&faX1.ftLastWriteTime, &faX2.ftLastWriteTime) == 1) - { - DeleteFile (load_str_2x.c_str ()); - SKIF_Util_GetWebResource (url, load_str_2x); - } - } - - // If 600x900_x2 exists now, load it - if (PathFileExistsW (load_str_2x.c_str ())) - load_str_final = load_str_2x; - } + ImGui::SetCursorPos (vecPosCoverImage); - load_str = load_str_final; - } - - LoadLibraryTexture ( LibraryTexture::Cover, - _pApp->id, - _pTexSRV, - load_str, - _vecCoverUv0, - _vecCoverUv1, - _pApp); + if (selection.appid == SKIF_STEAM_APPID && selection.store == "Steam" && pSKLogoTexSRV != nullptr) + { + ImGui::Image (pSKLogoTexSRV.p, + ImVec2 (600.0F * SKIF_ImGui_GlobalDPIScale, + 900.0F * SKIF_ImGui_GlobalDPIScale), + ImVec2 (0.0f, 0.0f), // Top Left coordinates + ImVec2 (1.0f, 1.0f), // Bottom Right coordinates + ImVec4 (1.0f, 1.0f, 1.0f, 1.0f), // Tint for Special K's logo (always full strength) + ImVec4 (0.0f, 0.0f, 0.0f, 0.0f) // Border + ); + } - PLOG_VERBOSE << "_pTexSRV = " << _pTexSRV; + // Every >15 ms, increase/decrease the cover fade effect (makes it frame rate independent) + static DWORD timeLastTick; + DWORD timeCurr = SKIF_Util_timeGetTime(); + bool isHovered = ImGui::IsItemHovered(); + bool incTick = false; + extern int startupFadeIn; - int currentQueueLength = textureLoadQueueLength.load(); + if (startupFadeIn == 0 && pTexSRV.p != nullptr) + startupFadeIn = 1; - if (currentQueueLength == queuePos) + if (startupFadeIn == 1) + { + if (fAlpha < 1.0f && pTexSRV.p != nullptr) + { + if (timeCurr - timeLastTick > 15) { - PLOG_DEBUG << "Texture is live! Swapping it in."; - vecCoverUv0 = _vecCoverUv0; - vecCoverUv1 = _vecCoverUv1; - pTexSRV = _pTexSRV; + fAlpha += 0.05f; + incTick = true; + } + } - // Indicate that we have stopped loading the cover - gameCoverLoading.store (false); + if (fAlpha >= 1.0f) + startupFadeIn = 2; + } - // Force a refresh when the cover has been swapped in - PostMessage (SKIF_Notify_hWnd, WM_SKIF_COVER, 0x0, 0x0); + if (_registry.iDimCovers == 2) + { + if (isHovered && fTint < 1.0f) + { + if (timeCurr - timeLastTick > 15) + { + fTint = fTint + 0.01f; + incTick = true; } - else if (_pTexSRV.p != nullptr) + coverFadeActive = true; + } + else if (! isHovered && fTint > fTintMin) + { + if (timeCurr - timeLastTick > 15) { - PLOG_DEBUG << "Texture is late! (" << queuePos << " vs " << currentQueueLength << ")"; - extern concurrency::concurrent_queue > SKIF_ResourcesToFree; - PLOG_VERBOSE << "SKIF_ResourcesToFree: Pushing " << _pTexSRV.p << " to be released";; - SKIF_ResourcesToFree.push(_pTexSRV.p); - _pTexSRV.p = nullptr; + fTint = fTint - 0.01f; + incTick = true; } - PLOG_INFO << "Finished streaming game cover asynchronously..."; - PLOG_INFO << "Thread stopped!"; - - }, 0x0, NULL); + coverFadeActive = true; + } } - ImGui::BeginGroup (); + // Increment the tick + if (incTick) + timeLastTick = timeCurr; - auto _HandleKeyboardInput = [&](void) + if (ImGui::BeginPopup ("CoverMenu", ImGuiWindowFlags_NoMove)) { - static auto - constexpr _text_chars = - { 'A','B','C','D','E','F','G','H', - 'I','J','K','L','M','N','O','P', - 'Q','R','S','T','U','V','W','X', - 'Y','Z','0','1','2','3','4','5', - '6','7','8','9',' ','-',':','.' }; + //static + // app_record_s* pApp = nullptr; - static char test_ [1024] = { }; - char out [2] = { 0, 0 }; - bool bText = false; + //for (auto& app : apps) + // if (app.second.id == appid) + // pApp = &app.second; - for ( auto c : _text_chars ) + if (pApp != nullptr) { - if (io.KeysDownDuration [c] == 0.0f && - (c != ' ' || strlen (test_) > 0)) - { - out [0] = c; - StrCatA (test_, out); - bText = true; - } - } + // Column 1: Icons - const DWORD dwTimeout = 850UL; // 425UL - static DWORD dwLastUpdate = SKIF_Util_timeGetTime (); + ImGui::BeginGroup ( ); + ImVec2 iconPos = ImGui::GetCursorPos(); - struct { - std::string text = ""; - std::string store = ""; - uint32_t app_id = 0; - size_t pos = 0; - size_t len = 0; - } static result; + ImGui::ItemSize ( ImVec2 (ImGui::CalcTextSize (ICON_FA_FILE_IMAGE ).x, ImGui::GetTextLineHeight())); + if (pApp->tex_cover.isCustom) + ImGui::ItemSize (ImVec2 (ImGui::CalcTextSize (ICON_FA_ROTATE_LEFT ).x, ImGui::GetTextLineHeight())); + else if (pApp->tex_cover.isManaged) + ImGui::ItemSize (ImVec2 (ImGui::CalcTextSize (ICON_FA_ROTATE ).x, ImGui::GetTextLineHeight())); + ImGui::PushStyleColor (ImGuiCol_Separator, ImVec4(0, 0, 0, 0)); + ImGui::Separator ( ); + ImGui::PopStyleColor ( ); + ImGui::ItemSize (ImVec2 (ImGui::CalcTextSize (ICON_FA_UP_RIGHT_FROM_SQUARE).x, ImGui::GetTextLineHeight())); - if (bText) - { - dwLastUpdate = SKIF_Util_timeGetTime (); - - // Prioritize trie search first - if (labels.search (test_)) + ImGui::EndGroup ( ); + + ImGui::SameLine ( ); + + // Column 2: Items + ImGui::BeginGroup ( ); + bool dontCare = false; + if (ImGui::Selectable ("Change", dontCare, ImGuiSelectableFlags_SpanAllColumns)) { - for (auto& app : apps) + LPWSTR pwszFilePath = NULL; + if (SK_FileOpenDialog(&pwszFilePath, COMDLG_FILTERSPEC{ L"Images", L"*.jpg;*.png" }, 1, FOS_FILEMUSTEXIST, FOLDERID_Pictures)) { - if (app.second.names.all_upper_alnum.find (test_) == 0) + std::wstring targetPath = L""; + std::wstring ext = std::filesystem::path(pwszFilePath).extension().wstring(); + + if (pApp->id == SKIF_STEAM_APPID) + targetPath = SK_FormatStringW (LR"(%ws\Assets\)", _path_cache.specialk_userdata); + else if (pApp->store == "Other") + targetPath = SK_FormatStringW (LR"(%ws\Assets\Custom\%i\)", _path_cache.specialk_userdata, pApp->id); + else if (pApp->store == "Epic") + targetPath = SK_FormatStringW (LR"(%ws\Assets\EGS\%ws\)", _path_cache.specialk_userdata, SK_UTF8ToWideChar(pApp->Epic_AppName).c_str()); + else if (pApp->store == "GOG") + targetPath = SK_FormatStringW (LR"(%ws\Assets\GOG\%i\)", _path_cache.specialk_userdata, pApp->id); + else if (pApp->store == "Xbox") + targetPath = SK_FormatStringW (LR"(%ws\Assets\Xbox\%ws\)", _path_cache.specialk_userdata, SK_UTF8ToWideChar(pApp->Xbox_PackageName).c_str()); + else if (pApp->store == "Steam") + targetPath = SK_FormatStringW (LR"(%ws\Assets\Steam\%i\)", _path_cache.specialk_userdata, pApp->id); + + if (targetPath != L"") { - result.text = app.second.names.normal; - result.store = app.second.store; - result.app_id = app.second.id; - result.pos = app.second.names.pre_stripped; - result.len = strlen (test_); + std::error_code ec; + // Create any missing directories + if (! std::filesystem::exists ( targetPath, ec)) + std::filesystem::create_directories (targetPath, ec); - // Handle cases where articles are ignored + targetPath += L"cover"; - // Add one to the length if the regular all_upper cannot find a match - // as this indicates a stripped character in the found pattern - if (app.second.names.all_upper.find (test_) != 0) - result.len++; + if (ext == L".jpg") + DeleteFile((targetPath + L".png").c_str()); - break; + CopyFile(pwszFilePath, (targetPath + ext).c_str(), false); + + update = true; + lastCover.reset(); // Needed as otherwise SKIF would not reload the cover } } } - - // Fall back to using free text search when the trie fails us else { - //strncpy (test_, result.text.c_str (), 1023); - - for (auto& app : apps) - { - size_t - pos = app.second.names.all_upper.find (test_); - if (pos != std::string::npos ) // == 0 - { - result.text = app.second.names.normal; - result.store = app.second.store; - result.app_id = app.second.id; - result.pos = pos; - result.len = strlen (test_); - - break; - } - } + SKIF_ImGui_SetMouseCursorHand ( ); } - } - - if (! result.text.empty ()) - { - size_t len = - (result.len < result.text.length ( )) - ? result.len : result.text.length ( ); - std::string preSearch = result.text.substr ( 0, result.pos), - curSearch = result.text.substr (result.pos, len), - postSearch = (result.pos + len < result.text.length ( )) - ? result.text.substr (result.pos + len, std::string::npos) - : ""; + // Only show option if: + // - We use a custom cover + // - We use an official cover as long as: + // * We do not have SKIF selected (has no official cover background to remove) + // * We do not have a GOG title selected (uses GOG Galaxy artwork) + if (pApp->tex_cover.isCustom || pApp->tex_cover.isManaged) + { + if (ImGui::Selectable ((pApp->tex_cover.isCustom) ? ((pApp->store == "Other") ? "Clear" : "Reset") : "Refresh", dontCare, ImGuiSelectableFlags_SpanAllColumns)) + { + std::wstring targetPath = L""; - ImGui::OpenPopup ("###KeyboardHint"); + if (pApp->id == SKIF_STEAM_APPID) + targetPath = SK_FormatStringW (LR"(%ws\Assets\)", _path_cache.specialk_userdata); + else if (pApp->store == "Other") + targetPath = SK_FormatStringW (LR"(%ws\Assets\Custom\%i\)", _path_cache.specialk_userdata, pApp->id); + else if (pApp->store == "Epic") + targetPath = SK_FormatStringW (LR"(%ws\Assets\EGS\%ws\)", _path_cache.specialk_userdata, SK_UTF8ToWideChar(pApp->Epic_AppName).c_str()); + else if (pApp->store == "GOG") + targetPath = SK_FormatStringW (LR"(%ws\Assets\GOG\%i\)", _path_cache.specialk_userdata, pApp->id); + else if (pApp->store == "Xbox") + targetPath = SK_FormatStringW (LR"(%ws\Assets\Xbox\%ws\)", _path_cache.specialk_userdata, SK_UTF8ToWideChar(pApp->Xbox_PackageName).c_str()); + else if (pApp->store == "Steam") + targetPath = SK_FormatStringW (LR"(%ws\Assets\Steam\%i\)", _path_cache.specialk_userdata, pApp->id); - ImGui::SetNextWindowPos (ImGui::GetCurrentWindowRead()->Viewport->GetMainRect().GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + if (PathFileExists (targetPath.c_str())) + { + std::wstring fileName = (pApp->tex_cover.isCustom) ? L"cover" : L"cover-original"; - if (ImGui::BeginPopupModal("###KeyboardHint", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize)) - { - if (! preSearch.empty ()) - { - ImGui::TextDisabled ("%s", preSearch.c_str ()); - ImGui::SameLine (0.0f, 0.0f); - } + bool d1 = DeleteFile ((targetPath + fileName + L".png").c_str()), + d2 = DeleteFile ((targetPath + fileName + L".jpg").c_str()), + d3 = false, + d4 = false; - ImGui::TextColored ( ImColor::HSV(0.0f, 0.0f, 0.75f), // ImColor(53, 255, 3) - "%s", curSearch.c_str () - ); + // For Xbox titles we also store a fallback cover that we must reset + if (! pApp->tex_cover.isCustom && pApp->store == "Xbox") + { + fileName = L"cover-fallback.png"; + d3 = DeleteFile ((targetPath + fileName + L".png").c_str()), + d4 = DeleteFile ((targetPath + fileName + L".jpg").c_str()); + } - if (! postSearch.empty ()) + // If any file was removed + if (d1 || d2 || d3 || d4) + { + update = true; + lastCover.reset(); // Needed as otherwise SKIF would not reload the cover + } + } + } + else { - ImGui::SameLine (0.0f, 0.0f); - ImGui::TextDisabled ("%s", postSearch.c_str ()); + SKIF_ImGui_SetMouseCursorHand ( ); } - - ImGui::EndPopup ( ); } - } - if ( dwLastUpdate != MAXDWORD && - SKIF_Util_timeGetTime () - dwLastUpdate > - dwTimeout ) - { - if (result.app_id != 0) + ImGui::Separator ( ); + + auto _GetSteamGridDBLink = [&](void) -> std::string { - *test_ = '\0'; - dwLastUpdate = MAXDWORD; - if (result.app_id != pApp->id || - result.store != pApp->store) + // Strip (recently added) from the game name + std::string name = pApp->names.normal; + try { + name = std::regex_replace(name, std::regex(R"( \(recently added\))"), ""); + } + catch (const std::exception& e) { - manual_selection.id = result.app_id; - manual_selection.store = result.store; + UNREFERENCED_PARAMETER(e); } - result = { }; - } - } - }; - if (AddGamePopup == PopupState_Closed && - ModifyGamePopup == PopupState_Closed && - RemoveGamePopup == PopupState_Closed && - ! io.KeyCtrl) - _HandleKeyboardInput (); + return (pApp->store == "Steam") + ? SK_FormatString("https://www.steamgriddb.com/steam/%lu/grids", pApp->id) + : SK_FormatString("https://www.steamgriddb.com/search/grids?term=%s", name.c_str()); -#pragma region PrintInjectionSummary + }; - auto _PrintInjectionSummary = [&](app_record_s* pTargetApp) -> void - { - if ( pTargetApp != nullptr && pTargetApp->id != SKIF_STEAM_APPID ) - { - struct summary_cache_s { - struct { - std::string type; - std::string type_version; - struct { - std::string text; - ImColor color; - ImColor color_hover; - } status; - std::string hover_text; - } injection; - std::string config_repo; - struct { - std::string shorthand; // Converted to utf-8 from utf-16 - std::string root_dir; // Converted to utf-8 from utf-16 - std::string full_path; // Converted to utf-8 from utf-16 - } config; - struct { - std::string shorthand; // Converted to utf-8 from utf-16 - std::string version; // Converted to utf-8 from utf-16 - std::string full_path; // Converted to utf-8 from utf-16 - } dll; - AppId_t app_id = 0; - DWORD running = 0; - bool service = false; - bool autostop = false; - } static cache; - - if ( cache.service != _inject.bCurrentState || - cache.running != pTargetApp->_status.running || - cache.autostop != _inject.bAckInj - ) + if (ImGui::Selectable ("Browse SteamGridDB", dontCare, ImGuiSelectableFlags_SpanAllColumns)) { - cache.app_id = 0; + SKIF_Util_OpenURI (SK_UTF8ToWideChar(_GetSteamGridDBLink()).c_str()); } - - if (pTargetApp->id != cache.app_id) + else { - cache.app_id = pTargetApp->id; - cache.running = pTargetApp->_status.running; - cache.autostop = _inject.bAckInj; + SKIF_ImGui_SetMouseCursorHand ( ); + SKIF_ImGui_SetHoverText (_GetSteamGridDBLink()); + } - cache.service = (pTargetApp->specialk.injection.injection.bitness == InjectionBitness::ThirtyTwo && _inject.pid32) || - (pTargetApp->specialk.injection.injection.bitness == InjectionBitness::SixtyFour && _inject.pid64) || - (pTargetApp->specialk.injection.injection.bitness == InjectionBitness::Unknown && (_inject.pid32 && - _inject.pid64)); + ImGui::EndGroup ( ); - sk_install_state_s& sk_install = - pTargetApp->specialk.injection; + ImGui::SetCursorPos (iconPos); - wchar_t wszDLLPath [MAX_PATH]; - wcsncpy_s ( wszDLLPath, MAX_PATH, - sk_install.injection.dll_path.c_str (), - _TRUNCATE ); + ImGui::TextColored ( + (_registry.iStyle == 2) ? ImColor (0, 0, 0) : ImColor (255, 255, 255), + ICON_FA_FILE_IMAGE + ); - cache.dll.full_path = SK_WideCharToUTF8 (wszDLLPath); + if (pApp->tex_cover.isCustom) + ImGui::TextColored ( + (_registry.iStyle == 2) ? ImColor (0, 0, 0) : ImColor (255, 255, 255), + ICON_FA_ROTATE_LEFT + ); + else if (pApp->tex_cover.isManaged) + ImGui::TextColored ( + (_registry.iStyle == 2) ? ImColor (0, 0, 0) : ImColor (255, 255, 255), + ICON_FA_ROTATE + ); - PathStripPathW ( wszDLLPath); - cache.dll.shorthand = SK_WideCharToUTF8 (wszDLLPath); - cache.dll.version = SK_WideCharToUTF8 (sk_install.injection.dll_ver); + ImGui::Separator ( ); - wchar_t wszConfigPath [MAX_PATH]; - wcsncpy_s ( wszConfigPath, MAX_PATH, - sk_install.config.file.c_str (), - _TRUNCATE ); + ImGui::TextColored ( + ImGui::GetStyleColorVec4(ImGuiCol_SKIF_Info), + ICON_FA_UP_RIGHT_FROM_SQUARE + ); - auto& cfg = cache.config; - cfg.root_dir = SK_WideCharToUTF8 (sk_install.config.dir); - cfg.full_path - = SK_WideCharToUTF8 (wszConfigPath); - PathStripPathW ( wszConfigPath); - cfg.shorthand = SK_WideCharToUTF8 (wszConfigPath); + } - //if (! PathFileExistsW (sk_install.config.file.c_str ())) - // cfg.shorthand.clear (); + ImGui::EndPopup ( ); + } - //if (! PathFileExistsA (cache.dll.full_path.c_str ())) - // cache.dll.shorthand.clear (); + float fY = + ImGui::GetCursorPosY ( ); - cache.injection.type = "None"; - cache.injection.status.text.clear (); - cache.injection.hover_text.clear (); + ImGui::EndGroup ( ); + ImGui::SameLine (0.0f, 3.0f * SKIF_ImGui_GlobalDPIScale); // 0.0f, 0.0f + + float fZ = + ImGui::GetCursorPosX ( ); - switch (sk_install.injection.type) - { - case sk_install_state_s::Injection::Type::Local: - cache.injection.type = "Local"; - cache.injection.type_version = SK_FormatString (R"(%s v %s (%s))", cache.injection.type.c_str(), cache.dll.version.c_str(), cache.dll.shorthand.c_str()); - break; + if (update) + { + update = false; - case sk_install_state_s::Injection::Type::Global: - default: // Unknown injection strategy, but let's assume global would work + // Ensure we aren't already loading this cover + if (lastCover.appid != pApp->id || lastCover.store != pApp->store) + { + loadCover = true; + lastCover.appid = pApp->id; + lastCover.store = pApp->store; + } + } - if ( _inject.bHasServlet ) - { - cache.injection.type = "Global"; - cache.injection.type_version = SK_FormatString (R"(%s v %s)", cache.injection.type.c_str(), cache.dll.version.c_str()); - cache.injection.status.text = - (cache.service) ? (_inject.bAckInj) ? "Waiting for game..." : "Running" - : " "; //"Service Status"; - - cache.injection.status.color = - (cache.service) ? ImGui::GetStyleColorVec4(ImGuiCol_SKIF_Success) // HSV (0.3F, 0.99F, 1.F) - : ImGui::GetStyleColorVec4(ImGuiCol_SKIF_Info); // HSV (0.08F, 0.99F, 1.F); - cache.injection.status.color_hover = - (cache.service) ? ImGui::GetStyleColorVec4(ImGuiCol_SKIF_Success) * ImVec4(0.8f, 0.8f, 0.8f, 1.0f) - : ImGui::GetStyleColorVec4(ImGuiCol_SKIF_Info) * ImVec4(0.8f, 0.8f, 0.8f, 1.0f); - cache.injection.hover_text = - (cache.service) ? "Click to stop the service" - : "Click to start the service"; - } - break; - } + if (loadCover && populated) // && ! InterlockedExchangeAdd (&icon_thread, 0)) /* && (ImGui::GetCurrentWindowRead()->HiddenFramesCannotSkipItems == 0) */ + { // Load cover first after the window has been shown -- to fix one copy leaking of the cover + // 2023-03-24: Is this even needed any longer after fixing the double-loading that was going on? + // 2023-03-25: Disabled HiddenFramesCannotSkipItems check to see if it's solved. + // 2023-10-05: Disabled waiting for the icon thread as well + loadCover = false; - switch (sk_install.config.type) - { - case ConfigType::Centralized: - cache.config_repo = "Centralized"; break; - case ConfigType::Localized: - cache.config_repo = "Localized"; break; - default: - cache.config_repo = "Unknown"; - cache.config.shorthand.clear (); break; - } - } + // Reset variables used to track whether we're still loading a game cover, or if we're missing one + gameCoverLoading.store (true); + tryingToLoadCover = true; + queuePosGameCover = textureLoadQueueLength.load() + 1; - auto __IsOutdatedLocalDLLFile = [&](void) -> bool - { - bool ret = false; - - if ((pTargetApp->store != "Steam") || - (pTargetApp->store == "Steam" && // Exclude the check for games with known older versions - cache.app_id != 405900 && // Disgaea PC - cache.app_id != 359870 && // FFX/X-2 HD Remaster - //cache.app_id != 578330 && // LEGO City Undercover // Do not exclude from the updater as its a part of mainline SK - cache.app_id != 429660 && // Tales of Berseria - cache.app_id != 372360 && // Tales of Symphonia - cache.app_id != 738540 && // Tales of Vesperia DE - cache.app_id != 351970 // Tales of Zestiria: - )) - { - if (SKIF_Util_CompareVersionStrings (SK_UTF8ToWideChar(_inject.SKVer32), SK_UTF8ToWideChar(cache.dll.version)) > 0) - { - ret = true; - } - } - - return ret; - }; +//#define _WRITE_APPID_INI +#ifdef _WRITE_APPID_INI + if ( appinfo != nullptr && pApp->store == "Steam") + { + skValveDataFile::appinfo_s *pAppInfo = + appinfo->getAppInfo ( pApp->id ); - auto __UpdateLocalDLLFile = [&](void) -> void - { - int iBinaryType = SKIF_Util_GetBinaryType (SK_UTF8ToWideChar (cache.dll.full_path).c_str()); - if (iBinaryType > 0) - { - wchar_t wszPathToGlobalDLL [MAX_PATH + 2] = { }; - GetModuleFileNameW (nullptr, wszPathToGlobalDLL, MAX_PATH); - PathRemoveFileSpecW ( wszPathToGlobalDLL); - PathAppendW ( wszPathToGlobalDLL, (iBinaryType == 2) ? L"SpecialK64.dll" : L"SpecialK32.dll"); + DBG_UNREFERENCED_LOCAL_VARIABLE (pAppInfo); + } +#endif - if (CopyFile (wszPathToGlobalDLL, SK_UTF8ToWideChar (cache.dll.full_path).c_str(), FALSE)) - { - PLOG_INFO << "Successfully updated " << SK_UTF8ToWideChar (cache.dll.full_path) << " from v " << SK_UTF8ToWideChar (cache.dll.version) << " to v " << SK_UTF8ToWideChar (_inject.SKVer32); + //PLOG_VERBOSE << "ImGui Frame Counter: " << ImGui::GetFrameCount(); - // Force an update of the injection strategy - updateInjStrat = true; - cache.app_id = 0; - } +#pragma region SKIF_LibCoverWorker - else { - PLOG_ERROR << "Failed to copy " << wszPathToGlobalDLL << " to " << SK_UTF8ToWideChar (cache.dll.full_path); - PLOG_ERROR << SKIF_Util_GetErrorAsWStr(); - } - } + // We're going to stream the cover in asynchronously on this thread + _beginthread ([](void*)->void + { + CoInitializeEx (nullptr, 0x0); - else { - PLOG_ERROR << "Failed to retrieve binary type from " << SK_UTF8ToWideChar (cache.dll.full_path) << " -- returned: " << iBinaryType; - PLOG_ERROR << SKIF_Util_GetErrorAsWStr(); - } - }; + SKIF_Util_SetThreadDescription (GetCurrentThread (), L"SKIF_LibCoverWorker"); - static constexpr float - num_lines = 4.0f; - auto line_ht = - ImGui::GetTextLineHeightWithSpacing (); - - auto frame_id = - ImGui::GetID ("###Injection_Summary_Frame"); - - SKIF_ImGui_BeginChildFrame ( frame_id, - ImVec2 ( _WIDTH - ImGui::GetStyle ().FrameBorderSize * 2.0f, - num_lines * line_ht ), - ImGuiWindowFlags_NavFlattened | - ImGuiWindowFlags_NoScrollbar | - ImGuiWindowFlags_NoScrollWithMouse | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoBackground - ); + PLOG_INFO << "Thread started!"; + PLOG_INFO << "Streaming game cover asynchronously..."; - ImGui::BeginGroup (); - - // Column 1 - ImGui::BeginGroup (); - ImGui::PushStyleColor (ImGuiCol_Text, ImVec4 (0.5f, 0.5f, 0.5f, 1.f)); - //ImGui::NewLine (); - ImGui::TextUnformatted ("Injection:"); - ImGui::TextUnformatted ("Config Root:"); - ImGui::TextUnformatted ("Config File:"); - ImGui::TextUnformatted ("Platform:"); - ImGui::PopStyleColor (); - ImGui::ItemSize (ImVec2 (110.f * SKIF_ImGui_GlobalDPIScale, - 0.f) - ); // Column should have min-width 130px (scaled with the DPI) - ImGui::EndGroup (); - - ImGui::SameLine (); - - // Column 2 - ImGui::BeginGroup (); - - // Injection - if (! cache.dll.shorthand.empty ()) + if (pApp == nullptr) { - //ImGui::TextUnformatted (cache.dll.shorthand.c_str ()); - ImGuiSelectableFlags flags = ImGuiSelectableFlags_AllowItemOverlap; + PLOG_ERROR << "Aborting due to pApp being a nullptr!"; + return; + } - if (cache.injection.type._Equal("Global")) - flags |= ImGuiSelectableFlags_Disabled; + app_record_s* _pApp = pApp; - bool openLocalMenu = false; + int queuePos = getTextureLoadQueuePos(); + //PLOG_VERBOSE << "queuePos = " << queuePos; - ImGui::PushStyleColor(ImGuiCol_TextDisabled, ImGui::GetStyleColorVec4(ImGuiCol_SKIF_TextCaption)); - if (ImGui::Selectable (cache.injection.type_version.c_str(), false, flags)) - { - openLocalMenu = true; - } - ImGui::PopStyleColor(); + static ImVec2 _vecCoverUv0(vecCoverUv0); + static ImVec2 _vecCoverUv1(vecCoverUv1); + static CComPtr _pTexSRV (pTexSRV.p); - if (cache.injection.type._Equal("Local")) - { - SKIF_ImGui_SetMouseCursorHand ( ); + // Most textures are pushed to be released by LoadLibraryTexture(), + // however the current cover pointer is only updated to the new one + // *after* the old cover has been pushed to be released. + // + // This means there's a short thread race where the main thread + // can still reference a texture that has already been released. + // + // As a result, we preface the whole loading of the new cover texture + // by explicitly changing the current cover texture to point to nothing. + // + // The only downside is that the cover transition is not seemless; + // a black/non-existent cover will be displayed in-between. + // + // But at least SKIF does not run the risk of crashing as often :) + pTexSRV = nullptr; - if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) - openLocalMenu = true; - } + std::wstring load_str; - SKIF_ImGui_SetHoverText (cache.dll.full_path.c_str ()); - - if (openLocalMenu && ! ImGui::IsPopupOpen ("LocalDLLMenu")) - ImGui::OpenPopup ("LocalDLLMenu"); + // SKIF + if (_pApp->id == SKIF_STEAM_APPID) + { + // No need to change the string in any way + } - if (ImGui::BeginPopup ("LocalDLLMenu", ImGuiWindowFlags_NoMove)) - { - if (__IsOutdatedLocalDLLFile ( )) - { - if (ImGui::Selectable (("Update to v " + _inject.SKVer32).c_str( ))) - { - __UpdateLocalDLLFile ( ); - } + // SKIF Custom + else if (_pApp->store == "Other") + { + load_str = L"cover"; + } - ImGui::Separator ( ); - } + // GOG + else if (_pApp->store == "GOG") + { + load_str = L"*_glx_vertical_cover.webp"; + } + + // Epic + else if (_pApp->store == "Epic") + { + load_str = + SK_FormatStringW (LR"(%ws\Assets\EGS\%ws\cover-original.jpg)", _path_cache.specialk_userdata, SK_UTF8ToWideChar(_pApp->Epic_AppName).c_str()); - if (ImGui::Selectable ("Uninstall")) + if ( ! PathFileExistsW (load_str. c_str ()) ) + { + SKIF_Epic_IdentifyAssetNew (_pApp->Epic_CatalogNamespace, _pApp->Epic_CatalogItemId, _pApp->Epic_AppName, _pApp->Epic_DisplayName); + } + + else { + // If the file exist, load the metadata from the local image, but only if low bandwidth mode is not enabled + if ( ! _registry.bLowBandwidthMode && + SUCCEEDED ( + DirectX::GetMetadataFromWICFile ( + load_str.c_str (), + DirectX::WIC_FLAGS_FILTER_POINT, + meta + ) + ) + ) { - - if (DeleteFile (SK_UTF8ToWideChar(cache.dll.full_path).c_str())) + // If the image is in reality 600 in width or 900 in height, which indicates a low-res cover, + // download the full-size cover and replace the existing one. + if (meta.width == 600 || + meta.height == 900) { - PLOG_INFO << "Successfully uninstalled local DLL v " << SK_UTF8ToWideChar(cache.dll.version) << " from " << SK_UTF8ToWideChar(cache.dll.full_path); - - // Force an update of the injection strategy - updateInjStrat = true; - cache.app_id = 0; + SKIF_Epic_IdentifyAssetNew (_pApp->Epic_CatalogNamespace, _pApp->Epic_CatalogItemId, _pApp->Epic_AppName, _pApp->Epic_DisplayName); } } - - ImGui::EndPopup ( ); } } - else - ImGui::TextUnformatted ("N/A"); - - // Config Root - // Config File - if (! cache.config.shorthand.empty ()) + // Xbox + else if (_pApp->store == "Xbox") { - // Config Root - if (ImGui::Selectable (cache.config_repo.c_str ())) - { - std::wstring wsRootDir = - SK_UTF8ToWideChar (cache.config.root_dir); - - std::error_code ec; - // Create any missing directories - if (! std::filesystem::exists ( wsRootDir, ec)) - std::filesystem::create_directories (wsRootDir, ec); - - SKIF_Util_ExplorePath (wsRootDir); - } - SKIF_ImGui_SetMouseCursorHand (); - SKIF_ImGui_SetHoverText (cache.config.root_dir.c_str ()); + load_str = + SK_FormatStringW (LR"(%ws\Assets\Xbox\%ws\cover-original.png)", _path_cache.specialk_userdata, SK_UTF8ToWideChar(_pApp->Xbox_PackageName).c_str()); - // Config File - if (ImGui::Selectable (cache.config.shorthand.c_str ())) + if ( ! PathFileExistsW (load_str. c_str ()) ) { - std::wstring wsRootDir = - SK_UTF8ToWideChar (cache.config.root_dir); - - std::error_code ec; - // Create any missing directories - if (! std::filesystem::exists ( wsRootDir, ec)) - std::filesystem::create_directories (wsRootDir, ec); - - HANDLE h = CreateFile ( SK_UTF8ToWideChar (cache.config.full_path).c_str(), - GENERIC_READ | GENERIC_WRITE, - FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, - NULL, - CREATE_NEW, - FILE_ATTRIBUTE_NORMAL, - NULL ); - - // We need to close the handle as well, as otherwise Notepad will think the file - // is still in use (trigger Save As dialog on Save) until SKIF gets closed - if (h != INVALID_HANDLE_VALUE) - CloseHandle (h); - - SKIF_Util_OpenURI (SK_UTF8ToWideChar (cache.config.full_path).c_str(), SW_SHOWNORMAL, NULL); + SKIF_Xbox_IdentifyAssetNew (_pApp->Xbox_PackageName, _pApp->Xbox_StoreId); } - SKIF_ImGui_SetMouseCursorHand (); - SKIF_ImGui_SetHoverText (cache.config.full_path.c_str ()); - - - if ( ! ImGui::IsPopupOpen ("ConfigFileMenu") && - ImGui::IsItemClicked (ImGuiMouseButton_Right)) - ImGui::OpenPopup ("ConfigFileMenu"); - - if (ImGui::BeginPopup ("ConfigFileMenu", ImGuiWindowFlags_NoMove)) - { - ImGui::TextColored ( - ImColor::HSV (0.11F, 1.F, 1.F), - "Troubleshooting:" - ); - - ImGui::Separator ( ); - - struct Preset - { - std::string Name; - std::wstring Path; - - Preset (std::wstring n, std::wstring p) - { - Name = SK_WideCharToUTF8 (n); - Path = p; - }; - }; - - // Static stuff :D - static SKIF_DirectoryWatch SKIF_GlobalWatch; - static SKIF_DirectoryWatch SKIF_CustomWatch; - static std::vector DefaultPresets; - static std::vector CustomPresets; - static bool runOnceDefaultPresets = true; - static bool runOnceCustomPresets = true; - - // Directory watches -- updates the vectors automatically - if (SKIF_GlobalWatch.isSignaled (LR"(Global)", false) || runOnceDefaultPresets) + + else { + // If the file exist, load the metadata from the local image, but only if low bandwidth mode is not enabled + if ( ! _registry.bLowBandwidthMode && + SUCCEEDED ( + DirectX::GetMetadataFromWICFile ( + load_str.c_str (), + DirectX::WIC_FLAGS_FILTER_POINT, + meta + ) + ) + ) { - runOnceDefaultPresets = false; - - HANDLE hFind = INVALID_HANDLE_VALUE; - WIN32_FIND_DATA ffd; - std::vector tmpPresets; - std::wstring PresetFolder = SK_FormatStringW (LR"(%ws\Global\)", _path_cache.specialk_userdata); - - hFind = FindFirstFile((PresetFolder + L"default_*.ini").c_str(), &ffd); - - if (INVALID_HANDLE_VALUE != hFind) + // If the image is in reality 600 in width or 900 in height, which indicates a low-res cover, + // download the full-size cover and replace the existing one. + if (meta.width == 600 || + meta.height == 900) { - do { - Preset newPreset = { PathFindFileName(ffd.cFileName), SK_FormatStringW (LR"(%ws\Global\%ws)", _path_cache.specialk_userdata, ffd.cFileName) }; - tmpPresets.push_back(newPreset); - } while (FindNextFile (hFind, &ffd)); - - DefaultPresets = tmpPresets; - FindClose (hFind); + SKIF_Xbox_IdentifyAssetNew (_pApp->Xbox_PackageName, _pApp->Xbox_StoreId); } } + } + } - if (SKIF_CustomWatch.isSignaled (LR"(Global\Custom)", false) || runOnceCustomPresets) - { - runOnceCustomPresets = false; - - HANDLE hFind = INVALID_HANDLE_VALUE; - WIN32_FIND_DATA ffd; - std::vector tmpPresets; - std::wstring PresetFolder = SK_FormatStringW (LR"(%ws\Global\Custom\)", _path_cache.specialk_userdata); - - hFind = FindFirstFile((PresetFolder + L"*.ini").c_str(), &ffd); + // Steam + else if (_pApp->store == "Steam") + { + std::wstring load_str_2x ( + SK_FormatStringW (LR"(%ws\Assets\Steam\%i\)", _path_cache.specialk_userdata, _pApp->id) + ); - if (INVALID_HANDLE_VALUE != hFind) - { - do { - Preset newPreset = { PathFindFileName(ffd.cFileName), SK_FormatStringW (LR"(%ws\Global\Custom\%ws)", _path_cache.specialk_userdata, ffd.cFileName) }; - tmpPresets.push_back(newPreset); - } while (FindNextFile (hFind, &ffd)); + std::error_code ec; + // Create any missing directories + if (! std::filesystem::exists ( load_str_2x, ec)) + std::filesystem::create_directories (load_str_2x, ec); - CustomPresets = tmpPresets; - FindClose (hFind); - } - } - - if ((! DefaultPresets.empty() || ! CustomPresets.empty())) - { - if (ImGui::BeginMenu("Apply Preset")) - { - // Default Presets - if (! DefaultPresets.empty()) - { - for (auto& preset : DefaultPresets) - { - if (ImGui::Selectable (preset.Name.c_str())) - { - CopyFile (preset.Path.c_str(), SK_UTF8ToWideChar(cache.config.full_path).c_str(), FALSE); - PLOG_VERBOSE << "Copying " << preset.Path << " over to " << SK_UTF8ToWideChar(cache.config.full_path) << ", overwriting any existing file in the process."; - } - } + load_str_2x += L"cover-original.jpg"; + + load_str = _path_cache.steam_install; - if (! CustomPresets.empty()) - ImGui::Separator ( ); - } + load_str += LR"(/appcache/librarycache/)" + + std::to_wstring (_pApp->id) + + L"_library_600x900.jpg"; - // Custom Presets - if (! CustomPresets.empty()) - { - for (auto& preset : CustomPresets) - { - if (ImGui::Selectable (preset.Name.c_str())) - { - CopyFile (preset.Path.c_str(), SK_UTF8ToWideChar(cache.config.full_path).c_str(), FALSE); - PLOG_VERBOSE << "Copying " << preset.Path << " over to " << SK_UTF8ToWideChar(cache.config.full_path) << ", overwriting any existing file in the process."; - } - } - } + std::wstring load_str_final = load_str; - ImGui::EndMenu ( ); - } + // Get UNIX-style time + time_t ltime; + time (<ime); - ImGui::Separator ( ); - } + std::wstring url = L"https://steamcdn-a.akamaihd.net/steam/apps/"; + url += std::to_wstring (_pApp->id); + url += L"/library_600x900_2x.jpg"; + url += L"?t="; + url += std::to_wstring (ltime); // Add UNIX-style timestamp to ensure we don't get anything cached - if (ImGui::Selectable ("Apply Compatibility Config")) + // If 600x900 exists but 600x900_x2 cannot be found + if ( PathFileExistsW (load_str. c_str ()) && + ! PathFileExistsW (load_str_2x.c_str ()) ) + { + // Load the metadata from 600x900, but only if low bandwidth mode is not enabled + if ( ! _registry.bLowBandwidthMode && + SUCCEEDED ( + DirectX::GetMetadataFromWICFile ( + load_str.c_str (), + DirectX::WIC_FLAGS_FILTER_POINT, + meta + ) + ) + ) { - std::wofstream config_file(SK_UTF8ToWideChar (cache.config.full_path).c_str()); - - if (config_file.is_open()) + // If the image is in reality 300x450, which indicates a real cover, + // download the real 600x900 cover and store it in _x2 + if (meta.width == 300 && + meta.height == 450) { - // Static const as this profile never changes - static const std::wstring out_text = -LR"([SpecialK.System] -ShowEULA=false -GlobalInjectDelay=0.0 - -[API.Hook] -d3d9=true -d3d9ex=true -d3d11=true -OpenGL=true -d3d12=true -Vulkan=true - -[Steam.Log] -Silent=true - -[Input.libScePad] -Enable=false - -[Input.XInput] -Enable=false - -[Input.Gamepad] -EnableDirectInput7=false -EnableDirectInput8=false -EnableHID=false -EnableNativePS4=false -EnableRawInput=true -AllowHapticUI=false - -[Input.Keyboard] -CatchAltF4=false -BypassAltF4Handler=false - -[Textures.D3D11] -Cache=false)"; - - config_file.write(out_text.c_str(), - out_text.length()); - - config_file.close(); + SKIF_Util_GetWebResource (url, load_str_2x); + load_str_final = load_str_2x; } } + } - SKIF_ImGui_SetHoverTip ("Known as the \"sledgehammer\" config within the community as it disables\n" - "various features of Special K in an attempt to improve compatibility."); + // If 600x900_x2 exists, check the last modified time stamps + else { + WIN32_FILE_ATTRIBUTE_DATA faX1{}, faX2{}; - if (ImGui::Selectable ("Reset")) + // ... but only if low bandwidth mode is disabled + if (! _registry.bLowBandwidthMode && + GetFileAttributesEx (load_str .c_str (), GetFileExInfoStandard, &faX1) && + GetFileAttributesEx (load_str_2x.c_str (), GetFileExInfoStandard, &faX2)) { - HANDLE h = CreateFile ( SK_UTF8ToWideChar (cache.config.full_path).c_str(), - GENERIC_READ | GENERIC_WRITE, - FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, - NULL, - TRUNCATE_EXISTING, - FILE_ATTRIBUTE_NORMAL, - NULL ); - - // We need to close the handle as well, as otherwise Notepad will think the file - // is still in use (trigger Save As dialog on Save) until SKIF gets closed - if (h != INVALID_HANDLE_VALUE) - CloseHandle (h); + // If 600x900 has been edited after 600_900_x2, + // download new copy of the 600_900_x2 cover + if (CompareFileTime (&faX1.ftLastWriteTime, &faX2.ftLastWriteTime) == 1) + { + DeleteFile (load_str_2x.c_str ()); + SKIF_Util_GetWebResource (url, load_str_2x); + } } - - ImGui::EndPopup ( ); + + // If 600x900_x2 exists now, load it + if (PathFileExistsW (load_str_2x.c_str ())) + load_str_final = load_str_2x; } - } - else - { - ImGui::TextUnformatted (cache.config_repo.c_str ()); - ImGui::TextUnformatted ("N/A"); + load_str = load_str_final; } + + LoadLibraryTexture ( LibraryTexture::Cover, + _pApp->id, + _pTexSRV, + load_str, + _vecCoverUv0, + _vecCoverUv1, + _pApp); - // Platform - ImGui::TextUnformatted (pTargetApp->store.c_str()); - - // Column should have min-width 100px (scaled with the DPI) - ImGui::ItemSize ( - ImVec2 ( 130.0f * SKIF_ImGui_GlobalDPIScale, - 0.0f - ) ); - ImGui::EndGroup ( ); - ImGui::SameLine ( ); - - // Column 3 - ImGui::BeginGroup ( ); + PLOG_VERBOSE << "_pTexSRV = " << _pTexSRV; - static bool quickServiceHover = false; + int currentQueueLength = textureLoadQueueLength.load(); - // Service quick toogle / Waiting for game... - if (cache.injection.type._Equal ("Global") && ! _inject.isPending()) + if (currentQueueLength == queuePos) { - ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImColor(0, 0, 0, 0).Value); - ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImColor(0, 0, 0, 0).Value); - ImGui::PushStyleColor(ImGuiCol_Text, (quickServiceHover) ? cache.injection.status.color_hover.Value - : cache.injection.status.color.Value); - - if (ImGui::Selectable (cache.injection.status.text.c_str(), false, ImGuiSelectableFlags_SpanAllColumns)) - { - _inject._StartStopInject (cache.service, _registry.bStopOnInjection, pApp->launch_configs[0].isElevated(pApp->id)); - - cache.app_id = 0; - } - - ImGui::PopStyleColor (3); - - quickServiceHover = ImGui::IsItemHovered (); - - SKIF_ImGui_SetMouseCursorHand (); - SKIF_ImGui_SetHoverTip ( - cache.injection.hover_text.c_str () - ); + PLOG_DEBUG << "Texture is live! Swapping it in."; + vecCoverUv0 = _vecCoverUv0; + vecCoverUv1 = _vecCoverUv1; + pTexSRV = _pTexSRV; - if ( ! ImGui::IsPopupOpen ("ServiceMenu") && - ImGui::IsItemClicked (ImGuiMouseButton_Right)) - ServiceMenu = PopupState_Open; - } + // Indicate that we have stopped loading the cover + gameCoverLoading.store (false); - else { - ImGui::NewLine ( ); + // Force a refresh when the cover has been swapped in + PostMessage (SKIF_Notify_hWnd, WM_SKIF_COVER, 0x0, 0x0); } - - if (cache.injection.type._Equal ("Local")) + else if (_pTexSRV.p != nullptr) { - if (__IsOutdatedLocalDLLFile ( )) - { - ImGui::SameLine ( ); - - ImGui::PushStyleColor (ImGuiCol_Button, ImVec4 (.1f, .1f, .1f, .5f)); - if (ImGui::SmallButton (ICON_FA_ARROW_UP)) - { - __UpdateLocalDLLFile ( ); - } - ImGui::PopStyleColor ( ); - - SKIF_ImGui_SetHoverTip (("The local DLL file is outdated.\n" - "Click to update it to v " + _inject.SKVer32 + ".")); - } + PLOG_DEBUG << "Texture is late! (" << queuePos << " vs " << currentQueueLength << ")"; + extern concurrency::concurrent_queue > SKIF_ResourcesToFree; + PLOG_VERBOSE << "SKIF_ResourcesToFree: Pushing " << _pTexSRV.p << " to be released";; + SKIF_ResourcesToFree.push(_pTexSRV.p); + _pTexSRV.p = nullptr; } - ImGui::EndGroup (); - - // End of columns - ImGui::EndGroup (); - - ImGui::EndChildFrame (); - - ImGui::Separator (); + PLOG_INFO << "Finished streaming game cover asynchronously..."; + PLOG_INFO << "Thread stopped!"; - auto frame_id2 = - ImGui::GetID ("###Injection_Play_Button_Frame"); + }, 0x0, NULL); - ImGui::PushStyleVar ( - ImGuiStyleVar_FramePadding, - ImVec2 ( 120.0f * SKIF_ImGui_GlobalDPIScale, - 40.0f * SKIF_ImGui_GlobalDPIScale) - ); +#pragma endregion + } - SKIF_ImGui_BeginChildFrame ( - frame_id2, ImVec2 ( 0.0f, - 110.f * SKIF_ImGui_GlobalDPIScale ), - ImGuiWindowFlags_NavFlattened | - ImGuiWindowFlags_NoScrollbar | - ImGuiWindowFlags_NoScrollWithMouse | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoBackground - ); + ImGui::BeginGroup (); - ImGui::PopStyleVar (); + auto _HandleKeyboardInput = [&](void) + { + static auto + constexpr _text_chars = + { 'A','B','C','D','E','F','G','H', + 'I','J','K','L','M','N','O','P', + 'Q','R','S','T','U','V','W','X', + 'Y','Z','0','1','2','3','4','5', + '6','7','8','9',' ','-',':','.' }; - std::string buttonLabel = ICON_FA_GAMEPAD " Launch ";// + pTargetApp->type; - ImGuiButtonFlags buttonFlags = ImGuiButtonFlags_None; + static char test_ [1024] = { }; + char out [2] = { 0, 0 }; + bool bText = false; - if (pTargetApp->_status.running) + for ( auto c : _text_chars ) + { + if (io.KeysDownDuration [c] == 0.0f && + (c != ' ' || strlen (test_) > 0)) { - buttonLabel = "Running..."; - buttonFlags = ImGuiButtonFlags_Disabled; - ImGui::PushStyleColor (ImGuiCol_Button, ImGui::GetStyleColorVec4 (ImGuiCol_Button) * ImVec4 (0.75f, 0.75f, 0.75f, 1.0f)); + out [0] = c; + StrCatA (test_, out); + bText = true; } + } - // Disable the button for global injection types if the servlets are missing - if ( ! _inject.bHasServlet && ! cache.injection.type._Equal ("Local") ) - SKIF_ImGui_PushDisableState ( ); - - // This captures two events -- launching through context menu + large button - if ( ImGui::ButtonEx ( - buttonLabel.c_str (), - ImVec2 ( 150.0f * SKIF_ImGui_GlobalDPIScale, - 50.0f * SKIF_ImGui_GlobalDPIScale ), buttonFlags ) - || - clickedGameLaunch - || - clickedGameLaunchWoSK ) - { - - if ( pTargetApp->store != "Steam" && pTargetApp->store != "Epic" && - pTargetApp->launch_configs[0].getExecutableFullPath(pApp->id).find(L"InvalidPath") != std::wstring::npos ) - { - confirmPopupText = "Could not launch game due to missing executable:\n\n" + SK_WideCharToUTF8(pTargetApp->launch_configs[0].getExecutableFullPath(pApp->id, false)); - ConfirmPopup = PopupState_Open; - } + const DWORD dwTimeout = 850UL; // 425UL + static DWORD dwLastUpdate = SKIF_Util_timeGetTime (); - else { - bool usingSK = (cache.injection.type._Equal("Local")); + struct { + std::string text = ""; + std::string store = ""; + uint32_t app_id = 0; + size_t pos = 0; + size_t len = 0; + } static result; - // Check if Global injection should be used - if (! usingSK) + if (bText) + { + dwLastUpdate = SKIF_Util_timeGetTime (); + + // Prioritize trie search first + if (labels.search (test_)) + { + for (auto& app : apps) + { + if (app.second.names.all_upper_alnum.find (test_) == 0) { - std::string fullPath = SK_WideCharToUTF8(pTargetApp->launch_configs[0].getExecutableFullPath (pTargetApp->id)); - bool isLocalBlacklisted = pTargetApp->launch_configs[0].isBlacklisted (pTargetApp->id), - isGlobalBlacklisted = _inject._TestUserList (fullPath.c_str (), false); - - usingSK = ! clickedGameLaunchWoSK && - ! isLocalBlacklisted && - ! isGlobalBlacklisted; - - if (usingSK) - { - // Whitelist the path if it haven't been already - if (pTargetApp->store == "Xbox") - { - if (! _inject._TestUserList (SK_WideCharToUTF8 (pTargetApp->Xbox_AppDirectory).c_str(), true)) - { - if (_inject.WhitelistPattern (pTargetApp->Xbox_PackageName)) - _inject.SaveWhitelist ( ); - } - } - - else - { - if (_inject.WhitelistPath (fullPath)) - _inject.SaveWhitelist ( ); - } + result.text = app.second.names.normal; + result.store = app.second.store; + result.app_id = app.second.id; + result.pos = app.second.names.pre_stripped; + result.len = strlen (test_); - // Disable the first service notification - if (_registry.bMinimizeOnGameLaunch) - _registry._SuppressServiceNotification = true; - } + // Handle cases where articles are ignored - // Kickstart service if it is currently not running - if (! _inject.bCurrentState && usingSK ) - _inject._StartStopInject (false, true, pApp->launch_configs[0].isElevated(pApp->id)); + // Add one to the length if the regular all_upper cannot find a match + // as this indicates a stripped character in the found pattern + if (app.second.names.all_upper.find (test_) != 0) + result.len++; - // Stop the service if the user attempts to launch without SK - else if ( clickedGameLaunchWoSK && _inject.bCurrentState ) - _inject._StartStopInject (true); + break; } + } + } - // Create the injection acknowledge events in case of a local injection - else { - _inject.SetInjectAckEx (true); - _inject.SetInjectExitAckEx (true); - } + // Fall back to using free text search when the trie fails us + else + { + //strncpy (test_, result.text.c_str (), 1023); - // Launch game - if (pTargetApp->store == "GOG" && GOGGalaxy_Installed && _registry.bPreferGOGGalaxyLaunch && ! clickedGameLaunch && ! clickedGameLaunchWoSK) + for (auto& app : apps) + { + size_t + pos = app.second.names.all_upper.find (test_); + if (pos != std::string::npos ) // == 0 { - extern std::wstring GOGGalaxy_Path; - - // "D:\Games\GOG Galaxy\GalaxyClient.exe" /command=runGame /gameId=1895572517 /path="D:\Games\GOG Games\AI War 2" - - std::wstring launchOptions = SK_FormatStringW(LR"(/command=runGame /gameId=%d /path="%ws")", pApp->id, pApp->install_dir.c_str()); - - SKIF_Util_OpenURI (GOGGalaxy_Path, SW_SHOWDEFAULT, L"OPEN", launchOptions.c_str()); - - /* - SHELLEXECUTEINFOW - sexi = { }; - sexi.cbSize = sizeof (SHELLEXECUTEINFOW); - sexi.lpVerb = L"OPEN"; - sexi.lpFile = GOGGalaxy_Path.c_str(); - sexi.lpParameters = launchOptions.c_str(); - //sexi.lpDirectory = NULL; - sexi.nShow = SW_SHOWDEFAULT; - sexi.fMask = SEE_MASK_FLAG_NO_UI | - SEE_MASK_ASYNCOK | SEE_MASK_NOZONECHECKS; + result.text = app.second.names.normal; + result.store = app.second.store; + result.app_id = app.second.id; + result.pos = pos; + result.len = strlen (test_); - ShellExecuteExW (&sexi); - */ + break; } + } + } + } - else if (pTargetApp->store == "Epic") - { - // com.epicgames.launcher://apps/CatalogNamespace%3ACatalogItemId%3AAppName?action=launch&silent=true - SKIF_Util_OpenURI ((L"com.epicgames.launcher://apps/" + pTargetApp->launch_configs[0].launch_options + L"?action=launch&silent=true").c_str()); - } + if (! result.text.empty ()) + { + size_t len = + (result.len < result.text.length ( )) + ? result.len : result.text.length ( ); - else if (pTargetApp->store == "Steam") { - //SKIF_Util_OpenURI_Threaded ((L"steam://run/" + std::to_wstring(pTargetApp->id)).c_str()); // This is seemingly unreliable - SKIF_Util_OpenURI ((L"steam://run/" + std::to_wstring(pTargetApp->id)).c_str()); - pTargetApp->_status.invalidate(); - } - - else { // SKIF Custom, GOG without Galaxy, Xbox + std::string preSearch = result.text.substr ( 0, result.pos), + curSearch = result.text.substr (result.pos, len), + postSearch = (result.pos + len < result.text.length ( )) + ? result.text.substr (result.pos + len, std::string::npos) + : ""; - std::wstring wszPath = (pTargetApp->store == "Xbox") - ? pTargetApp->launch_configs[0].executable_helper - : pTargetApp->launch_configs[0].getExecutableFullPath(pTargetApp->id); - - SKIF_Util_OpenURI (wszPath, SW_SHOWDEFAULT, L"OPEN", pTargetApp->launch_configs[0].launch_options.c_str(), pTargetApp->launch_configs[0].working_dir.c_str()); + ImGui::OpenPopup ("###KeyboardHint"); - /* - SHELLEXECUTEINFOW - sexi = { }; - sexi.cbSize = sizeof (SHELLEXECUTEINFOW); - sexi.lpVerb = L"OPEN"; - sexi.lpFile = wszPath.c_str(); - sexi.lpParameters = pTargetApp->launch_configs[0].launch_options.c_str(); - sexi.lpDirectory = pTargetApp->launch_configs[0].working_dir .c_str(); - sexi.nShow = SW_SHOWDEFAULT; - sexi.fMask = SEE_MASK_FLAG_NO_UI | SEE_MASK_NOZONECHECKS; // SEE_MASK_ASYNCOK cannot be used since we are removing the environmental variable + ImGui::SetNextWindowPos (ImGui::GetCurrentWindowRead()->Viewport->GetMainRect().GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); - ShellExecuteExW (&sexi); - */ - } - - // Fallback for minimizing SKIF when not using SK if configured as such - if (_registry.bMinimizeOnGameLaunch && ! usingSK && SKIF_ImGui_hWnd != NULL) - ShowWindowAsync (SKIF_ImGui_hWnd, SW_SHOWMINNOACTIVE); + if (ImGui::BeginPopupModal("###KeyboardHint", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize)) + { + if (! preSearch.empty ()) + { + ImGui::TextDisabled ("%s", preSearch.c_str ()); + ImGui::SameLine (0.0f, 0.0f); } - clickedGameLaunch = clickedGameLaunchWoSK = false; - } - - // Disable the button for global injection types if the servlets are missing - if ( ! _inject.bHasServlet && ! cache.injection.type._Equal ("Local") ) - SKIF_ImGui_PopDisableState ( ); + ImGui::TextColored ( ImColor::HSV(0.0f, 0.0f, 0.75f), // ImColor(53, 255, 3) + "%s", curSearch.c_str () + ); - if (pTargetApp->_status.running) - ImGui::PopStyleColor ( ); + if (! postSearch.empty ()) + { + ImGui::SameLine (0.0f, 0.0f); + ImGui::TextDisabled ("%s", postSearch.c_str ()); + } - if (ImGui::IsItemClicked(ImGuiMouseButton_Right) && - ! openedGameContextMenu) - { - openedGameContextMenu = true; + ImGui::EndPopup ( ); } + } - ImGui::EndChildFrame (); - - // This needs to be run at the end of this statement to ensure there's no traces of the cache remaining on the next frame - if (selection.dir_watch.isSignaled()) + if ( dwLastUpdate != MAXDWORD && + SKIF_Util_timeGetTime () - dwLastUpdate > + dwTimeout ) + { + if (result.app_id != 0) { - updateInjStrat = true; // Force an update of the injection strategy for the app - cache.app_id = 0; // Force an update of the cached data on the next frame + *test_ = '\0'; + dwLastUpdate = MAXDWORD; + if (result.app_id != pApp->id || + result.store != pApp->store) + { + manual_selection.id = result.app_id; + manual_selection.store = result.store; + } + result = { }; } - - } // End IF (pTargetApp != nullptr) statement -- cache goes out of scope here + } }; -#pragma endregion - + if (AddGamePopup == PopupState_Closed && + ModifyGamePopup == PopupState_Closed && + RemoveGamePopup == PopupState_Closed && + ! io.KeyCtrl) + _HandleKeyboardInput (); // GamesList START #pragma region GamesList @@ -2514,30 +2626,30 @@ Cache=false)"; ImGui::PopStyleColor (2 ); static DWORD timeClicked = 0; - static uint32_t idClicked = 0; // Handle double click on a game row if ( ImGui::IsItemHovered ( ) && pApp != nullptr && pApp->id != SKIF_STEAM_APPID && ! pApp->_status.running ) { if ( ImGui::IsMouseDoubleClicked (ImGuiMouseButton_Left) && - timeClicked != 0 && idClicked == pApp->id) + timeClicked != 0 && (item_clicked.appid == pApp->id && item_clicked.store == pApp->store)) { - timeClicked = 0; - idClicked = 0; + timeClicked = 0; + item_clicked.reset ( ); clickedGameLaunch = true; } else if (ImGui::IsMouseClicked (ImGuiMouseButton_Left) ) { - timeClicked = SKIF_Util_timeGetTime ( ); - idClicked = pApp->id; + timeClicked = SKIF_Util_timeGetTime ( ); + item_clicked.appid = pApp->id; + item_clicked.store = pApp->store; } else if (timeClicked + 500 < SKIF_Util_timeGetTime ( )) { // Reset after 500 ms timeClicked = 0; - idClicked = 0; + item_clicked.reset ( ); } } @@ -2573,28 +2685,12 @@ Cache=false)"; // End Icon + Selectable row - - //change |= - // _HandleItemSelection (); - - // FIXME; Bug that doesn't actually change focus on search - /* - if (manual_selection.id == app.second.id && - manual_selection.store == app.second.store) - { - selection.appid = 0; - selection.store.clear(); - manual_selection.id = 0; - manual_selection.store.clear(); - change = true; - } - */ - - if ( app.second.id == selection.appid && + if ( app.second.id == selection.appid && + app.second.store == selection.store && sort_changed && (! ImGui::IsItemVisible ()) ) { - selection.appid = 0; + selection.reset ( ); change = true; } @@ -2612,7 +2708,8 @@ Cache=false)"; if (update) { timeClicked = SKIF_Util_timeGetTime ( ); - idClicked = selection.appid; + item_clicked.appid = selection.appid; + item_clicked.store = selection.store; app.second._status.invalidate (); @@ -2643,6 +2740,12 @@ Cache=false)"; ImGui::SetScrollHereY (0.5f); pApp = &app.second; + + if (update) + { + UpdateInjectionStrategy (pApp); + _cache.Refresh (pApp); + } } } @@ -2688,185 +2791,17 @@ Cache=false)"; // Stop populating the whole list - // This ensures the next block gets run when launching SKIF with a last selected item - SK_RunOnce (update = true); - - // Update the injection strategy for the game - if ((update && pApp != nullptr) || (updateInjStrat && pApp != nullptr)) + if (pApp != nullptr) { - updateInjStrat = false; - - // Handle GOG, Epic, and SKIF Custom games - if (pApp->store != "Steam") + // Update the injection strategy for the game + if (selection.dir_watch.isSignaled ( )) { - DWORD dwBinaryType = MAXDWORD; - if ( GetBinaryTypeW (pApp->launch_configs[0].getExecutableFullPath(pApp->id).c_str (), &dwBinaryType) ) - { - if (dwBinaryType == SCS_32BIT_BINARY) - pApp->specialk.injection.injection.bitness = InjectionBitness::ThirtyTwo; - else if (dwBinaryType == SCS_64BIT_BINARY) - pApp->specialk.injection.injection.bitness = InjectionBitness::SixtyFour; - } - - std::wstring test_paths[] = { - pApp->launch_configs[0].getExecutableDir(pApp->id, false), - pApp->launch_configs[0].working_dir - }; - - if (test_paths[0] == test_paths[1]) - test_paths[1] = L""; - - struct { - InjectionBitness bitness; - InjectionPoint entry_pt; - std::wstring name; - std::wstring path; - } test_dlls [] = { - { pApp->specialk.injection.injection.bitness, InjectionPoint::D3D9, L"d3d9", L"" }, - { pApp->specialk.injection.injection.bitness, InjectionPoint::DXGI, L"dxgi", L"" }, - { pApp->specialk.injection.injection.bitness, InjectionPoint::D3D11, L"d3d11", L"" }, - { pApp->specialk.injection.injection.bitness, InjectionPoint::OpenGL, L"OpenGL32", L"" }, - { pApp->specialk.injection.injection.bitness, InjectionPoint::DInput8, L"dinput8", L"" } - }; - - // Assume Global 32-bit if we don't know otherwise - bool bIs64Bit = - ( pApp->specialk.injection.injection.bitness == - InjectionBitness::SixtyFour ); - - pApp->specialk.injection.config.type = - ConfigType::Centralized; - - wchar_t wszPathToSelf [MAX_PATH] = { }; - GetModuleFileNameW (0, wszPathToSelf, MAX_PATH); - PathRemoveFileSpecW ( wszPathToSelf); - PathAppendW ( wszPathToSelf, - bIs64Bit ? L"SpecialK64.dll" - : L"SpecialK32.dll" ); - - pApp->specialk.injection.injection.dll_path = wszPathToSelf; - pApp->specialk.injection.injection.dll_ver = - SKIF_GetSpecialKDLLVersion ( wszPathToSelf); - - pApp->specialk.injection.injection.type = - InjectionType::Global; - pApp->specialk.injection.injection.entry_pt = - InjectionPoint::CBTHook; - pApp->specialk.injection.config.file = - L"SpecialK.ini"; - - bool breakOuterLoop = false; - for ( auto& test_path : test_paths) - { - if (test_path.empty()) - continue; - - for ( auto& dll : test_dlls ) - { - dll.path = - ( test_path + LR"(\)" ) + - ( dll.name + L".dll" ); - - if (PathFileExistsW (dll.path.c_str ())) - { - std::wstring dll_ver = - SKIF_GetSpecialKDLLVersion (dll.path.c_str ()); - - if (! dll_ver.empty ()) - { - pApp->specialk.injection.injection = { - dll.bitness, - dll.entry_pt, InjectionType::Local, - dll.path, dll_ver - }; - - if (PathFileExistsW ((test_path + LR"(\SpecialK.Central)").c_str ())) - { - pApp->specialk.injection.config.type = - ConfigType::Centralized; - } - - else - { - pApp->specialk.injection.config = { - ConfigType::Localized, - test_path - }; - } - - pApp->specialk.injection.config.file = - dll.name + L".ini"; - - breakOuterLoop = true; - break; - } - } - } - - if (breakOuterLoop) - break; - } - - if (pApp->specialk.injection.config.type == ConfigType::Centralized) - { - pApp->specialk.injection.config.dir = - SK_FormatStringW(LR"(%ws\Profiles\%ws)", - _path_cache.specialk_userdata, - pApp->specialk.profile_dir.c_str()); - } - - pApp->specialk.injection.config.file = - ( pApp->specialk.injection.config.dir + LR"(\)" ) + - pApp->specialk.injection.config.file; - - } - - // Handle Steam games - else { - pApp->specialk.injection = - SKIF_InstallUtils_GetInjectionStrategy (pApp->id); - - // Scan Special K configuration, etc. - if (pApp->specialk.profile_dir.empty ()) - { - pApp->specialk.profile_dir = pApp->specialk.injection.config.dir; - - if (! pApp->specialk.profile_dir.empty ()) - { - SK_VirtualFS profile_vfs; - - int files = - SK_VFS_ScanTree ( profile_vfs, - pApp->specialk.profile_dir.data (), 2 ); - - UNREFERENCED_PARAMETER (files); - - //SK_VirtualFS::vfsNode* pFile = - // profile_vfs; - - // 4/15/21: Temporarily disable Screenshot Browser, it's not functional enough - // to have it distract users yet. - // - /////for (const auto& it : pFile->children) - /////{ - ///// if (it.second->type_ == SK_VirtualFS::vfsNode::type::Directory) - ///// { - ///// if (it.second->name.find (LR"(\Screenshots)") != std::wstring::npos) - ///// { - ///// for ( const auto& it2 : it.second->children ) - ///// { - ///// pApp->specialk.screenshots.emplace ( - ///// SK_WideCharToUTF8 (it2.second->getFullPath ()) - ///// ); - ///// } - ///// } - ///// } - /////} - } - } + UpdateInjectionStrategy (pApp); + _cache.Refresh (pApp); } } + #pragma region GamesList::IconMenu if (IconMenu == PopupState_Open) @@ -2945,7 +2880,7 @@ Cache=false)"; pApp->tex_icon.texture, (pApp->store == "GOG") ? pApp->install_dir + L"\\goggame-" + std::to_wstring(pApp->id) + L".ico" - : SK_FormatStringW (LR"(%ws\appcache\librarycache\%i_icon.jpg)", SK_GetSteamDir(), pApp->id), //L"_icon.jpg", + : SK_FormatStringW (LR"(%ws\appcache\librarycache\%i_icon.jpg)", _path_cache.steam_install, pApp->id), //L"_icon.jpg", dontCare1, dontCare2, pApp ); @@ -2995,7 +2930,7 @@ Cache=false)"; pApp->tex_icon.texture, (pApp->store == "GOG") ? pApp->install_dir + L"\\goggame-" + std::to_wstring(pApp->id) + L".ico" - : SK_FormatStringW (LR"(%ws\appcache\librarycache\%i_icon.jpg)", SK_GetSteamDir(), pApp->id), //L"_icon.jpg", + : SK_FormatStringW (LR"(%ws\appcache\librarycache\%i_icon.jpg)", _path_cache.steam_install, pApp->id), //L"_icon.jpg", dontCare1, dontCare2, pApp ); @@ -3136,7 +3071,7 @@ Cache=false)"; else if (pApp != nullptr) { - _PrintInjectionSummary (pApp); + GetInjectionSummary (pApp); if ( pApp->extended_config.vac.enabled == 1 ) { @@ -3293,7 +3228,7 @@ Cache=false)"; // Special handling at the bottom for Special K - if ( selection.appid == SKIF_STEAM_APPID ) { + if (selection.appid == SKIF_STEAM_APPID && selection.store == "Steam") { ImGui::SetCursorPos ( ImVec2 ( vecPosCoverImage.x + ImGui::GetStyle().FrameBorderSize, fY - floorf((204.f * SKIF_ImGui_GlobalDPIScale) + ImGui::GetStyle().FrameBorderSize) )); ImGui::BeginGroup (); @@ -4331,11 +4266,10 @@ Cache=false)"; } // Reset selection to Special K - selection.appid = SKIF_STEAM_APPID; - selection.store = "Steam"; + selection.reset ( ); for (auto& app : apps) - if (app.second.id == selection.appid) + if (app.second.id == selection.appid && app.second.store == selection.store) pApp = &app.second; update = true; @@ -4541,27 +4475,6 @@ Cache=false)"; strncpy (charPath, "\0", MAX_PATH); strncpy (charArgs, "\0", 500); - // Change selection to the new game - /* - selection.appid = newAppId; - for (auto& app : apps) - if (app.second.id == selection.appid && app.second.store == "Other") - pApp = &app.second; - - ImVec2 dontCare1, dontCare2; - - // Load the new icon (hopefully) - LoadLibraryTexture (LibraryTexture::Icon, - newAppId, - pApp->tex_icon.texture, - L"icon", - dontCare1, - dontCare2, - pApp ); - - update = true; - */ - // Unload any current cover if (pTexSRV.p != nullptr) { @@ -4870,7 +4783,7 @@ Cache=false)"; selection.appid = SelectNewSKIFGame; selection.store = "Other"; for (auto& app : apps) - if (app.second.id == selection.appid && app.second.store == "Other") + if (app.second.id == selection.appid && app.second.store == selection.store) pApp = &app.second; update = true; @@ -4909,4 +4822,4 @@ Cache=false)"; // Trigger a refresh of the cover loadCover = true; } -} \ No newline at end of file +}