diff --git a/CMakeLists.txt b/CMakeLists.txt index 5bbf5a2..498aa13 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) @@ -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") diff --git a/README.md b/README.md index 009cdb0..ce3f22c 100644 --- a/README.md +++ b/README.md @@ -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", diff --git a/res/config/p2ce.json b/res/config/p2ce.json index 397da31..1c45056 100644 --- a/res/config/p2ce.json +++ b/res/config/p2ce.json @@ -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": [ @@ -116,6 +116,11 @@ "name": "SDK Tools", "type": "directory", "action": "${ROOT}/sdk_tools" + }, + { + "name": "Steam SourceMods", + "type": "directory", + "action": "${SOURCEMODS}" } ] } diff --git a/src/GameConfig.cpp b/src/GameConfig.cpp index 0640722..95a72d4 100644 --- a/src/GameConfig.cpp +++ b/src/GameConfig.cpp @@ -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); diff --git a/src/NewModDialog.cpp b/src/NewModDialog.cpp index 9800b7b..fee8fcb 100644 --- a/src/NewModDialog.cpp +++ b/src/NewModDialog.cpp @@ -21,88 +21,10 @@ #include #include -#ifdef _WIN32 - #include - #include -#else - #include -#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 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(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 ""; @@ -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); @@ -352,7 +274,7 @@ QString NewModDialog::getModInstallDirParent() const { } switch (selectedIndex) { case 0: - return ::getSourceModsDirLocation(); + return ::getSourceModsDir(); case 1: return this->gameRoot; default: diff --git a/src/Steam.cpp b/src/Steam.cpp new file mode 100644 index 0000000..5742bdf --- /dev/null +++ b/src/Steam.cpp @@ -0,0 +1,86 @@ +#include "Steam.h" + +#include +#ifdef _WIN32 + #include + #include +#else + #include +#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 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(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; +} diff --git a/src/Steam.h b/src/Steam.h new file mode 100644 index 0000000..10270ba --- /dev/null +++ b/src/Steam.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +[[nodiscard]] const QString& getSourceModsDir(); diff --git a/src/Window.cpp b/src/Window.cpp index 8be3d9e..022e785 100644 --- a/src/Window.cpp +++ b/src/Window.cpp @@ -21,6 +21,7 @@ #include "LaunchButton.h" #include "NewModDialog.h" #include "NewP2CEAddonDialog.h" +#include "Steam.h" #ifdef _WIN32 #include @@ -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()}); @@ -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")); @@ -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 @@ -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); @@ -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); @@ -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); @@ -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} @@ -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]; diff --git a/src/Window.h b/src/Window.h index cf242b1..38a82e3 100644 --- a/src/Window.h +++ b/src/Window.h @@ -20,13 +20,14 @@ class Window : public QMainWindow { void regenerateRecentConfigs(); private: + QString gameDefault; bool configUsingLegacyBinDir; QString configModTemplateURL; QMenu* recent; QAction* config_loadDefault; - QAction* game_resetToDefault; QAction* game_overrideGame; + QAction* game_resetToDefault; QAction* utilities_createNewMod; QAction* utilities_createNewAddon;