diff --git a/CMakeLists.txt b/CMakeLists.txt index 217b444..740aec2 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 "A minimal SDK launcher for Strata Source engine games" - VERSION "0.2.0" + VERSION "0.3.0" HOMEPAGE_URL "https://github.com/StrataSource/sdk-launcher") set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -152,3 +152,5 @@ target_include_directories( "${QT_INCLUDE}/QtCore" "${QT_INCLUDE}/QtGui" "${QT_INCLUDE}/QtWidgets") + +set_target_properties(${PROJECT_TARGET_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "C:/Program Files (x86)/Steam/steamapps/common/Portal 2 Community Edition/bin/win64") diff --git a/README.md b/README.md index 23a8739..99d3efd 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,15 @@ Here is an example config file that may be loaded into the SDK launcher. ```json5 { // The name of the game directory - "game": "p2ce", - // Optional, the default is game.ico (searches in the /resource/ directory) - "game_icon": "game.ico", - // Optional, the default is false (set this to true if the SDK launcher is inside bin/ instead of bin//) + "game_default": "p2ce", + // Optional, the default is "${ROOT}/${GAME}/resource/game.ico" + "game_icon": "${ROOT}/${GAME}/resource/game.ico", + // Optional, the default is false (set this to true if the SDK launcher is inside bin/ instead of bin/${PLATFORM}/) "uses_legacy_bin_dir": false, + // Optional, the default is 256 (changes the fixed width of the window) + "window_width": 256, + // Optional, the default is 450 (changes the fixed height of the window) + "window_height": 450, // Sections hold titled groups of buttons "sections": [ { @@ -44,10 +48,10 @@ Here is an example config file that may be loaded into the SDK launcher. // of the action is command, ".exe" will be appended to search for the // default icon on Windows. Link types are Internet URLs, and directory // types are directories that open in a file explorer. - "action": "${ROOT}/bin/${PLATFORM}/strata", // Expands to "./bin/win64/strata" on Windows. + "action": "${ROOT}/bin/${PLATFORM}/strata", // Expands to "/bin/win64/strata" on Windows. // Arguments are optional for command-type actions, and will be passed - // to the command when ran. - "arguments": ["-game", "p2ce", "-dev"], + // to the command when ran. ${GAME} expands to the game directory name. + "arguments": ["-game", "${GAME}", "-dev"], // The icon override (optional) allows the config creator to change the // icon for any button. ${GAME_ICON} is a special keyword for steam-type // configs which loads the game icon from Steam. ${STRATA_ICON} is a @@ -65,7 +69,7 @@ Here is an example config file that may be loaded into the SDK launcher. "name": "Portal 2: CE - Dev Mode && Tools Mode", "type": "command", "action": "${ROOT}/bin/${PLATFORM}/strata", - "arguments": ["-game", "p2ce", "-dev", "-tools"], + "arguments": ["-game", "${GAME}", "-dev", "-tools"], "icon_override": "${GAME_ICON}" } ] diff --git a/res/config/momentum.json b/res/config/momentum.json index 2392815..618945c 100644 --- a/res/config/momentum.json +++ b/res/config/momentum.json @@ -1,5 +1,5 @@ { - "game": "momentum", + "game_default": "momentum", "sections": [ { "name": "Game", @@ -8,14 +8,14 @@ "name": "Momentum Mod - Dev Mode", "type": "command", "action": "${ROOT}/bin/${PLATFORM}/chaos", - "arguments": ["-game", "momentum", "-dev", "-console"], + "arguments": ["-game", "${GAME}", "-dev", "-console"], "icon_override": "${GAME_ICON}" }, { "name": "Momentum Mod - Dev && Tools Mode", "type": "command", "action": "${ROOT}/bin/${PLATFORM}/chaos", - "arguments": ["-game", "momentum", "-dev", "-console", "-tools"], + "arguments": ["-game", "${GAME}", "-dev", "-console", "-tools"], "icon_override": "${GAME_ICON}" } ] @@ -27,21 +27,21 @@ "name": "Hammer Editor", "type": "command", "action": "${ROOT}/bin/win64/hammer", - "arguments": ["-game", "momentum"], + "arguments": ["-game", "${GAME}"], "os": "windows" }, { "name": "Model Viewer", "type": "command", "action": "${ROOT}/bin/win64/hlmv", - "arguments": ["-game", "momentum"], + "arguments": ["-game", "${GAME}"], "os": "windows" }, { "name": "Face Poser", "type": "command", "action": "${ROOT}/bin/win64/hlfaceposer", - "arguments": ["-game", "momentum"], + "arguments": ["-game", "${GAME}"], "os": "windows" } ] diff --git a/res/config/p2ce.json b/res/config/p2ce.json index e71b7bd..51e5199 100644 --- a/res/config/p2ce.json +++ b/res/config/p2ce.json @@ -1,5 +1,6 @@ { - "game": "p2ce", + "game_default": "p2ce", + "window_height": 575, "sections": [ { "name": "Game", @@ -8,14 +9,14 @@ "name": "Portal 2: CE - Dev Mode", "type": "command", "action": "${ROOT}/bin/${PLATFORM}/chaos", - "arguments": ["-game", "p2ce", "-dev", "-console"], + "arguments": ["-game", "${GAME}", "-dev", "-console"], "icon_override": "${GAME_ICON}" }, { "name": "Portal 2: CE - Dev && Tools Mode", "type": "command", "action": "${ROOT}/bin/${PLATFORM}/chaos", - "arguments": ["-game", "p2ce", "-dev", "-console", "-tools"], + "arguments": ["-game", "${GAME}", "-dev", "-console", "-tools"], "icon_override": "${GAME_ICON}" } ] @@ -27,21 +28,21 @@ "name": "Hammer Editor", "type": "command", "action": "${ROOT}/bin/win64/hammer", - "arguments": ["-game", "p2ce"], + "arguments": ["-game", "${GAME}"], "os": "windows" }, { "name": "Model Viewer", "type": "command", "action": "${ROOT}/bin/win64/hlmv", - "arguments": ["-game", "p2ce"], + "arguments": ["-game", "${GAME}"], "os": "windows" }, { "name": "Face Poser", "type": "command", "action": "${ROOT}/bin/win64/hlfaceposer", - "arguments": ["-game", "p2ce"], + "arguments": ["-game", "${GAME}"], "os": "windows" }, { @@ -58,7 +59,7 @@ "name": "Workshop Uploader", "type": "command", "action": "${ROOT}/bin/${PLATFORM}/workshopgui", - "arguments": ["-game", "p2ce"] + "arguments": ["-game", "${GAME}"] } ] }, diff --git a/res/config/revolution.json b/res/config/revolution.json index fe6335c..769566d 100644 --- a/res/config/revolution.json +++ b/res/config/revolution.json @@ -1,5 +1,5 @@ { - "game": "revolution", + "game_default": "revolution", "sections": [ { "name": "Game", @@ -8,21 +8,14 @@ "name": "Portal: Revolution - Dev Mode", "type": "command", "action": "${ROOT}/bin/${PLATFORM}/revolution", - "arguments": [ - "-dev", - "-console" - ], + "arguments": ["-game", "${GAME}", "-dev", "-console"], "icon_override": "${GAME_ICON}" }, { "name": "Portal: Revolution - Dev && Tools Mode", "type": "command", "action": "${ROOT}/bin/${PLATFORM}/revolution", - "arguments": [ - "-dev", - "-console", - "-tools" - ], + "arguments": ["-game", "${GAME}", "-dev", "-console", "-tools"], "icon_override": "${GAME_ICON}" } ] @@ -34,30 +27,21 @@ "name": "Hammer Editor", "type": "command", "action": "${ROOT}/bin/win64/hammer", - "arguments": [ - "-game", - "revolution" - ], + "arguments": ["-game", "${GAME}"], "os": "windows" }, { "name": "Model Viewer", "type": "command", "action": "${ROOT}/bin/win64/hlmv", - "arguments": [ - "-game", - "revolution" - ], + "arguments": ["-game", "${GAME}"], "os": "windows" }, { "name": "Face Poser", "type": "command", "action": "${ROOT}/bin/win64/hlfaceposer", - "arguments": [ - "-game", - "revolution" - ], + "arguments": ["-game", "${GAME}"], "os": "windows" } ] diff --git a/src/GameConfig.cpp b/src/GameConfig.cpp index f9489ee..8a2a25e 100644 --- a/src/GameConfig.cpp +++ b/src/GameConfig.cpp @@ -50,21 +50,29 @@ std::optional GameConfig::parse(const QString& path) { GameConfig gameConfig; - if (!configObject.contains("game") || !configObject["game"].isString()) { + if (!configObject.contains("game_default") || !configObject["game_default"].isString()) { return std::nullopt; } - gameConfig.game = configObject["game"].toString(); + gameConfig.gameDefault = configObject["game_default"].toString(); if (configObject.contains("game_icon") && configObject["game_icon"].isString()) { gameConfig.gameIcon = configObject["game_icon"].toString(); } else { - gameConfig.gameIcon = "game.ico"; + gameConfig.gameIcon = "${ROOT}/${GAME}/resource/game.ico"; } if (configObject.contains("uses_legacy_bin_dir") && configObject["uses_legacy_bin_dir"].isBool()) { gameConfig.usesLegacyBinDir = configObject["uses_legacy_bin_dir"].toBool(); } + if (configObject.contains("window_width")) { + gameConfig.windowWidth = configObject["window_width"].toInt(DEFAULT_WINDOW_WIDTH); + } + + if (configObject.contains("window_height")) { + gameConfig.windowHeight = configObject["window_height"].toInt(DEFAULT_WINDOW_HEIGHT); + } + if (!configObject.contains("sections") || !configObject["sections"].isArray()) { return std::nullopt; } @@ -130,3 +138,21 @@ std::optional GameConfig::parse(const QString& path) { } return gameConfig; } + +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->gameIcon); + for (auto& section : this->sections) { + setVar(section.name); + for (auto& entry : section.entries) { + setVar(entry.name); + setVar(entry.action); + for (auto& argument : entry.arguments) { + setVar(argument); + } + setVar(entry.iconOverride); + } + } +} diff --git a/src/GameConfig.h b/src/GameConfig.h index e0b0916..616856e 100644 --- a/src/GameConfig.h +++ b/src/GameConfig.h @@ -4,6 +4,9 @@ #include #include +constexpr int DEFAULT_WINDOW_WIDTH = 256; +constexpr int DEFAULT_WINDOW_HEIGHT = 450; + class GameConfig { public: enum class ActionType : unsigned char { @@ -39,18 +42,26 @@ class GameConfig { [[nodiscard]] static std::optional parse(const QString& path); - [[nodiscard]] const QString& getGame() const { return this->game; } + [[nodiscard]] const QString& getGameDefault() const { return this->gameDefault; } [[nodiscard]] const QString& getGameIcon() const { return this->gameIcon; } [[nodiscard]] bool getUsesLegacyBinDir() const { return this->usesLegacyBinDir; } + [[nodiscard]] int getWindowWidth() const { return this->windowWidth; } + + [[nodiscard]] int getWindowHeight() const { return this->windowHeight; } + [[nodiscard]] const QList
& getSections() const { return this->sections; } + void setVariable(const QString& variable, const QString& replacement); + private: - QString game; + QString gameDefault; QString gameIcon; bool usesLegacyBinDir = false; + int windowWidth = DEFAULT_WINDOW_WIDTH; + int windowHeight = DEFAULT_WINDOW_HEIGHT; QList
sections; GameConfig() = default; diff --git a/src/Window.cpp b/src/Window.cpp index 68109b9..044f567 100644 --- a/src/Window.cpp +++ b/src/Window.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -50,24 +51,28 @@ QIcon getExecutableIcon(const QString& path) { } #endif -constexpr int FIXED_WINDOW_WIDTH = 256; - constexpr std::string_view STR_RECENT_CONFIGS = "str_recent_configs"; +constexpr std::string_view STR_GAME_OVERRIDE = "str_game_override"; } // namespace Window::Window(QWidget* parent) : QMainWindow(parent) { this->setWindowTitle(PROJECT_NAME.data()); - this->setFixedSize(FIXED_WINDOW_WIDTH, 450); + + // 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(getStrataIcon()); + this->setWindowIcon(QIcon{getStrataIconPath()}); - // Profile menu + // Config menu auto* configMenu = this->menuBar()->addMenu(tr("Config")); - this->loadDefault = configMenu->addAction("Load Default", [this] { + this->config_loadDefault = configMenu->addAction("Load Default", Qt::CTRL | Qt::Key_R, [this] { this->loadGameConfig(QString(":/config/%1.json").arg(PROJECT_DEFAULT_MOD.data())); }); @@ -83,13 +88,34 @@ Window::Window(QWidget* parent) this->recent = configMenu->addMenu(this->style()->standardIcon(QStyle::SP_FileDialogDetailedView), tr("Load Recent...")); // Will be regenerated naturally later on + // 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\"").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\"").arg(settings.value(STR_GAME_OVERRIDE).toString()), [this] { + if (auto text = QInputDialog::getText(this, tr("Set Game Override"), tr("New game folder to use:")); !text.isEmpty()) { + QSettings settings; + settings.setValue(STR_GAME_OVERRIDE, text); + this->game_overrideGame->setText(tr("Override \"%1\"").arg(settings.value(STR_GAME_OVERRIDE).toString())); + this->loadGameConfig(settings.value(STR_RECENT_CONFIGS).toStringList().first()); + } + }); + // Help menu auto* helpMenu = this->menuBar()->addMenu(tr("Help")); helpMenu->addAction(this->style()->standardIcon(QStyle::SP_DialogHelpButton), tr("About"), Qt::Key_F1, [this] { QMessageBox about(this); about.setWindowTitle(tr("About")); - about.setIconPixmap(getStrataIcon().pixmap(64, 64)); + about.setIconPixmap(QIcon{getStrataIconPath()}.pixmap(64, 64)); about.setTextFormat(Qt::TextFormat::MarkdownText); about.setText(QString("## %1\n*Created by Strata Source Contributors*\n
\n").arg(PROJECT_TITLE.data())); about.exec(); @@ -112,7 +138,7 @@ Window::Window(QWidget* parent) new QVBoxLayout(this->main); - if (QSettings settings; !settings.contains(STR_RECENT_CONFIGS)) { + if (!settings.contains(STR_RECENT_CONFIGS)) { settings.setValue(STR_RECENT_CONFIGS, QStringList{}); if (auto defaultConfigPath = QCoreApplication::applicationDirPath() + "/SDKLauncherDefault.json"; QFile::exists(defaultConfigPath)) { this->loadGameConfig(defaultConfigPath); @@ -124,11 +150,11 @@ Window::Window(QWidget* parent) } } -QIcon Window::getStrataIcon() { +QString Window::getStrataIconPath() { if (QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark) { - return QIcon{":/icons/strata_dark.png"}; + return ":/icons/strata_dark.png"; } - return QIcon{":/icons/strata_light.png"}; + return ":/icons/strata_light.png"; } void Window::loadGameConfig(const QString& path) { @@ -143,6 +169,8 @@ void Window::loadGameConfig(const QString& path) { return; } + this->setFixedSize(gameConfig->getWindowWidth(), gameConfig->getWindowHeight()); + QSettings settings; auto recentConfigs = settings.value(STR_RECENT_CONFIGS).value(); if (recentConfigs.contains(path)) { @@ -155,20 +183,53 @@ void Window::loadGameConfig(const QString& path) { settings.setValue(STR_RECENT_CONFIGS, recentConfigs); this->regenerateRecentConfigs(); + // Set ${ROOT} QString rootPath = QCoreApplication::applicationDirPath(); if (gameConfig->getUsesLegacyBinDir()) { rootPath += "/.."; } else { rootPath += "/../.."; } + gameConfig->setVariable("ROOT", rootPath); + + // Set ${PLATFORM} +#if defined(_WIN32) + gameConfig->setVariable("PLATFORM", "win64"); +#elif defined(__APPLE__) + gameConfig->setVariable("PLATFORM", "osx64"); +#elif defined(__linux__) + gameConfig->setVariable("PLATFORM", "linux64"); +#else + #warning "Unknown platform! ${PLATFORM} will not be substituted!" +#endif + + // tiny hack: get default game icon before ${GAME} substitution + QString defaultGameIconPath = gameConfig->getGameIcon(); + defaultGameIconPath.replace("${GAME}", PROJECT_DEFAULT_MOD.data()); + if (QIcon defaultGameIcon{defaultGameIconPath}; !defaultGameIcon.isNull() && !defaultGameIcon.availableSizes().isEmpty()) { + this->config_loadDefault->setIcon(defaultGameIcon); + this->game_resetToDefault->setIcon(defaultGameIcon); + } else { + this->config_loadDefault->setIcon(this->style()->standardIcon(QStyle::SP_FileLinkIcon)); + this->game_resetToDefault->setIcon(this->style()->standardIcon(QStyle::SP_FileLinkIcon)); + } - QIcon gameIcon{QString("%1/%2/resource/%3").arg(rootPath, gameConfig->getGame(), gameConfig->getGameIcon())}; - if (!gameIcon.isNull() && !gameIcon.availableSizes().isEmpty()) { - this->loadDefault->setIcon(gameIcon); + // Set ${GAME} + QString gameDir = settings.contains(STR_GAME_OVERRIDE) ? settings.value(STR_GAME_OVERRIDE).toString() : gameConfig->getGameDefault(); + gameConfig->setVariable("GAME", gameDir); + + // Set ${GAME_ICON} + if (QIcon gameIcon{gameConfig->getGameIcon()}; !gameIcon.isNull() && !gameIcon.availableSizes().isEmpty()) { + this->game_overrideGame->setIcon(gameIcon); + gameConfig->setVariable("GAME_ICON", gameConfig->getGameIcon()); } else { - this->loadDefault->setIcon(this->style()->standardIcon(QStyle::SP_FileLinkIcon)); + this->game_overrideGame->setIcon(this->style()->standardIcon(QStyle::SP_FileLinkIcon)); + gameConfig->setVariable("GAME_ICON", ""); } + // Set ${STRATA_ICON} + gameConfig->setVariable("STRATA_ICON", getStrataIconPath()); + for (int i = 0; i < gameConfig->getSections().size(); i++) { auto& section = gameConfig->getSections()[i]; @@ -180,7 +241,7 @@ void Window::loadGameConfig(const QString& path) { line->setFrameShape(QFrame::HLine); layout->addWidget(line); - for (const auto& entry : section.entries) { + for (auto& entry : section.entries) { auto* button = new QToolButton(this->main); button->setStyleSheet( "QToolButton { background-color: rgba( 0, 0, 0, 0); border: none; }\n" @@ -189,42 +250,23 @@ void Window::loadGameConfig(const QString& path) { button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); button->setText(entry.name); button->setIconSize({16, 16}); - button->setFixedWidth(FIXED_WINDOW_WIDTH - 18); + button->setFixedWidth(gameConfig->getWindowWidth() - 18); layout->addWidget(button); bool iconSet = false; if (!entry.iconOverride.isEmpty()) { - if (entry.iconOverride == "${GAME_ICON}") { - if (!gameIcon.isNull() && !gameIcon.availableSizes().isEmpty()) { - button->setIcon(gameIcon); - } else { - button->setIcon(this->style()->standardIcon(QStyle::SP_FileLinkIcon)); - } - } else if (entry.iconOverride == "${STRATA_ICON}") { - button->setIcon(getStrataIcon()); - } else { - button->setIcon(QIcon{entry.iconOverride}); - } + button->setIcon(QIcon{entry.iconOverride}); iconSet = true; } QString action = entry.action; - action.replace("${ROOT}", rootPath); -#if defined(_WIN32) - action.replace("${PLATFORM}", "win64"); -#elif defined(__APPLE__) - action.replace("${PLATFORM}", "osx64"); -#elif defined(__linux__) - action.replace("${PLATFORM}", "linux64"); -#else - #warning "Unknown platform! ${PLATFORM} will not be substituted!" -#endif if (action.endsWith('/') || action.endsWith('\\')) { action = action.sliced(0, action.size() - 1); } switch (entry.type) { case GameConfig::ActionType::INVALID: button->setIcon(this->style()->standardIcon(QStyle::SP_MessageBoxCritical)); + button->setToolTip(tr("This button has an invalid type. Check the config for any spelling errors.")); break; case GameConfig::ActionType::COMMAND: if (!iconSet) { @@ -238,6 +280,7 @@ void Window::loadGameConfig(const QString& path) { button->setIcon(this->style()->standardIcon(QStyle::SP_FileLinkIcon)); #endif } + button->setToolTip(action + " " + entry.arguments.join(" ")); QObject::connect(button, &QToolButton::clicked, this, [this, action, args=entry.arguments, cwd=rootPath] { auto* process = new QProcess; QObject::connect(process, &QProcess::errorOccurred, this, [this](QProcess::ProcessError code) { @@ -271,6 +314,7 @@ void Window::loadGameConfig(const QString& path) { if (!iconSet) { button->setIcon(this->style()->standardIcon(QStyle::SP_MessageBoxInformation)); } + button->setToolTip(action); QObject::connect(button, &QToolButton::clicked, this, [action] { QDesktopServices::openUrl({action}); }); @@ -279,6 +323,7 @@ void Window::loadGameConfig(const QString& path) { if (!iconSet) { button->setIcon(this->style()->standardIcon(QStyle::SP_DirLinkIcon)); } + button->setToolTip(action); QObject::connect(button, &QToolButton::clicked, this, [action] { QDesktopServices::openUrl({QString("file:///") + action}); }); diff --git a/src/Window.h b/src/Window.h index f0b69b6..d42c786 100644 --- a/src/Window.h +++ b/src/Window.h @@ -11,7 +11,7 @@ class Window : public QMainWindow { public: explicit Window(QWidget* parent = nullptr); - [[nodiscard]] static QIcon getStrataIcon(); + [[nodiscard]] static QString getStrataIconPath(); void loadGameConfig(const QString& path); @@ -19,7 +19,9 @@ class Window : public QMainWindow { private: QMenu* recent; - QAction* loadDefault; + QAction* config_loadDefault; + QAction* game_resetToDefault; + QAction* game_overrideGame; QWidget* main; };