Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/spell-check/allow/code.txt
Original file line number Diff line number Diff line change
Expand Up @@ -273,4 +273,4 @@ mengyuanchen
testhost

#Tools
OIP
OIP
6 changes: 5 additions & 1 deletion .github/actions/spell-check/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
ACCEPTFILES
ACCESSDENIED
ACCESSTOKEN
acfs
AClient
AColumn
acrt
Expand Down Expand Up @@ -520,6 +521,7 @@
gacutil
Gaeilge
Gaidhlig
gameid
GC'ed
GCLP
gdi
Expand Down Expand Up @@ -707,7 +709,8 @@
INPUTSINK
INPUTTYPE
INSTALLDESKTOPSHORTCUT
INSTALLDIR

Check warning on line 712 in .github/actions/spell-check/expect.txt

View workflow job for this annotation

GitHub Actions / Check Spelling

`INSTALLDIR` is ignored by check spelling because another more general variant is also in expect. (ignored-expect-variant)
installdir
INSTALLFOLDER
INSTALLFOLDERTOBOOTSTRAPPERINSTALLFOLDER
INSTALLFOLDERTOPREVIOUSINSTALLFOLDER
Expand Down Expand Up @@ -1557,6 +1560,7 @@
stdcpplatest
STDMETHODCALLTYPE
STDMETHODIMP
steamapps
STGC
STGM
STGMEDIUM
Expand Down Expand Up @@ -1956,4 +1960,4 @@
ZOOMITX
ZXk
ZXNs
zzz
zzz
16 changes: 16 additions & 0 deletions src/modules/Workspaces/WorkspacesLauncher/AppLauncher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
36 changes: 36 additions & 0 deletions src/modules/Workspaces/WorkspacesLib/AppUtils.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "pch.h"
#include "AppUtils.h"
#include "SteamHelper.h"

#include <atlbase.h>
#include <propvarutil.h>
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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());
}
}
}
}
}

Expand Down Expand Up @@ -397,5 +428,10 @@ namespace Utils
{
return installPath.ends_with(NonLocalizable::ChromeFilename);
}

bool AppData::IsSteamGame() const
{
return protocolPath.rfind(NonLocalizable::SteamUrlProtocol, 0) == 0;
}
}
}
2 changes: 2 additions & 0 deletions src/modules/Workspaces/WorkspacesLib/AppUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down
171 changes: 171 additions & 0 deletions src/modules/Workspaces/WorkspacesLib/SteamGameHelper.cpp
Original file line number Diff line number Diff line change
@@ -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 changes: 24 additions & 0 deletions src/modules/Workspaces/WorkspacesLib/SteamHelper.h
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 2 additions & 0 deletions src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand All @@ -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" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down Expand Up @@ -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" />
Expand Down
Loading
Loading