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/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 diff --git a/src/GameStart.cpp b/src/GameStart.cpp index 6216313..7446ae9 100644 --- a/src/GameStart.cpp +++ b/src/GameStart.cpp @@ -18,47 +18,86 @@ #endif #include "Logger.h" +#include "Options.h" #include "Startup.h" +#include "Utils.h" #include #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; + return Path.string(); - 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!"); + 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"); } - 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 (const auto startupIniPath = std::filesystem::path(GetGameDir()) / "startup.ini"; exists(startupIniPath)) { + + 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 (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()) { - fatal("Cannot get Local Appdata directory"); - } + 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); - Path += "\\BeamNG.drive\\"; + if (Path.empty()) { + char appDataPath[MAX_PATH]; + HRESULT result = SHGetFolderPathA(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, appDataPath); + + 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() { diff --git a/src/Options.cpp b/src/Options.cpp index 6d2f0c0..0f08a83 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("You must specify a path after the `--user-path` argument"); + } + 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);