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
4 changes: 3 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.25 FATAL_ERROR)
# Create project
project(sdk_launcher
DESCRIPTION "An SDK launcher for Strata Source engine games"
VERSION "0.5.3"
VERSION "0.6.0"
HOMEPAGE_URL "https://github.com/StrataSource/sdk-launcher")
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
Expand Down Expand Up @@ -144,6 +144,8 @@ add_executable(${PROJECT_TARGET_NAME} WIN32
"${CMAKE_CURRENT_SOURCE_DIR}/src/NewModDialog.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/NewP2CEAddonDialog.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/NewP2CEAddonDialog.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/Steam.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/Steam.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/Window.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/Window.h")

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ Here is an example config file that may be loaded into the SDK launcher.

```json5
{
// The name of the game directory
// The name of the game directory (if relative), or an absolute path. If this field is an absolute path,
// the game_icon field (which assumes ${GAME} is relative) should be updated to prevent a missing logo.
// Use ${SOURCEMODS} to enter the SourceMods folder, for example "${SOURCEMODS}/mymod"
"game_default": "p2ce",
// Optional, the default is "${ROOT}/${GAME}/resource/game.ico"
"game_icon": "${ROOT}/${GAME}/resource/game.ico",
Expand Down
7 changes: 6 additions & 1 deletion res/config/p2ce.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"game_default": "p2ce",
"window_height": 625,
"window_height": 647,
"mod_template_url": "https://github.com/StrataSource/p2ce-mod-template/archive/refs/heads/main.zip",
"supports_p2ce_addons": true,
"sections": [
Expand Down Expand Up @@ -116,6 +116,11 @@
"name": "SDK Tools",
"type": "directory",
"action": "${ROOT}/sdk_tools"
},
{
"name": "Steam SourceMods",
"type": "directory",
"action": "${SOURCEMODS}"
}
]
}
Expand Down
1 change: 1 addition & 0 deletions src/GameConfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ void GameConfig::setVariable(const QString& variable, const QString& replacement
const auto setVar = [&variable, &replacement](QString& str) {
str.replace(QString("${%1}").arg(variable), replacement);
};
setVar(this->gameDefault);
setVar(this->gameIcon);
for (auto& section : this->sections) {
setVar(section.name);
Expand Down
84 changes: 3 additions & 81 deletions src/NewModDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,88 +21,10 @@
#include <QProgressDialog>
#include <QStandardPaths>

#ifdef _WIN32
#include <memory>
#include <Windows.h>
#else
#include <cstdlib>
#endif
#include "Steam.h"

namespace {

/// Copied from sourcepp
[[nodiscard]] QString getSourceModsDirLocationHelper() {
std::filesystem::path steamLocation;
std::error_code ec;

#ifdef _WIN32
{
// 16383 being the maximum length of a path
static constexpr DWORD STEAM_LOCATION_MAX_SIZE = 16383;
std::unique_ptr<char[]> steamLocationData{new char[STEAM_LOCATION_MAX_SIZE]};

HKEY steam;
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, R"(SOFTWARE\Valve\Steam)", 0, KEY_QUERY_VALUE | KEY_WOW64_32KEY, &steam) != ERROR_SUCCESS) {
return "";
}

DWORD steamLocationSize = STEAM_LOCATION_MAX_SIZE;
if (RegQueryValueExA(steam, "InstallPath", nullptr, nullptr, reinterpret_cast<LPBYTE>(steamLocationData.get()), &steamLocationSize) != ERROR_SUCCESS) {
return "";
}

RegCloseKey(steam);
steamLocation = steamLocationSize > 0 ? std::string(steamLocationData.get(), steamLocationSize - 1) : "";
}
#else
{
std::filesystem::path home{std::getenv("HOME")};
#ifdef __APPLE__
steamLocation = home / "Library" / "Application Support" / "Steam";
#else
// Snap install takes priority, the .steam symlink may exist simultaneously with Snap installs
steamLocation = home / "snap" / "steam" / "common" / ".steam" / "steam";

if (!std::filesystem::exists(steamLocation, ec)) {
// Use the regular install path
steamLocation = home / ".steam" / "steam";
}
#endif
}

if (!std::filesystem::exists(steamLocation, ec)) {
std::string location;
std::filesystem::path d{"cwd/steamclient64.dll"};
for (const auto& entry : std::filesystem::directory_iterator{"/proc/"}) {
if (std::filesystem::exists(entry / d, ec)) {
ec.clear();
const auto s = std::filesystem::read_symlink(entry.path() / "cwd", ec);
if (ec) {
continue;
}
location = s.string();
break;
}
}
if (location.empty()) {
return "";
} else {
steamLocation = location;
}
}
#endif

if (auto sourceModPath = (steamLocation / "steamapps" / "sourcemods").string(); std::filesystem::exists(sourceModPath, ec)) {
return sourceModPath.c_str();
}
return "";
}

[[nodiscard]] const QString& getSourceModsDirLocation() {
static QString location = ::getSourceModsDirLocationHelper();
return location;
}

[[nodiscard]] QString join(const QStringList& list, const QString& separator) {
if (list.isEmpty()) {
return "";
Expand Down Expand Up @@ -217,7 +139,7 @@ NewModDialog::NewModDialog(QString gameRoot_, QString downloadURL_, QWidget* par
, gameRoot(std::move(gameRoot_))
, downloadURL(std::move(downloadURL_)) {
// Check for sourcemods
const bool knowsSourcemodsDirLocation = !::getSourceModsDirLocation().isEmpty();
const bool knowsSourcemodsDirLocation = !::getSourceModsDir().isEmpty();

// Window setup
this->setModal(true);
Expand Down Expand Up @@ -352,7 +274,7 @@ QString NewModDialog::getModInstallDirParent() const {
}
switch (selectedIndex) {
case 0:
return ::getSourceModsDirLocation();
return ::getSourceModsDir();
case 1:
return this->gameRoot;
default:
Expand Down
86 changes: 86 additions & 0 deletions src/Steam.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#include "Steam.h"

#include <filesystem>
#ifdef _WIN32
#include <memory>
#include <Windows.h>
#else
#include <cstdlib>
#endif

namespace {

/// Copied from sourcepp
[[nodiscard]] QString getSourceModsDirHelper() {
std::filesystem::path steamLocation;
std::error_code ec;

#ifdef _WIN32
{
// 16383 being the maximum length of a path
static constexpr DWORD STEAM_LOCATION_MAX_SIZE = 16383;
std::unique_ptr<char[]> steamLocationData{new char[STEAM_LOCATION_MAX_SIZE]};

HKEY steam;
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, R"(SOFTWARE\Valve\Steam)", 0, KEY_QUERY_VALUE | KEY_WOW64_32KEY, &steam) != ERROR_SUCCESS) {
return "";
}

DWORD steamLocationSize = STEAM_LOCATION_MAX_SIZE;
if (RegQueryValueExA(steam, "InstallPath", nullptr, nullptr, reinterpret_cast<LPBYTE>(steamLocationData.get()), &steamLocationSize) != ERROR_SUCCESS) {
return "";
}

RegCloseKey(steam);
steamLocation = steamLocationSize > 0 ? std::string(steamLocationData.get(), steamLocationSize - 1) : "";
}
#else
{
std::filesystem::path home{std::getenv("HOME")};
#ifdef __APPLE__
steamLocation = home / "Library" / "Application Support" / "Steam";
#else
// Snap install takes priority, the .steam symlink may exist simultaneously with Snap installs
steamLocation = home / "snap" / "steam" / "common" / ".steam" / "steam";

if (!std::filesystem::exists(steamLocation, ec)) {
// Use the regular install path
steamLocation = home / ".steam" / "steam";
}
#endif
}

if (!std::filesystem::exists(steamLocation, ec)) {
std::string location;
std::filesystem::path d{"cwd/steamclient64.dll"};
for (const auto& entry : std::filesystem::directory_iterator{"/proc/"}) {
if (std::filesystem::exists(entry / d, ec)) {
ec.clear();
const auto s = std::filesystem::read_symlink(entry.path() / "cwd", ec);
if (ec) {
continue;
}
location = s.string();
break;
}
}
if (location.empty()) {
return "";
} else {
steamLocation = location;
}
}
#endif

if (auto sourceModPath = (steamLocation / "steamapps" / "sourcemods").string(); std::filesystem::exists(sourceModPath, ec)) {
return sourceModPath.c_str();
}
return "";
}

} // namespace

const QString& getSourceModsDir() {
static QString location = ::getSourceModsDirHelper();
return location;
}
5 changes: 5 additions & 0 deletions src/Steam.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#pragma once

#include <QString>

[[nodiscard]] const QString& getSourceModsDir();
62 changes: 34 additions & 28 deletions src/Window.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "LaunchButton.h"
#include "NewModDialog.h"
#include "NewP2CEAddonDialog.h"
#include "Steam.h"

#ifdef _WIN32
#include <shlobj_core.h>
Expand Down Expand Up @@ -75,16 +76,11 @@ constexpr std::string_view STR_GAME_OVERRIDE = "str_game_override";

Window::Window(QWidget* parent)
: QMainWindow(parent)
, gameDefault(PROJECT_DEFAULT_MOD.data())
, configUsingLegacyBinDir(false) {
this->setWindowTitle(PROJECT_NAME.data());
this->setMinimumHeight(400);

// Default settings (recent configs are set later on)
QSettings settings;
if (!settings.contains(STR_GAME_OVERRIDE)) {
settings.setValue(STR_GAME_OVERRIDE, QString(PROJECT_DEFAULT_MOD.data()));
}

// Icon
this->setWindowIcon(QIcon{getSDKLauncherIconPath()});

Expand All @@ -110,26 +106,22 @@ Window::Window(QWidget* parent)
// Game menu
auto* gameMenu = this->menuBar()->addMenu(tr("Game"));

this->game_resetToDefault = gameMenu->addAction(tr("Reset to Default"), [this] {
QSettings settings;
settings.setValue(STR_GAME_OVERRIDE, QString(PROJECT_DEFAULT_MOD.data()));
this->game_overrideGame->setText(tr("Override \"%1\" Folder").arg(settings.value(STR_GAME_OVERRIDE).toString()));
this->loadGameConfig(settings.value(STR_RECENT_CONFIGS).toStringList().first());
});

gameMenu->addSeparator();

this->game_overrideGame = gameMenu->addAction(tr("Override \"%1\" Folder").arg(settings.value(STR_GAME_OVERRIDE).toString()), [this] {
this->game_overrideGame = gameMenu->addAction(tr("Override Game Folder"), [this] {
const auto rootPath = ::getRootPath(this->configUsingLegacyBinDir);
if (auto path = QFileDialog::getExistingDirectory(this, tr("Override Game Folder"), rootPath); !path.isEmpty()) {
const QDir rootDir{rootPath};
QSettings settings;
settings.setValue(STR_GAME_OVERRIDE, QDir::cleanPath(rootDir.relativeFilePath(path)));
this->game_overrideGame->setText(tr("Override \"%1\" Folder").arg(settings.value(STR_GAME_OVERRIDE).toString()));
this->loadGameConfig(settings.value(STR_RECENT_CONFIGS).toStringList().first());
}
});

this->game_resetToDefault = gameMenu->addAction(tr("Reset to Default"), [this] {
QSettings settings;
settings.remove(STR_GAME_OVERRIDE);
this->loadGameConfig(settings.value(STR_RECENT_CONFIGS).toStringList().first());
});

// Utilities menu
auto* utilitiesMenu = this->menuBar()->addMenu(tr("Utilities"));

Expand All @@ -138,8 +130,16 @@ Window::Window(QWidget* parent)
});

this->utilities_createNewAddon = utilitiesMenu->addAction(this->style()->standardIcon(QStyle::SP_FileIcon), tr("Create New Addon"), [this] {
QSettings settings;
NewP2CEAddonDialog::open(::getRootPath(this->configUsingLegacyBinDir) + QDir::separator() + settings.value(STR_GAME_OVERRIDE, {PROJECT_DEFAULT_MOD.data()}).toString(), this);
QString gameRoot;
if (QSettings settings; settings.contains(STR_GAME_OVERRIDE)) {
gameRoot = settings.value(STR_GAME_OVERRIDE).toString();
} else {
gameRoot = this->gameDefault;
}
if (!QDir::isAbsolutePath(gameRoot)) {
gameRoot = ::getRootPath(this->configUsingLegacyBinDir) + QDir::separator() + gameRoot;
}
NewP2CEAddonDialog::open(gameRoot, this);
});

// Help menu
Expand Down Expand Up @@ -171,7 +171,7 @@ Window::Window(QWidget* parent)

new QVBoxLayout(this->main);

if (!settings.contains(STR_RECENT_CONFIGS)) {
if (QSettings settings; !settings.contains(STR_RECENT_CONFIGS)) {
settings.setValue(STR_RECENT_CONFIGS, QStringList{});
if (auto defaultConfigPath = QCoreApplication::applicationDirPath() + "/SDKLauncherDefault.json"; QFile::exists(defaultConfigPath)) {
this->loadGameConfig(defaultConfigPath);
Expand Down Expand Up @@ -227,6 +227,9 @@ void Window::loadGameConfig(const QString& path) {
settings.setValue(STR_RECENT_CONFIGS, recentConfigs);
this->regenerateRecentConfigs();

// Set ${SOURCEMODS}
gameConfig->setVariable("SOURCEMODS", ::getSourceModsDir());

// Set ${ROOT}
const auto rootPath = ::getRootPath(this->configUsingLegacyBinDir);
gameConfig->setVariable("ROOT", rootPath);
Expand All @@ -242,9 +245,18 @@ void Window::loadGameConfig(const QString& path) {
#warning "Unknown platform! ${PLATFORM} will not be substituted!"
#endif

// Set ${STRATA_ICON}
gameConfig->setVariable("STRATA_ICON", getStrataIconPath());

// Set ${SDKLAUNCHER_ICON}
gameConfig->setVariable("SDKLAUNCHER_ICON", getSDKLauncherIconPath());

// Get default game
this->gameDefault = gameConfig->getGameDefault();

// tiny hack: get default game icon before ${GAME} substitution
QString defaultGameIconPath = gameConfig->getGameIcon();
defaultGameIconPath.replace("${GAME}", PROJECT_DEFAULT_MOD.data());
defaultGameIconPath.replace("${GAME}", this->gameDefault);
if (QIcon defaultGameIcon{defaultGameIconPath}; !defaultGameIcon.isNull() && !defaultGameIcon.availableSizes().isEmpty()) {
this->config_loadDefault->setIcon(defaultGameIcon);
this->game_resetToDefault->setIcon(defaultGameIcon);
Expand All @@ -254,7 +266,7 @@ void Window::loadGameConfig(const QString& path) {
}

// Set ${GAME}
QString gameDir = settings.contains(STR_GAME_OVERRIDE) ? settings.value(STR_GAME_OVERRIDE).toString() : gameConfig->getGameDefault();
QString gameDir = settings.contains(STR_GAME_OVERRIDE) ? settings.value(STR_GAME_OVERRIDE).toString() : this->gameDefault;
gameConfig->setVariable("GAME", gameDir);

// Set ${GAME_ICON}
Expand All @@ -266,12 +278,6 @@ void Window::loadGameConfig(const QString& path) {
gameConfig->setVariable("GAME_ICON", "");
}

// Set ${STRATA_ICON}
gameConfig->setVariable("STRATA_ICON", getStrataIconPath());

// Set ${SDKLAUNCHER_ICON}
gameConfig->setVariable("SDKLAUNCHER_ICON", getSDKLauncherIconPath());

for (int i = 0; i < gameConfig->getSections().size(); i++) {
auto& section = gameConfig->getSections()[i];

Expand Down
Loading