From ffc36e7f3de0b68c7eefd6aa3ce5176ed25504b0 Mon Sep 17 00:00:00 2001 From: Tyler Hoyt Date: Mon, 11 Nov 2024 19:14:17 -0500 Subject: [PATCH 1/7] Added cli option for user-path --- include/Options.h | 1 + src/GameStart.cpp | 39 +++++++++++++++++++++------------------ src/Options.cpp | 7 +++++++ 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/include/Options.h b/include/Options.h index 279a8d4..da56051 100644 --- a/include/Options.h +++ b/include/Options.h @@ -19,6 +19,7 @@ struct Options { bool no_download = false; bool no_update = false; bool no_launch = false; + const char* user_path = nullptr; const char **game_arguments = nullptr; int game_arguments_length = 0; const char** argv = nullptr; diff --git a/src/GameStart.cpp b/src/GameStart.cpp index 6216313..88b9a26 100644 --- a/src/GameStart.cpp +++ b/src/GameStart.cpp @@ -31,28 +31,31 @@ std::string GetGamePath() { static std::string Path; if (!Path.empty()) return Path; - - HKEY hKey; - LPCTSTR sk = "Software\\BeamNG\\BeamNG.drive"; - LONG openRes = RegOpenKeyEx(HKEY_CURRENT_USER, sk, 0, KEY_ALL_ACCESS, &hKey); - if (openRes != ERROR_SUCCESS) { - fatal("Please launch the game at least once!"); - } - Path = QueryKey(hKey, 4); - - if (Path.empty()) { - Path = ""; - char appDataPath[MAX_PATH]; - HRESULT result = SHGetFolderPathA(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, appDataPath); - if (SUCCEEDED(result)) { - Path = appDataPath; + if (options.user_path) { + Path = options.user_path; + } else { + HKEY hKey; + LPCTSTR sk = "Software\\BeamNG\\BeamNG.drive"; + LONG openRes = RegOpenKeyEx(HKEY_CURRENT_USER, sk, 0, KEY_ALL_ACCESS, &hKey); + if (openRes != ERROR_SUCCESS) { + fatal("Please launch the game at least once!"); } + Path = QueryKey(hKey, 4); if (Path.empty()) { - fatal("Cannot get Local Appdata directory"); - } + Path = ""; + char appDataPath[MAX_PATH]; + HRESULT result = SHGetFolderPathA(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, appDataPath); + if (SUCCEEDED(result)) { + Path = appDataPath; + } - Path += "\\BeamNG.drive\\"; + if (Path.empty()) { + fatal("Cannot get Local Appdata directory"); + } + + Path += "\\BeamNG.drive\\"; + } } std::string Ver = CheckVer(GetGameDir()); diff --git a/src/Options.cpp b/src/Options.cpp index 6d2f0c0..600152d 100644 --- a/src/Options.cpp +++ b/src/Options.cpp @@ -86,6 +86,12 @@ void InitOptions(int argc, const char *argv[], Options &options) { options.no_download = true; options.no_launch = true; options.no_update = true; + } else if (argument == "--user-path") { + if (i + 1 >= argc) { + error("No user path specified after flag"); + } + options.user_path = argv[i + 1]; + i++; } else if (argument == "--" || argument == "--game") { options.game_arguments = &argv[i + 1]; options.game_arguments_length = argc - i - 1; @@ -101,6 +107,7 @@ void InitOptions(int argc, const char *argv[], Options &options) { "\t--no-update Skip applying launcher updates (you must update manually)\n" "\t--no-launch Skip launching the game (you must launch the game manually)\n" "\t--dev Developer mode, same as --verbose --no-download --no-launch --no-update\n" + "\t--user-path Path to BeamNG's User Path\n" "\t--game Passes ALL following arguments to the game, see also `--`\n" << std::flush; exit(0); From a63f1bd27c1e73a9f83af4b6b56f8a3c76f05751 Mon Sep 17 00:00:00 2001 From: Tixx <83774803+WiserTixx@users.noreply.github.com> Date: Fri, 6 Dec 2024 18:49:23 +0100 Subject: [PATCH 2/7] Check if user path argument exists --- src/GameStart.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GameStart.cpp b/src/GameStart.cpp index 88b9a26..2d50176 100644 --- a/src/GameStart.cpp +++ b/src/GameStart.cpp @@ -31,7 +31,7 @@ std::string GetGamePath() { static std::string Path; if (!Path.empty()) return Path; - if (options.user_path) { + if (options.user_path && std::filesystem::exists(options.user_path)) { Path = options.user_path; } else { HKEY hKey; From f9d347bd9b4157f1e060aa1391247fd8fd008ffd Mon Sep 17 00:00:00 2001 From: Tixx <83774803+WiserTixx@users.noreply.github.com> Date: Fri, 6 Dec 2024 19:51:24 +0100 Subject: [PATCH 3/7] Look for a userfolder specficied in the game's ini config --- src/GameStart.cpp | 65 +++++++++++++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 22 deletions(-) diff --git a/src/GameStart.cpp b/src/GameStart.cpp index 2d50176..0bb8b7f 100644 --- a/src/GameStart.cpp +++ b/src/GameStart.cpp @@ -23,45 +23,66 @@ #include #include #include "Options.h" +#include unsigned long GamePID = 0; #if defined(_WIN32) std::string QueryKey(HKEY hKey, int ID); std::string GetGamePath() { - static std::string Path; + static std::filesystem::path Path; if (!Path.empty()) - return Path; - if (options.user_path && std::filesystem::exists(options.user_path)) { - Path = options.user_path; - } else { - HKEY hKey; - LPCTSTR sk = "Software\\BeamNG\\BeamNG.drive"; - LONG openRes = RegOpenKeyEx(HKEY_CURRENT_USER, sk, 0, KEY_ALL_ACCESS, &hKey); - if (openRes != ERROR_SUCCESS) { - fatal("Please launch the game at least once!"); - } - Path = QueryKey(hKey, 4); + return Path.string(); + + if (options.user_path) { + if (std::filesystem::exists(options.user_path)) { + Path = options.user_path; + debug("Using custom user folder path: " + Path.string()); + } else + warn("Invalid or non-existent path (" + std::string(options.user_path) + ") specified using --user-path, skipping"); + } + + if (std::string startupIniPath = GetGameDir() + "\\startup.ini"; std::filesystem::exists(startupIniPath)) { + + std::ifstream startupIni(startupIniPath); + std::string line; + while (std::getline(startupIni, line)) + if (line.find("UserPath = ") == 0) { + std::string userPath = line.substr(11); + if (!userPath.empty()) + if (std::filesystem::exists(userPath)) { + Path = userPath; + debug("Using custom user folder path from startup.ini: " + Path.string()); + } else + warn("Found custom user folder path ("+ userPath + ") in startup.ini but it doesn't exist, skipping"); + + } if (Path.empty()) { - Path = ""; - char appDataPath[MAX_PATH]; - HRESULT result = SHGetFolderPathA(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, appDataPath); - if (SUCCEEDED(result)) { - Path = appDataPath; + HKEY hKey; + LPCTSTR sk = "Software\\BeamNG\\BeamNG.drive"; + LONG openRes = RegOpenKeyEx(HKEY_CURRENT_USER, sk, 0, KEY_ALL_ACCESS, &hKey); + if (openRes != ERROR_SUCCESS) { + fatal("Please launch the game at least once!"); } + Path = QueryKey(hKey, 4); if (Path.empty()) { - fatal("Cannot get Local Appdata directory"); - } + char appDataPath[MAX_PATH]; + HRESULT result = SHGetFolderPathA(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, appDataPath); - Path += "\\BeamNG.drive\\"; + if (!SUCCEEDED(result)) { + fatal("Cannot get Local Appdata directory"); + } + + Path = std::filesystem::path(appDataPath) / "BeamNG.drive"; + } } } std::string Ver = CheckVer(GetGameDir()); Ver = Ver.substr(0, Ver.find('.', Ver.find('.') + 1)); - Path += Ver + "\\"; - return Path; + Path = Path / (Ver + "\\"); + return Path.string(); } #elif defined(__linux__) std::string GetGamePath() { From ca93effb7d2b94e86a889ce3750c8ead111cb69a Mon Sep 17 00:00:00 2001 From: Tixx <83774803+WiserTixx@users.noreply.github.com> Date: Sat, 26 Apr 2025 18:46:15 +0200 Subject: [PATCH 4/7] Update --user-path error message Co-authored-by: Lion --- src/Options.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Options.cpp b/src/Options.cpp index 600152d..0f08a83 100644 --- a/src/Options.cpp +++ b/src/Options.cpp @@ -88,7 +88,7 @@ void InitOptions(int argc, const char *argv[], Options &options) { options.no_update = true; } else if (argument == "--user-path") { if (i + 1 >= argc) { - error("No user path specified after flag"); + error("You must specify a path after the `--user-path` argument"); } options.user_path = argv[i + 1]; i++; From 6244fdafc69a4e5dd51a0dfb36b55086b04a5061 Mon Sep 17 00:00:00 2001 From: Tixx <83774803+WiserTixx@users.noreply.github.com> Date: Sat, 26 Apr 2025 19:45:10 +0200 Subject: [PATCH 5/7] Use std::filesystem::operator/ instead of string concat --- src/GameStart.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GameStart.cpp b/src/GameStart.cpp index 0bb8b7f..8174ccb 100644 --- a/src/GameStart.cpp +++ b/src/GameStart.cpp @@ -41,7 +41,7 @@ std::string GetGamePath() { warn("Invalid or non-existent path (" + std::string(options.user_path) + ") specified using --user-path, skipping"); } - if (std::string startupIniPath = GetGameDir() + "\\startup.ini"; std::filesystem::exists(startupIniPath)) { + if (const auto startupIniPath = std::filesystem::path(GetGameDir()) / "startup.ini"; exists(startupIniPath)) { std::ifstream startupIni(startupIniPath); std::string line; From efe9f5b614d5c567431682c520a70260994a629f Mon Sep 17 00:00:00 2001 From: Tixx <83774803+WiserTixx@users.noreply.github.com> Date: Sun, 27 Apr 2025 23:35:11 +0200 Subject: [PATCH 6/7] Add INI parser and function to expand env vars --- include/Utils.h | 89 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/include/Utils.h b/include/Utils.h index 77638ec..33c01d6 100644 --- a/include/Utils.h +++ b/include/Utils.h @@ -5,10 +5,13 @@ */ #pragma once +#include +#include #include #include namespace Utils { + inline std::vector Split(const std::string& String, const std::string& delimiter) { std::vector Val; size_t pos; @@ -23,4 +26,90 @@ namespace Utils { Val.push_back(s); return Val; }; + inline std::string ExpandEnvVars(const std::string& input) { + std::string result; + std::regex envPattern(R"(%([^%]+)%|\$([A-Za-z_][A-Za-z0-9_]*)|\$\{([^}]+)\})"); + + std::sregex_iterator begin(input.begin(), input.end(), envPattern); + std::sregex_iterator end; + + size_t lastPos = 0; + + for (auto it = begin; it != end; ++it) { + const auto& match = *it; + + result.append(input, lastPos, match.position() - lastPos); + + std::string varName; + if (match[1].matched) varName = match[1].str(); // %VAR% + else if (match[2].matched) varName = match[2].str(); // $VAR + else if (match[3].matched) varName = match[3].str(); // ${VAR} + + if (const char* envValue = std::getenv(varName.c_str())) { + result.append(envValue); + } + + lastPos = match.position() + match.length(); + } + + result.append(input, lastPos, input.length() - lastPos); + + return result; + } + inline std::map> ParseINI(const std::string& contents) { + std::map> ini; + + std::string currentSection; + auto sections = Split(contents, "\n"); + + for (size_t i = 0; i < sections.size(); i++) { + std::string line = sections[i]; + if (line.empty() || line[0] == ';' || line[0] == '#') + continue; + + for (auto& c : line) { + if (c == '#' || c == ';') { + line = line.substr(0, &c - &line[0]); + break; + } + } + + auto invalidLineLog = [&]{ + warn("Invalid INI line: " + line); + warn("Surrounding lines: \n" + + (i > 0 ? sections[i - 1] : "") + "\n" + + (i < sections.size() - 1 ? sections[i + 1] : "")); + }; + + if (line[0] == '[') { + currentSection = line.substr(1, line.find(']') - 1); + } else { + + if (currentSection.empty()) { + invalidLineLog(); + continue; + } + + std::string key, value; + size_t pos = line.find('='); + if (pos != std::string::npos) { + key = line.substr(0, pos); + value = line.substr(pos + 1); + ini[currentSection][key] = value; + } else { + invalidLineLog(); + continue; + } + + key.erase(key.find_last_not_of(" \t") + 1); + value.erase(0, value.find_first_not_of(" \t")); + value.erase(value.find_last_not_of(" \t") + 1); + + ini[currentSection][key] = value; + } + } + + return ini; + } + }; \ No newline at end of file From f193c25de6978035edf184d3a2dd2a58b69e80d4 Mon Sep 17 00:00:00 2001 From: Tixx <83774803+WiserTixx@users.noreply.github.com> Date: Sun, 27 Apr 2025 23:35:39 +0200 Subject: [PATCH 7/7] Switch to using INI parser for startup.ini file --- src/GameStart.cpp | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/GameStart.cpp b/src/GameStart.cpp index 8174ccb..7446ae9 100644 --- a/src/GameStart.cpp +++ b/src/GameStart.cpp @@ -18,11 +18,13 @@ #endif #include "Logger.h" +#include "Options.h" #include "Startup.h" +#include "Utils.h" #include #include #include -#include "Options.h" + #include unsigned long GamePID = 0; @@ -43,19 +45,32 @@ std::string GetGamePath() { if (const auto startupIniPath = std::filesystem::path(GetGameDir()) / "startup.ini"; exists(startupIniPath)) { - std::ifstream startupIni(startupIniPath); - std::string line; - while (std::getline(startupIni, line)) - if (line.find("UserPath = ") == 0) { - std::string userPath = line.substr(11); + if (std::ifstream startupIni(startupIniPath); startupIni.is_open()) { + std::string contents((std::istreambuf_iterator(startupIni)), std::istreambuf_iterator()); + startupIni.close(); + + if (contents.size() > 3) { + contents.erase(0, 3); + } + + auto ini = Utils::ParseINI(contents); + if (ini.empty()) { + warn("Failed to parse startup.ini"); + } else + debug("Successfully parsed startup.ini"); + + + std::string userPath; + if (ini.contains("filesystem") && ini["filesystem"].contains("UserPath")) + userPath = ini["filesystem"]["UserPath"]; + if (!userPath.empty()) - if (std::filesystem::exists(userPath)) { + if (userPath = Utils::ExpandEnvVars(userPath); std::filesystem::exists(userPath)) { Path = userPath; debug("Using custom user folder path from startup.ini: " + Path.string()); } else warn("Found custom user folder path ("+ userPath + ") in startup.ini but it doesn't exist, skipping"); - - } + } if (Path.empty()) { HKEY hKey;