mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-15 19:27:56 +01:00
[Workspaces]Fix for steam games capture&launch: capture and correctly launch steam games. (#38380)
* Workspaces fix: capture steam games. * minor fix * Launch steam apps by url appmodeluserid instead of directly exe call. * fix copilot comment * fix * remove unnecessary string * expect words * white list words * Order of alphabet * exclude thin frame if it's not a steam game. * fix build * fix regression * adjust comment
This commit is contained in:
2
.github/actions/spell-check/allow/code.txt
vendored
2
.github/actions/spell-check/allow/code.txt
vendored
@@ -273,4 +273,4 @@ mengyuanchen
|
||||
testhost
|
||||
|
||||
#Tools
|
||||
OIP
|
||||
OIP
|
||||
6
.github/actions/spell-check/expect.txt
vendored
6
.github/actions/spell-check/expect.txt
vendored
@@ -8,6 +8,7 @@ Acceleratorkeys
|
||||
ACCEPTFILES
|
||||
ACCESSDENIED
|
||||
ACCESSTOKEN
|
||||
acfs
|
||||
AClient
|
||||
AColumn
|
||||
acrt
|
||||
@@ -523,6 +524,7 @@ FZE
|
||||
gacutil
|
||||
Gaeilge
|
||||
Gaidhlig
|
||||
gameid
|
||||
GC'ed
|
||||
GCLP
|
||||
gdi
|
||||
@@ -712,6 +714,7 @@ INPUTSINK
|
||||
INPUTTYPE
|
||||
INSTALLDESKTOPSHORTCUT
|
||||
INSTALLDIR
|
||||
installdir
|
||||
INSTALLFOLDER
|
||||
INSTALLFOLDERTOBOOTSTRAPPERINSTALLFOLDER
|
||||
INSTALLFOLDERTOPREVIOUSINSTALLFOLDER
|
||||
@@ -1569,6 +1572,7 @@ stdcpp
|
||||
stdcpplatest
|
||||
STDMETHODCALLTYPE
|
||||
STDMETHODIMP
|
||||
steamapps
|
||||
STGC
|
||||
STGM
|
||||
STGMEDIUM
|
||||
@@ -1969,4 +1973,4 @@ zoomit
|
||||
ZOOMITX
|
||||
ZXk
|
||||
ZXNs
|
||||
zzz
|
||||
zzz
|
||||
@@ -121,6 +121,22 @@ namespace AppLauncher
|
||||
// packaged apps: try launching first by AppUserModel.ID
|
||||
// usage example: elevated Terminal
|
||||
if (!launched && !app.appUserModelId.empty() && !app.packageFullName.empty())
|
||||
{
|
||||
Logger::trace(L"Launching {} as {} - {app.packageFullName}", app.name, app.appUserModelId, app.packageFullName);
|
||||
auto res = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated);
|
||||
if (res.isOk())
|
||||
{
|
||||
launched = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
launchErrors.push_back({ std::filesystem::path(app.path).filename(), res.error() });
|
||||
}
|
||||
}
|
||||
|
||||
// win32 app with appUserModelId:
|
||||
// usage example: steam games
|
||||
if (!launched && !app.appUserModelId.empty())
|
||||
{
|
||||
Logger::trace(L"Launching {} as {}", app.name, app.appUserModelId);
|
||||
auto res = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "pch.h"
|
||||
#include "AppUtils.h"
|
||||
#include "SteamHelper.h"
|
||||
|
||||
#include <atlbase.h>
|
||||
#include <propvarutil.h>
|
||||
@@ -34,6 +35,8 @@ namespace Utils
|
||||
|
||||
constexpr const wchar_t* EdgeFilename = L"msedge.exe";
|
||||
constexpr const wchar_t* ChromeFilename = L"chrome.exe";
|
||||
|
||||
constexpr const wchar_t* SteamUrlProtocol = L"steam:";
|
||||
}
|
||||
|
||||
AppList IterateAppsFolder()
|
||||
@@ -138,6 +141,34 @@ namespace Utils
|
||||
else if (prop == NonLocalizable::PackageInstallPathProp || prop == NonLocalizable::InstallPathProp)
|
||||
{
|
||||
data.installPath = propVariantString.m_pData;
|
||||
|
||||
if (!data.installPath.empty())
|
||||
{
|
||||
const bool isSteamProtocol = data.installPath.rfind(NonLocalizable::SteamUrlProtocol, 0) == 0;
|
||||
|
||||
if (isSteamProtocol)
|
||||
{
|
||||
Logger::info(L"Found steam game: protocol path: {}", data.installPath);
|
||||
data.protocolPath = data.installPath;
|
||||
|
||||
try
|
||||
{
|
||||
auto gameId = Steam::GetGameIdFromUrlProtocolPath(data.installPath);
|
||||
auto gameFolder = Steam::GetSteamGameInfoFromAcfFile(gameId);
|
||||
|
||||
if (gameFolder)
|
||||
{
|
||||
data.installPath = gameFolder->gameInstallationPath;
|
||||
Logger::info(L"Found steam game: physical path: {}", data.installPath);
|
||||
}
|
||||
}
|
||||
catch (std::exception ex)
|
||||
{
|
||||
Logger::error(L"Failed to get installPath for game {}", data.installPath);
|
||||
Logger::error("Error: {}", ex.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,5 +428,10 @@ namespace Utils
|
||||
{
|
||||
return installPath.ends_with(NonLocalizable::ChromeFilename);
|
||||
}
|
||||
|
||||
bool AppData::IsSteamGame() const
|
||||
{
|
||||
return protocolPath.rfind(NonLocalizable::SteamUrlProtocol, 0) == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,12 @@ namespace Utils
|
||||
std::wstring packageFullName;
|
||||
std::wstring appUserModelId;
|
||||
std::wstring pwaAppId;
|
||||
std::wstring protocolPath;
|
||||
bool canLaunchElevated = false;
|
||||
|
||||
bool IsEdge() const;
|
||||
bool IsChrome() const;
|
||||
bool IsSteamGame() const;
|
||||
};
|
||||
|
||||
using AppList = std::vector<AppData>;
|
||||
|
||||
171
src/modules/Workspaces/WorkspacesLib/SteamGameHelper.cpp
Normal file
171
src/modules/Workspaces/WorkspacesLib/SteamGameHelper.cpp
Normal file
@@ -0,0 +1,171 @@
|
||||
#include "pch.h"
|
||||
#include "SteamHelper.h"
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
#include <filesystem>
|
||||
#include <regex>
|
||||
#include <string>
|
||||
|
||||
namespace Utils
|
||||
{
|
||||
|
||||
static std::wstring Utf8ToWide(const std::string& utf8)
|
||||
{
|
||||
if (utf8.empty())
|
||||
return L"";
|
||||
|
||||
int size = MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast<int>(utf8.size()), nullptr, 0);
|
||||
if (size <= 0)
|
||||
return L"";
|
||||
|
||||
std::wstring wide(size, L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast<int>(utf8.size()), wide.data(), size);
|
||||
return wide;
|
||||
}
|
||||
|
||||
namespace Steam
|
||||
{
|
||||
using namespace std;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static std::optional<std::wstring> GetSteamExePathFromRegistry()
|
||||
{
|
||||
static std::optional<std::wstring> cachedPath;
|
||||
if (cachedPath.has_value())
|
||||
{
|
||||
return cachedPath;
|
||||
}
|
||||
|
||||
const std::vector<HKEY> roots = { HKEY_CLASSES_ROOT, HKEY_LOCAL_MACHINE, HKEY_USERS };
|
||||
const std::vector<std::wstring> subKeys = {
|
||||
L"steam\\shell\\open\\command",
|
||||
L"Software\\Classes\\steam\\shell\\open\\command",
|
||||
};
|
||||
|
||||
for (HKEY root : roots)
|
||||
{
|
||||
for (const auto& subKey : subKeys)
|
||||
{
|
||||
HKEY hKey;
|
||||
if (RegOpenKeyExW(root, subKey.c_str(), 0, KEY_READ, &hKey) == ERROR_SUCCESS)
|
||||
{
|
||||
wchar_t value[512];
|
||||
DWORD size = sizeof(value);
|
||||
DWORD type = 0;
|
||||
|
||||
if (RegQueryValueExW(hKey, nullptr, nullptr, &type, reinterpret_cast<LPBYTE>(value), &size) == ERROR_SUCCESS &&
|
||||
(type == REG_SZ || type == REG_EXPAND_SZ))
|
||||
{
|
||||
std::wregex exeRegex(LR"delim("([^"]+steam\.exe)")delim");
|
||||
std::wcmatch match;
|
||||
if (std::regex_search(value, match, exeRegex) && match.size() > 1)
|
||||
{
|
||||
RegCloseKey(hKey);
|
||||
cachedPath = match[1].str();
|
||||
return cachedPath;
|
||||
}
|
||||
}
|
||||
|
||||
RegCloseKey(hKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cachedPath = std::nullopt;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static fs::path GetSteamBasePath()
|
||||
{
|
||||
auto steamFolderOpt = GetSteamExePathFromRegistry();
|
||||
if (!steamFolderOpt)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
return fs::path(*steamFolderOpt).parent_path() / L"steamapps";
|
||||
}
|
||||
|
||||
static fs::path GetAcfFilePath(const std::wstring& gameId)
|
||||
{
|
||||
auto steamFolderOpt = GetSteamExePathFromRegistry();
|
||||
if (!steamFolderOpt)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
return GetSteamBasePath() / (L"appmanifest_" + gameId + L".acf");
|
||||
}
|
||||
|
||||
static fs::path GetGameInstallPath(const std::wstring& gameFolderName)
|
||||
{
|
||||
auto steamFolderOpt = GetSteamExePathFromRegistry();
|
||||
if (!steamFolderOpt)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
return GetSteamBasePath() / L"common" / gameFolderName;
|
||||
}
|
||||
|
||||
static unordered_map<wstring, wstring> ParseAcfFile(const fs::path& acfPath)
|
||||
{
|
||||
unordered_map<wstring, wstring> result;
|
||||
|
||||
ifstream file(acfPath);
|
||||
if (!file.is_open())
|
||||
return result;
|
||||
|
||||
string line;
|
||||
while (getline(file, line))
|
||||
{
|
||||
smatch matches;
|
||||
static const regex pattern(R"delim("([^"]+)"\s+"([^"]+)")delim");
|
||||
|
||||
if (regex_search(line, matches, pattern) && matches.size() == 3)
|
||||
{
|
||||
wstring key = Utf8ToWide(matches[1].str());
|
||||
wstring value = Utf8ToWide(matches[2].str());
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::unique_ptr<Steam::SteamGame> GetSteamGameInfoFromAcfFile(const std::wstring& gameId)
|
||||
{
|
||||
fs::path acfPath = Steam::GetAcfFilePath(gameId);
|
||||
|
||||
if (!fs::exists(acfPath))
|
||||
return nullptr;
|
||||
|
||||
auto kv = ParseAcfFile(acfPath);
|
||||
if (kv.empty() || kv.find(L"installdir") == kv.end())
|
||||
return nullptr;
|
||||
|
||||
fs::path gamePath = Steam::GetGameInstallPath(kv[L"installdir"]);
|
||||
if (!fs::exists(gamePath))
|
||||
return nullptr;
|
||||
|
||||
auto game = std::make_unique<Steam::SteamGame>();
|
||||
game->gameId = gameId;
|
||||
game->gameInstallationPath = gamePath.wstring();
|
||||
return game;
|
||||
}
|
||||
|
||||
std::wstring GetGameIdFromUrlProtocolPath(const std::wstring& urlPath)
|
||||
{
|
||||
const std::wstring steamGamePrefix = L"steam://rungameid/";
|
||||
|
||||
if (urlPath.rfind(steamGamePrefix, 0) == 0)
|
||||
{
|
||||
return urlPath.substr(steamGamePrefix.length());
|
||||
}
|
||||
|
||||
return L"";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
24
src/modules/Workspaces/WorkspacesLib/SteamHelper.h
Normal file
24
src/modules/Workspaces/WorkspacesLib/SteamHelper.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "pch.h"
|
||||
|
||||
namespace Utils
|
||||
{
|
||||
namespace NonLocalizable
|
||||
{
|
||||
const std::wstring AcfFileNameTemplate = L"appmanifest_<gameid>.acfs";
|
||||
}
|
||||
|
||||
namespace Steam
|
||||
{
|
||||
struct SteamGame
|
||||
{
|
||||
std::wstring gameId;
|
||||
std::wstring gameInstallationPath;
|
||||
};
|
||||
|
||||
std::unique_ptr<SteamGame> GetSteamGameInfoFromAcfFile(const std::wstring& gameId);
|
||||
|
||||
std::wstring GetGameIdFromUrlProtocolPath(const std::wstring& urlPath);
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="PwaHelper.h" />
|
||||
<ClInclude Include="Result.h" />
|
||||
<ClInclude Include="SteamHelper.h" />
|
||||
<ClInclude Include="StringUtils.h" />
|
||||
<ClInclude Include="utils.h" />
|
||||
<ClInclude Include="WbemHelper.h" />
|
||||
@@ -57,6 +58,7 @@
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="PwaHelper.cpp" />
|
||||
<ClCompile Include="SteamGameHelper.cpp" />
|
||||
<ClCompile Include="two_way_pipe_message_ipc.cpp" />
|
||||
<ClCompile Include="WbemHelper.cpp" />
|
||||
<ClCompile Include="WorkspacesData.cpp" />
|
||||
|
||||
@@ -53,6 +53,9 @@
|
||||
<ClInclude Include="StringUtils.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="SteamHelper.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="pch.cpp">
|
||||
@@ -88,6 +91,9 @@
|
||||
<ClCompile Include="WbemHelper.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="SteamGameHelper.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
|
||||
@@ -71,6 +71,8 @@ namespace SnapshotUtils
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger::info("Try to get window app:{}", reinterpret_cast<void*>(window));
|
||||
|
||||
DWORD pid{};
|
||||
GetWindowThreadProcessId(window, &pid);
|
||||
|
||||
@@ -118,10 +120,19 @@ namespace SnapshotUtils
|
||||
auto data = Utils::Apps::GetApp(processPath, pid, installedApps);
|
||||
if (!data.has_value() || data->name.empty())
|
||||
{
|
||||
Logger::info(L"Installed app not found: {}", processPath);
|
||||
Logger::info(L"Installed app not found:{},{}", reinterpret_cast<void*>(window), processPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!data->IsSteamGame() && !WindowUtils::HasThickFrame(window))
|
||||
{
|
||||
// Only care about steam games if it has no thick frame to remain consistent with
|
||||
// the behavior as before.
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger::info(L"Found app for window:{},{}", reinterpret_cast<void*>(window), processPath);
|
||||
|
||||
auto appData = data.value();
|
||||
|
||||
bool isEdge = appData.IsEdge();
|
||||
|
||||
@@ -200,6 +200,14 @@ std::optional<WindowWithDistance> WindowArranger::GetNearestWindow(const Workspa
|
||||
}
|
||||
|
||||
auto data = Utils::Apps::GetApp(processPath, pid, m_installedApps);
|
||||
|
||||
if (!data->IsSteamGame() && !WindowUtils::HasThickFrame(window))
|
||||
{
|
||||
// Only care about steam games if it has no thick frame to remain consistent with
|
||||
// the behavior as before.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!data.has_value())
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -9,10 +9,12 @@ namespace WindowFilter
|
||||
{
|
||||
auto style = GetWindowLong(window, GWL_STYLE);
|
||||
bool isPopup = WindowUtils::HasStyle(style, WS_POPUP);
|
||||
bool hasThickFrame = WindowUtils::HasStyle(style, WS_THICKFRAME);
|
||||
bool hasCaption = WindowUtils::HasStyle(style, WS_CAPTION);
|
||||
bool hasMinimizeMaximizeButtons = WindowUtils::HasStyle(style, WS_MINIMIZEBOX) || WindowUtils::HasStyle(style, WS_MAXIMIZEBOX);
|
||||
if (isPopup && !(hasThickFrame && (hasCaption || hasMinimizeMaximizeButtons)))
|
||||
|
||||
Logger::info("Style for window: {}, {:#x}", reinterpret_cast<void*>(window), style);
|
||||
|
||||
if (isPopup && !(hasCaption || hasMinimizeMaximizeButtons))
|
||||
{
|
||||
// popup windows we want to snap: e.g. Calculator, Telegram
|
||||
// popup windows we don't want to snap: start menu, notification popup, tray window, etc.
|
||||
|
||||
@@ -121,4 +121,11 @@ namespace WindowUtils
|
||||
|
||||
return std::wstring(title);
|
||||
}
|
||||
|
||||
|
||||
inline bool HasThickFrame(HWND window)
|
||||
{
|
||||
auto style = GetWindowLong(window, GWL_STYLE);
|
||||
return WindowUtils::HasStyle(style, WS_THICKFRAME);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user