Compare commits

..

53 Commits

Author SHA1 Message Date
Tixx
7bef6f35c2
Bump version to 2.5.0 2025-06-28 20:21:17 +02:00
Tixx
b64d645f73
Switch to wstring for paths on windows (#167)
This PR changes everything relating to file system paths to use
std::wstring instead of std::string. This allows for non-latin
characters to be in the user's path, for example in their windows
username.

- [x] Convert windows code to use wstring
- [x] Fix linux build
- [x] Fix hashing

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-06-28 19:25:13 +02:00
Tixx
1780133569
Include assert 2025-06-25 14:29:29 +02:00
Tixx
c89afdf477
Check
Co-authored-by: SaltySnail <51403141+SaltySnail@users.noreply.github.com>
2025-06-24 22:17:14 +02:00
Tixx
9d44146224
Regex assert
Co-authored-by: SaltySnail <51403141+SaltySnail@users.noreply.github.com>
2025-06-24 22:16:17 +02:00
Tixx
a5c02217fa
Update ini parse check formatting
Co-authored-by: SaltySnail <51403141+SaltySnail@users.noreply.github.com>
2025-06-20 23:04:07 +02:00
Tixx
303fc55d94
Fix syntax error 2025-06-19 18:13:46 +02:00
Tixx
5f1e7c6409
Fix resources dir log message for linux 2025-06-19 18:07:59 +02:00
Tixx
8025c0884f
Fix download path generation 2025-06-19 17:56:59 +02:00
Tixx
51d096deac
Check if BeamMP.zip exists before hashing 2025-06-08 14:01:48 +02:00
Tixx
9c53f86593
Convert GetGamePath() to fs::path 2025-06-08 14:01:23 +02:00
Tixx
e0257e9526
Update NewSyncResources logs to use wstring 2025-06-08 13:51:16 +02:00
Tixx
8b96ffb098
Fix .git folder check and GetGamePath() 2025-06-08 13:45:09 +02:00
Tixx
6c740e2562
Fix merge 2025-06-08 13:34:15 +02:00
Tixx
06c741edc5
Fix wstring for windows and add crossplatform code for fs strings 2025-06-08 13:34:15 +02:00
Tixx
5e448dc34f
Switch to wstring for paths on windows 2025-06-08 13:34:15 +02:00
Tixx
676084f283
Implement DeleteDuplicateMods option (#190)
Adds `DeleteDuplicateMods` option to the launcher config which, well,
deletes mods with the same name if their hashes mismatch. Useful for
development where client mod hashes can frequently change.

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-06-08 12:56:23 +02:00
Tixx
a8cd208208
Make duplicate mod detection more readable
Co-authored-by: SaltySnail <51403141+SaltySnail@users.noreply.github.com>
2025-06-08 12:37:35 +02:00
Tixx
2529146d5a
Implement DeleteDuplicateMods option 2025-06-08 12:37:32 +02:00
Tixx
8d641f326d
Check if unpacked BeamMP has a .git folder before deleting it (#193)
This PR makes it so that the launcher will first check for the presence
of a .git folder before deleting the unpacked BeamMP directory.
This is useful when you're working on the BeamMP mod and you
accidentally start the launcher without dev mode. (This has happened to
me more times than I would like to admit.)

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-06-07 20:07:23 +02:00
Tixx
5af9f5da36
Show cached mods in download progress (#189)
This PR adds cached mods to the "Loading resources" pop-up in-game and
makes it so that the count is actually right.
This PR also fixes the mod protection message because for some reason I
forced it to be always off 😁.

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-06-07 20:05:52 +02:00
Tixx
baba0ad026
Check if unpacked BeamMP has a .git folder before deleting it 2025-06-07 17:50:20 +02:00
Tixx
b1ebcfc18d
Fix mod protection message 2025-05-27 22:12:07 +02:00
Tixx
187ef3b24f
Show cached mods being loaded in download progress 2025-05-27 22:09:52 +02:00
Tixx
943889d588
Fix mod downloading progress 2025-05-27 22:08:50 +02:00
Tixx
edbd99f389
Added CLI argument for user-path (#148)
Simple addition to the CLI args. It could probably do with some
validation, ie; making sure it ends in a slash.

cc: @WiserTixx
2025-05-05 23:47:11 +02:00
Tixx
0341d401e7
Download check (#184)
This PR makes it so that the mod hash and download confirmation are
verified before proceeding.

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-05-05 23:46:32 +02:00
Tixx
da84d62391
Notify user about missing protected mods (#183)
Launcher implementation for
https://github.com/BeamMP/BeamMP-Server/pull/430. This PR checks if mods
are protected, and if a mod is missing the launcher will notify the user
about it and give them instructions on how to resolve it.

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-05-05 23:46:14 +02:00
Tixx
cc6167cd2e
Bump version 2025-05-03 22:20:55 +02:00
Tixx
d3263acead
Log corrupted download confirmation 2025-05-01 20:54:28 +02:00
Tixx
4de0bc9a40
Check download confirmation packet 2025-05-01 20:47:16 +02:00
Tixx
25a1061700
Verify mod hash after downloading 2025-05-01 20:42:01 +02:00
Tixx
f193c25de6
Switch to using INI parser for startup.ini file 2025-04-27 23:35:39 +02:00
Tixx
efe9f5b614
Add INI parser and function to expand env vars 2025-04-27 23:35:11 +02:00
Tixx
6244fdafc6
Use std::filesystem::operator/ instead of string concat 2025-04-26 19:45:10 +02:00
Tixx
ca93effb7d
Update --user-path error message
Co-authored-by: Lion <development@kortlepel.com>
2025-04-26 18:46:15 +02:00
Tixx
f9d347bd9b
Look for a userfolder specficied in the game's ini config 2025-04-26 18:42:41 +02:00
Tixx
a63f1bd27c
Check if user path argument exists 2025-04-26 18:42:40 +02:00
Tyler Hoyt
ffc36e7f3d
Added cli option for user-path 2025-04-26 18:42:31 +02:00
SaltySnail
e216b6ec06
Get error code when the game fails to launch on windows (#179)
This PR logs the error `CreateProcessA` returns when failing on windows.
This can help with figuring out why the launcher `Failed to Launch the
game!`

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-04-19 21:43:41 +02:00
Tixx
6597fe5e26
Rename windows api variable 2025-04-19 21:23:51 +02:00
Tixx
2fa5d69369
Link to the docs (#181)
Link to the docs instead

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-04-05 12:45:22 +02:00
O1LER
fec80e2c67 Link to the docs 2025-04-05 12:04:57 +02:00
Tixx
dc78883451
Notify user about missing protected mods 2025-04-01 09:09:49 +02:00
Tixx
fa8627a22b
Fix recv return type and better download error handling (#178)
Fixes the recv return value type. This PR corrects the recv return value
type from `int32_t` to `int`. Casting the return value from `int` to
`int32_t` (Currently the case, changed by this pr) would in some cases,
if the transmitted packet was large enough, flip the value causing it to
be a high negative number, which recv will never return. This happens
frequently when downloading big mods over a fast connection.

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-03-29 20:18:19 +01:00
Tixx
dd5256ae22
Increase download speed calculation precision 2025-03-29 00:19:25 +01:00
Tixx
e24cbf61bb
Only fail on socket error or connection closed 2025-03-29 00:19:24 +01:00
Tixx
472e2d16b6
Fix recv return type and better download error handling 2025-03-29 00:19:24 +01:00
Tixx
ae650cc142
Get error code when the game fails to launch on windows 2025-03-28 23:16:37 +01:00
Tixx
ad7177bec8
Include chrono (#176)
By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-03-23 22:38:20 +01:00
Tixx
a4005c5876
Include chrono 2025-03-15 23:06:49 +01:00
Tixx
a3ad6f8700
Properly handle the futures (#172)
This PR makes it so that the std::async calls are actually asynchronous.

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-03-08 22:29:08 +01:00
Tixx
d3bddb0203
Properly handle the future 2025-02-23 22:04:15 +01:00
20 changed files with 576 additions and 275 deletions

View File

@ -2,57 +2,7 @@
The launcher is the way we communitcate to outside the game, it does a few automated actions such as but not limited to: downloading the mod, launching the game, and create a connection to a server.
**To clone this repository**: `git clone --recurse-submodules https://github.com/BeamMP/BeamMP-Launcher.git`
## How to build for Windows
Make sure you have the necessary development tools installed:
[vcpkg](https://vcpkg.io/en/)
### Release
In the root directory of the project,
1. `cmake -DCMAKE_BUILD_TYPE=Release . -B bin -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows-static`
2. `cmake --build bin --parallel --config Release`
Remember to change `C:/vcpkg` to wherever you have vcpkg installed.
### Debug
In the root directory of the project,
1. `cmake . -B bin -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows-static`
2. `cmake --build bin --parallel`
Remember to change `C:/vcpkg` to wherever you have vcpkg installed.
## How to build for Linux
Make sure you have `vcpkg` installed, as well as basic development tools, often found in packages, for example:
- Debian: `sudo apt install build-essential`
- Fedora: `sudo dnf groupinstall "Development Tools"`
- Arch: `sudo pacman -S base-devel`
- openSUSE: `zypper in -t pattern devel-basis`
### Release
In the root directory of the project,
1. `cmake -DCMAKE_BUILD_TYPE=Release . -B bin -DCMAKE_TOOLCHAIN_FILE=~/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-linux`
2. `cmake --build bin --parallel --config Release`
### Debug
In the root directory of the project,
1. `cmake . -B bin -DCMAKE_TOOLCHAIN_FILE=~/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-linux`
2. `cmake --build bin --parallel`
## Running out of RAM while building
Should you run out of RAM while building, you can ommit the `--parallel` instruction, it will then use less RAM due to building only on one CPU thread.
You can also specify a number of threads to use, for example `--parallel 4` will use four CPU threads, but due to the small project size, you may be faster just omitting `--parallel` instead of trying to find the highest possible multithread number
## [Getting started](https://docs.beammp.com/game/getting-started/)
## License

View File

@ -6,10 +6,12 @@
#pragma once
#include "Logger.h"
#include "Utils.h"
#include <string>
class HTTP {
public:
static bool Download(const std::string& IP, const std::string& Path);
static bool Download(const std::string& IP, const beammp_fs_string& Path);
static std::string Post(const std::string& IP, const std::string& Fields);
static std::string Get(const std::string& IP);
static bool ProgressBar(size_t c, size_t t);

View File

@ -14,4 +14,11 @@ void debug(const std::string& toPrint);
void error(const std::string& toPrint);
void info(const std::string& toPrint);
void warn(const std::string& toPrint);
void except(const std::wstring& toPrint);
void fatal(const std::wstring& toPrint);
void debug(const std::wstring& toPrint);
void error(const std::wstring& toPrint);
void info(const std::wstring& toPrint);
void warn(const std::wstring& toPrint);
std::string getDate();

View File

@ -6,6 +6,7 @@
#pragma once
#include <filesystem>
#include <string>
#ifdef __linux__
@ -29,7 +30,8 @@ extern bool Terminate;
extern uint64_t UDPSock;
extern uint64_t TCPSock;
extern std::string Branch;
extern std::string CachingDirectory;
extern std::filesystem::path CachingDirectory;
extern bool deleteDuplicateMods;
extern bool TCPTerminate;
extern std::string LastIP;
extern std::string MStatus;

View File

@ -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;

View File

@ -6,9 +6,9 @@
#pragma once
#include <string>
void PreGame(const std::string& GamePath);
std::string CheckVer(const std::string& path);
void InitGame(const std::string& Dir);
std::string GetGameDir();
void PreGame(const beammp_fs_string& GamePath);
std::string CheckVer(const beammp_fs_string& path);
void InitGame(const beammp_fs_string& Dir);
beammp_fs_string GetGameDir();
void LegitimacyCheck();
void CheckLocalKey();

View File

@ -5,14 +5,16 @@
*/
#pragma once
#include "Utils.h"
#include <compare>
#include <string>
#include <vector>
void InitLauncher();
std::string GetEP(const char* P = nullptr);
std::string GetGamePath();
beammp_fs_string GetEP(const beammp_fs_char* P = nullptr);
std::filesystem::path GetGamePath();
std::string GetVer();
std::string GetPatch();
std::string GetEN();
beammp_fs_string GetEN();
void ConfigInit();

View File

@ -5,10 +5,31 @@
*/
#pragma once
#include <cassert>
#include <filesystem>
#include <fstream>
#include <locale>
#include <map>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <regex>
#include <string>
#include <vector>
#ifdef _WIN32
#define beammp_fs_string std::wstring
#define beammp_fs_char wchar_t
#define beammp_wide(str) L##str
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#else
#define beammp_fs_string std::string
#define beammp_fs_char char
#define beammp_wide(str) str
#endif
namespace Utils {
inline std::vector<std::string> Split(const std::string& String, const std::string& delimiter) {
std::vector<std::string> Val;
size_t pos;
@ -23,4 +44,206 @@ 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;
}
#ifdef _WIN32
inline std::wstring ExpandEnvVars(const std::wstring& input) {
std::wstring result;
std::wregex envPattern(LR"(%([^%]+)%|\$([A-Za-z_][A-Za-z0-9_]*)|\$\{([^}]+)\})");
std::wsregex_iterator begin(input.begin(), input.end(), envPattern);
std::wsregex_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::wstring varName;
assert(match.size() == 4 && "Input regex has incorrect amount of capturing groups");
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 wchar_t* envValue = _wgetenv(varName.c_str())) {
if (envValue != nullptr) {
result.append(envValue);
}
}
lastPos = match.position() + match.length();
}
result.append(input, lastPos, input.length() - lastPos);
return result;
}
#endif
inline std::map<std::string, std::map<std::string, std::string>> ParseINI(const std::string& contents) {
std::map<std::string, std::map<std::string, std::string>> 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;
}
#ifdef _WIN32
inline std::wstring ToWString(const std::string& s) {
if (s.empty()) return std::wstring();
int size_needed = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), nullptr, 0);
if (size_needed <= 0) {
return L"";
}
std::wstring result(size_needed, 0);
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), &result[0], size_needed);
return result;
}
#else
inline std::string ToWString(const std::string& s) {
return s;
}
#endif
inline std::string GetSha256HashReallyFast(const beammp_fs_string& filename) {
try {
EVP_MD_CTX* mdctx;
const EVP_MD* md;
uint8_t sha256_value[EVP_MAX_MD_SIZE];
md = EVP_sha256();
if (md == nullptr) {
throw std::runtime_error("EVP_sha256() failed");
}
mdctx = EVP_MD_CTX_new();
if (mdctx == nullptr) {
throw std::runtime_error("EVP_MD_CTX_new() failed");
}
if (!EVP_DigestInit_ex2(mdctx, md, NULL)) {
EVP_MD_CTX_free(mdctx);
throw std::runtime_error("EVP_DigestInit_ex2() failed");
}
std::ifstream stream(filename, std::ios::binary);
const size_t FileSize = std::filesystem::file_size(filename);
size_t Read = 0;
std::vector<char> Data;
while (Read < FileSize) {
Data.resize(size_t(std::min<size_t>(FileSize - Read, 4096)));
size_t RealDataSize = Data.size();
stream.read(Data.data(), std::streamsize(Data.size()));
if (stream.eof() || stream.fail()) {
RealDataSize = size_t(stream.gcount());
}
Data.resize(RealDataSize);
if (RealDataSize == 0) {
break;
}
if (RealDataSize > 0 && !EVP_DigestUpdate(mdctx, Data.data(), Data.size())) {
EVP_MD_CTX_free(mdctx);
throw std::runtime_error("EVP_DigestUpdate() failed");
}
Read += RealDataSize;
}
unsigned int sha256_len = 0;
if (!EVP_DigestFinal_ex(mdctx, sha256_value, &sha256_len)) {
EVP_MD_CTX_free(mdctx);
throw std::runtime_error("EVP_DigestFinal_ex() failed");
}
EVP_MD_CTX_free(mdctx);
std::string result;
for (size_t i = 0; i < sha256_len; i++) {
char buf[3];
sprintf(buf, "%02x", sha256_value[i]);
buf[2] = 0;
result += buf;
}
return result;
} catch (const std::exception& e) {
error(beammp_wide("Sha256 hashing of '") + filename + beammp_wide("' failed: ") + ToWString(e.what()));
return "";
}
}
};

View File

@ -6,15 +6,17 @@
#include "Logger.h"
#include "Network/network.hpp"
#include "Options.h"
#include "Utils.h"
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <nlohmann/json.hpp>
#include "Options.h"
namespace fs = std::filesystem;
std::string Branch;
std::string CachingDirectory = "./Resources";
std::filesystem::path CachingDirectory = std::filesystem::path("./Resources");
bool deleteDuplicateMods = false;
void ParseConfig(const nlohmann::json& d) {
if (d["Port"].is_number()) {
@ -31,8 +33,8 @@ void ParseConfig(const nlohmann::json& d) {
c = char(tolower(c));
}
if (d.contains("CachingDirectory") && d["CachingDirectory"].is_string()) {
CachingDirectory = d["CachingDirectory"].get<std::string>();
info("Mod caching directory: " + CachingDirectory);
CachingDirectory = std::filesystem::path(d["CachingDirectory"].get<std::string>());
info(beammp_wide("Mod caching directory: ") + beammp_fs_string(CachingDirectory.relative_path()));
}
if (d.contains("Dev") && d["Dev"].is_boolean()) {
@ -42,6 +44,11 @@ void ParseConfig(const nlohmann::json& d) {
options.no_launch = dev;
options.no_update = dev;
}
if (d.contains(("DeleteDuplicateMods")) && d["DeleteDuplicateMods"].is_boolean()) {
deleteDuplicateMods = d["DeleteDuplicateMods"].get<bool>();
}
}
void ConfigInit() {

View File

@ -4,9 +4,7 @@
SPDX-License-Identifier: AGPL-3.0-or-later
*/
#if defined(_WIN32)
#include <windows.h>
#include <shlobj.h>
#elif defined(__linux__)
#include "vdf_parser.hpp"
@ -18,50 +16,86 @@
#endif
#include "Logger.h"
#include "Options.h"
#include "Startup.h"
#include "Utils.h"
#include <Security/Init.h>
#include <filesystem>
#include <thread>
#include "Options.h"
#include <fstream>
unsigned long GamePID = 0;
#if defined(_WIN32)
std::string QueryKey(HKEY hKey, int ID);
std::string GetGamePath() {
static std::string Path;
std::wstring QueryKey(HKEY hKey, int ID);
std::filesystem::path GetGamePath() {
static std::filesystem::path Path;
if (!Path.empty())
return Path;
return Path.wstring();
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(L"Using custom user folder path: " + Path.wstring());
} else
warn(L"Invalid or non-existent path (" + Utils::ToWString(options.user_path) + L") 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<char>());
startupIni.close();
auto ini = Utils::ParseINI(contents);
if (ini.empty())
warn("Failed to parse startup.ini");
else
debug("Successfully parsed startup.ini");
std::wstring userPath;
if (ini.contains("filesystem") && ini["filesystem"].contains("UserPath"))
userPath = Utils::ToWString(ini["filesystem"]["UserPath"]);
if (!userPath.empty())
if (userPath = Utils::ExpandEnvVars(userPath); std::filesystem::exists(userPath)) {
Path = userPath;
debug(L"Using custom user folder path from startup.ini: " + Path.wstring());
} else
warn(L"Found custom user folder path (" + userPath + L") 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()) {
wchar_t* appDataPath = new wchar_t[MAX_PATH];
HRESULT result = SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, appDataPath);
if (!SUCCEEDED(result)) {
fatal("Cannot get Local Appdata directory");
}
Path = std::filesystem::path(appDataPath) / "BeamNG.drive";
delete[] appDataPath;
}
}
}
std::string Ver = CheckVer(GetGameDir());
Ver = Ver.substr(0, Ver.find('.', Ver.find('.') + 1));
Path += Ver + "\\";
Path /= Utils::ToWString(Ver);
return Path;
}
#elif defined(__linux__)
std::string GetGamePath() {
std::filesystem::path GetGamePath() {
// Right now only steam is supported
struct passwd* pw = getpwuid(getuid());
std::string homeDir = pw->pw_dir;
@ -75,29 +109,44 @@ std::string GetGamePath() {
#endif
#if defined(_WIN32)
void StartGame(std::string Dir) {
void StartGame(std::wstring Dir) {
BOOL bSuccess = FALSE;
PROCESS_INFORMATION pi;
STARTUPINFO si = { 0 };
STARTUPINFOW si = { 0 };
si.cb = sizeof(si);
std::string BaseDir = Dir; //+"\\Bin64";
std::wstring BaseDir = Dir; //+"\\Bin64";
// Dir += R"(\Bin64\BeamNG.drive.x64.exe)";
Dir += "\\BeamNG.drive.exe";
std::string gameArgs = "";
Dir += L"\\BeamNG.drive.exe";
std::wstring gameArgs = L"";
for (int i = 0; i < options.game_arguments_length; i++) {
gameArgs += " ";
gameArgs += options.game_arguments[i];
gameArgs += L" ";
gameArgs += Utils::ToWString(options.game_arguments[i]);
}
bSuccess = CreateProcessA(nullptr, (LPSTR)(Dir + gameArgs).c_str(), nullptr, nullptr, TRUE, 0, nullptr, BaseDir.c_str(), &si, &pi);
debug(L"BeamNG executable path: " + Dir);
bSuccess = CreateProcessW(nullptr, (wchar_t*)(Dir + gameArgs).c_str(), nullptr, nullptr, TRUE, 0, nullptr, BaseDir.c_str(), &si, &pi);
if (bSuccess) {
info("Game Launched!");
GamePID = pi.dwProcessId;
WaitForSingleObject(pi.hProcess, INFINITE);
error("Game Closed! launcher closing soon");
} else {
error("Failed to Launch the game! launcher closing soon");
std::string err = "";
DWORD dw = GetLastError();
LPVOID lpErrorMsgBuffer;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, dw,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpErrorMsgBuffer, 0, nullptr)
== 0) {
err = "Unknown error code: " + std::to_string(dw);
} else {
err = "Error " + std::to_string(dw) + ": " + (char*)lpErrorMsgBuffer;
}
error("Failed to Launch the game! launcher closing soon. " + err);
}
std::this_thread::sleep_for(std::chrono::seconds(5));
exit(2);
@ -133,7 +182,7 @@ void StartGame(std::string Dir) {
}
#endif
void InitGame(const std::string& Dir) {
void InitGame(const beammp_fs_string& Dir) {
if (!options.no_launch) {
std::thread Game(StartGame, Dir);
Game.detach();

View File

@ -7,6 +7,7 @@
#include "Logger.h"
#include "Startup.h"
#include "Utils.h"
#include <chrono>
#include <fstream>
#include <sstream>
@ -36,7 +37,7 @@ std::string getDate() {
}
void InitLog() {
std::ofstream LFS;
LFS.open(GetEP() + "Launcher.log");
LFS.open(GetEP() + beammp_wide("Launcher.log"));
if (!LFS.is_open()) {
error("logger file init failed!");
} else
@ -44,7 +45,13 @@ void InitLog() {
}
void addToLog(const std::string& Line) {
std::ofstream LFS;
LFS.open(GetEP() + "Launcher.log", std::ios_base::app);
LFS.open(GetEP() + beammp_wide("Launcher.log"), std::ios_base::app);
LFS << Line.c_str();
LFS.close();
}
void addToLog(const std::wstring& Line) {
std::wofstream LFS;
LFS.open(GetEP() + beammp_wide("Launcher.log"), std::ios_base::app);
LFS << Line.c_str();
LFS.close();
}
@ -82,3 +89,41 @@ void except(const std::string& toPrint) {
std::cout << Print;
addToLog(Print);
}
#ifdef _WIN32
void info(const std::wstring& toPrint) {
std::wstring Print = Utils::ToWString(getDate()) + L"[INFO] " + toPrint + L"\n";
std::wcout << Print;
addToLog(Print);
}
void debug(const std::wstring& toPrint) {
std::wstring Print = Utils::ToWString(getDate()) + L"[DEBUG] " + toPrint + L"\n";
if (options.verbose) {
std::wcout << Print;
}
addToLog(Print);
}
void warn(const std::wstring& toPrint) {
std::wstring Print = Utils::ToWString(getDate()) + L"[WARN] " + toPrint + L"\n";
std::wcout << Print;
addToLog(Print);
}
void error(const std::wstring& toPrint) {
std::wstring Print = Utils::ToWString(getDate()) + L"[ERROR] " + toPrint + L"\n";
std::wcout << Print;
addToLog(Print);
}
void fatal(const std::wstring& toPrint) {
std::wstring Print = Utils::ToWString(getDate()) + L"[FATAL] " + toPrint + L"\n";
std::wcout << Print;
addToLog(Print);
std::this_thread::sleep_for(std::chrono::seconds(5));
std::exit(1);
}
void except(const std::wstring& toPrint) {
std::wstring Print = Utils::ToWString(getDate()) + L"[EXCEP] " + toPrint + L"\n";
std::wcout << Print;
addToLog(Print);
}
#endif

View File

@ -12,6 +12,7 @@
#if defined(_WIN32)
#include <winsock2.h>
#include <ws2tcpip.h>
#include <shellapi.h>
#elif defined(__linux__)
#include <cstring>
#include <errno.h>
@ -141,7 +142,7 @@ void GetServerInfo(std::string Data) {
const std::string buffer = ([&]() -> std::string {
int32_t Header;
std::vector<char> data(sizeof(Header));
int32_t Temp = recv(ISock, data.data(), sizeof(Header), MSG_WAITALL);
int Temp = recv(ISock, data.data(), sizeof(Header), MSG_WAITALL);
auto checkBytes = ([&](const int32_t bytes) -> bool {
if (bytes == 0) {
@ -200,7 +201,17 @@ bool IsAllowedLink(const std::string& Link) {
return std::regex_search(Link, link_match, link_pattern) && link_match.position() == 0;
}
std::vector<std::future<void>> futures;
void Parse(std::string Data, SOCKET CSocket) {
std::erase_if(futures, [](const std::future<void>& f) {
if (f.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
return true;
}
return false;
});
char Code = Data.at(0), SubCode = 0;
if (Data.length() > 1)
SubCode = Data.at(1);
@ -213,9 +224,9 @@ void Parse(std::string Data, SOCKET CSocket) {
Terminate = true;
TCPTerminate = true;
Data.clear();
auto future = std::async(std::launch::async, []() {
futures.push_back(std::async(std::launch::async, []() {
CoreSend("B" + HTTP::Get("https://backend.beammp.com/servers-info"));
});
}));
}
break;
case 'C':
@ -312,9 +323,9 @@ void Parse(std::string Data, SOCKET CSocket) {
}
Data = "N" + Auth.dump();
} else {
auto future = std::async(std::launch::async, [data = std::move(Data)]() {
futures.push_back(std::async(std::launch::async, [data = std::move(Data)]() {
CoreSend("N" + Login(data.substr(data.find(':') + 1)));
});
}));
Data.clear();
}
break;
@ -342,7 +353,8 @@ void Parse(std::string Data, SOCKET CSocket) {
}
void GameHandler(SOCKET Client) {
CoreSocket = Client;
int32_t Size, Temp, Rcv;
int32_t Size, Rcv;
int Temp;
char Header[10] = { 0 };
do {
Rcv = 0;

View File

@ -26,6 +26,7 @@
#include <string>
#include <thread>
#include "Options.h"
#include <chrono>
std::chrono::time_point<std::chrono::high_resolution_clock> PingStart, PingEnd;
bool GConnected = false;
@ -261,7 +262,8 @@ void TCPGameServer(const std::string& IP, int Port) {
NetMainThread = std::make_unique<std::thread>(NetMain, IP, Port);
CServer = false;
}
int32_t Size, Temp, Rcv;
int32_t Size, Rcv;
int Temp;
char Header[10] = { 0 };
// Read byte by byte until '>' is rcved then get the size and read based on it

View File

@ -127,7 +127,7 @@ std::string HTTP::Post(const std::string& IP, const std::string& Fields) {
return Ret;
}
bool HTTP::Download(const std::string& IP, const std::string& Path) {
bool HTTP::Download(const std::string& IP, const beammp_fs_string& Path) {
static std::mutex Lock;
std::scoped_lock Guard(Lock);
@ -145,7 +145,7 @@ bool HTTP::Download(const std::string& IP, const std::string& Path) {
File.close();
info("Download Complete!");
} else {
error("Failed to open file directory: " + Path);
error(beammp_wide("Failed to open file directory: ") + Path);
return false;
}

View File

@ -164,9 +164,12 @@ std::vector<char> TCPRcvRaw(SOCKET Sock, uint64_t& GRcv, uint64_t Size) {
do {
// receive at most some MB at a time
int Len = std::min(int(Size - Rcv), 1 * 1024 * 1024);
int32_t Temp = recv(Sock, &File[Rcv], Len, MSG_WAITALL);
if (Temp < 1) {
info(std::to_string(Temp));
int Temp = recv(Sock, &File[Rcv], Len, MSG_WAITALL);
if (Temp == -1 || Temp == 0) {
debug("Recv returned: " + std::to_string(Temp));
if (Temp == -1) {
error("Socket error during download: " + std::to_string(WSAGetLastError()));
}
UUl("Socket Closed Code 1");
KillSocket(Sock);
Terminate = true;
@ -177,8 +180,8 @@ std::vector<char> TCPRcvRaw(SOCKET Sock, uint64_t& GRcv, uint64_t Size) {
auto end = std::chrono::high_resolution_clock::now();
auto difference = end - start;
float bits_per_s = float(Rcv * 8) / float(std::chrono::duration_cast<std::chrono::milliseconds>(difference).count());
float megabits_per_s = bits_per_s / 1000;
double bits_per_s = double(Rcv * 8) / double(std::chrono::duration_cast<std::chrono::milliseconds>(difference).count());
double megabits_per_s = bits_per_s / 1000;
DownloadSpeed = megabits_per_s;
// every 8th iteration print the speed
if (i % 8 == 0) {
@ -298,68 +301,6 @@ void InvalidResource(const std::string& File) {
Terminate = true;
}
std::string GetSha256HashReallyFast(const std::string& filename) {
try {
EVP_MD_CTX* mdctx;
const EVP_MD* md;
uint8_t sha256_value[EVP_MAX_MD_SIZE];
md = EVP_sha256();
if (md == nullptr) {
throw std::runtime_error("EVP_sha256() failed");
}
mdctx = EVP_MD_CTX_new();
if (mdctx == nullptr) {
throw std::runtime_error("EVP_MD_CTX_new() failed");
}
if (!EVP_DigestInit_ex2(mdctx, md, NULL)) {
EVP_MD_CTX_free(mdctx);
throw std::runtime_error("EVP_DigestInit_ex2() failed");
}
std::ifstream stream(filename, std::ios::binary);
const size_t FileSize = std::filesystem::file_size(filename);
size_t Read = 0;
std::vector<char> Data;
while (Read < FileSize) {
Data.resize(size_t(std::min<size_t>(FileSize - Read, 4096)));
size_t RealDataSize = Data.size();
stream.read(Data.data(), std::streamsize(Data.size()));
if (stream.eof() || stream.fail()) {
RealDataSize = size_t(stream.gcount());
}
Data.resize(RealDataSize);
if (RealDataSize == 0) {
break;
}
if (RealDataSize > 0 && !EVP_DigestUpdate(mdctx, Data.data(), Data.size())) {
EVP_MD_CTX_free(mdctx);
throw std::runtime_error("EVP_DigestUpdate() failed");
}
Read += RealDataSize;
}
unsigned int sha256_len = 0;
if (!EVP_DigestFinal_ex(mdctx, sha256_value, &sha256_len)) {
EVP_MD_CTX_free(mdctx);
throw std::runtime_error("EVP_DigestFinal_ex() failed");
}
EVP_MD_CTX_free(mdctx);
std::string result;
for (size_t i = 0; i < sha256_len; i++) {
char buf[3];
sprintf(buf, "%02x", sha256_value[i]);
buf[2] = 0;
result += buf;
}
return result;
} catch (const std::exception& e) {
error("Sha256 hashing of '" + filename + "' failed: " + e.what());
return "";
}
}
struct ModInfo {
static std::pair<bool, std::vector<ModInfo>> ParseModInfosFromPacket(const std::string& packet) {
bool success = false;
@ -377,6 +318,11 @@ struct ModInfo {
.Hash = entry["hash"],
.HashAlgorithm = entry["hash_algorithm"],
};
if (entry.contains("protected")) {
modInfo.Protected = entry["protected"];
}
modInfos.push_back(modInfo);
success = true;
}
@ -390,13 +336,14 @@ struct ModInfo {
size_t FileSize;
std::string Hash;
std::string HashAlgorithm;
bool Protected = false;
};
nlohmann::json modUsage = {};
void UpdateModUsage(const std::string& fileName) {
try {
fs::path usageFile = fs::path(CachingDirectory) / "mods.json";
fs::path usageFile = CachingDirectory / "mods.json";
if (!fs::exists(usageFile)) {
if (std::ofstream file(usageFile); !file.is_open()) {
@ -414,7 +361,7 @@ void UpdateModUsage(const std::string& fileName) {
}
if (modUsage.empty()) {
auto Size = fs::file_size(fs::path(CachingDirectory) / "mods.json");
auto Size = fs::file_size(CachingDirectory / "mods.json");
std::string modsJson(Size, 0);
file.read(&modsJson[0], Size);
@ -466,22 +413,45 @@ void NewSyncResources(SOCKET Sock, const std::string& Mods, const std::vector<Mo
info("Syncing...");
int ModNo = 1;
std::vector<std::pair<std::string, std::filesystem::path>> CachedMods = {};
if (deleteDuplicateMods) {
for (const auto& entry : fs::directory_iterator(CachingDirectory)) {
const std::string filename = entry.path().filename().string();
if (entry.is_regular_file() && entry.path().extension() == ".zip" && filename.length() > 10) {
CachedMods.push_back(std::make_pair(filename.substr(0, filename.length() - 13) + ".zip", entry.path()));
}
}
}
int ModNo = 0;
int TotalMods = ModInfos.size();
for (auto ModInfoIter = ModInfos.begin(), AlsoModInfoIter = ModInfos.begin(); ModInfoIter != ModInfos.end() && !Terminate; ++ModInfoIter, ++AlsoModInfoIter) {
++ModNo;
if (deleteDuplicateMods) {
for (auto& CachedMod : CachedMods) {
const bool cachedModExists = CachedMod.first == ModInfoIter->FileName;
const bool cachedModIsNotNewestVersion = CachedMod.second.stem().string() + ".zip" != std::filesystem::path(ModInfoIter->FileName).stem().string() + "-" + ModInfoIter->Hash.substr(0, 8) + ".zip";
if (cachedModExists && cachedModIsNotNewestVersion) {
debug("Found duplicate mod '" + CachedMod.second.stem().string() + ".zip" + "' in cache, removing it");
std::filesystem::remove(CachedMod.second);
break;
}
}
}
if (ModInfoIter->Hash.length() < 8 || ModInfoIter->HashAlgorithm != "sha256") {
error("Unsupported hash algorithm or invalid hash for '" + ModInfoIter->FileName + "'");
Terminate = true;
return;
}
auto FileName = std::filesystem::path(ModInfoIter->FileName).stem().string() + "-" + ModInfoIter->Hash.substr(0, 8) + std::filesystem::path(ModInfoIter->FileName).extension().string();
auto PathToSaveTo = (fs::path(CachingDirectory) / FileName).string();
if (fs::exists(PathToSaveTo) && GetSha256HashReallyFast(PathToSaveTo) == ModInfoIter->Hash) {
auto PathToSaveTo = (CachingDirectory / FileName);
if (fs::exists(PathToSaveTo) && Utils::GetSha256HashReallyFast(PathToSaveTo) == ModInfoIter->Hash) {
debug("Mod '" + FileName + "' found in cache");
UpdateUl(false, std::to_string(ModNo) + "/" + std::to_string(TotalMods) + ": " + ModInfoIter->FileName);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
try {
if (!fs::exists(GetGamePath() + "mods/multiplayer")) {
fs::create_directories(GetGamePath() + "mods/multiplayer");
if (!fs::exists(GetGamePath() / beammp_wide("mods/multiplayer"))) {
fs::create_directories(GetGamePath() / beammp_wide("mods/multiplayer"));
}
auto modname = ModInfoIter->FileName;
#if defined(__linux__)
@ -505,15 +475,16 @@ void NewSyncResources(SOCKET Sock, const std::string& Mods, const std::vector<Mo
}
WaitForConfirm();
continue;
} else if (auto OldCachedPath = fs::path(CachingDirectory) / std::filesystem::path(ModInfoIter->FileName).filename();
fs::exists(OldCachedPath) && GetSha256HashReallyFast(OldCachedPath.string()) == ModInfoIter->Hash) {
} else if (auto OldCachedPath = CachingDirectory / std::filesystem::path(ModInfoIter->FileName).filename();
fs::exists(OldCachedPath) && Utils::GetSha256HashReallyFast(OldCachedPath) == ModInfoIter->Hash) {
debug("Mod '" + FileName + "' found in old cache, copying it to the new cache");
UpdateUl(false, std::to_string(ModNo) + "/" + std::to_string(TotalMods) + ": " + ModInfoIter->FileName);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
try {
fs::copy_file(OldCachedPath, PathToSaveTo, fs::copy_options::overwrite_existing);
if (!fs::exists(GetGamePath() + "mods/multiplayer")) {
fs::create_directories(GetGamePath() + "mods/multiplayer");
if (!fs::exists(GetGamePath() / beammp_wide("mods/multiplayer"))) {
fs::create_directories(GetGamePath() / beammp_wide("mods/multiplayer"));
}
auto modname = ModInfoIter->FileName;
@ -541,10 +512,20 @@ void NewSyncResources(SOCKET Sock, const std::string& Mods, const std::vector<Mo
WaitForConfirm();
continue;
}
if (ModInfoIter->Protected) {
std::string message = "Mod '" + ModInfoIter->FileName + "' is protected and therefore must be placed in the Resources/Caching folder manually here: " + absolute(CachingDirectory).string();
error(message);
UUl(message);
Terminate = true;
return;
}
CheckForDir();
std::string FName = ModInfoIter->FileName;
do {
debug("Loading file '" + FName + "' to '" + PathToSaveTo + "'");
debug(beammp_wide("Loading file '") + Utils::ToWString(FName) + beammp_wide("' to '") + beammp_fs_string(PathToSaveTo) + beammp_wide("'"));
TCPSend("f" + ModInfoIter->FileName, Sock);
std::string Data = TCPRcv(Sock);
@ -554,6 +535,13 @@ void NewSyncResources(SOCKET Sock, const std::string& Mods, const std::vector<Mo
break;
}
if (Data != "AG") {
UUl("Received corrupted download confirmation, aborting download.");
debug("Corrupted download confirmation: " + Data);
Terminate = true;
break;
}
std::string Name = std::to_string(ModNo) + "/" + std::to_string(TotalMods) + ": " + FName;
std::vector<char> DownloadedFile = SingleNormalDownload(Sock, ModInfoIter->FileSize, Name);
@ -568,15 +556,20 @@ void NewSyncResources(SOCKET Sock, const std::string& Mods, const std::vector<Mo
OutFile.write(DownloadedFile.data(), DownloadedFile.size());
OutFile.flush();
}
// 2. verify size
// 2. verify size and hash
if (std::filesystem::file_size(PathToSaveTo) != DownloadedFile.size()) {
error("Failed to write the entire file '" + PathToSaveTo + "' correctly (file size mismatch)");
error(beammp_wide("Failed to write the entire file '") + beammp_fs_string(PathToSaveTo) + beammp_wide("' correctly (file size mismatch)"));
Terminate = true;
}
if (Utils::GetSha256HashReallyFast(PathToSaveTo) != ModInfoIter->Hash) {
error(beammp_wide("Failed to write or download the entire file '") + beammp_fs_string(PathToSaveTo) + beammp_wide("' correctly (hash mismatch)"));
Terminate = true;
}
} while (fs::file_size(PathToSaveTo) != ModInfoIter->FileSize && !Terminate);
if (!Terminate) {
if (!fs::exists(GetGamePath() + "mods/multiplayer")) {
fs::create_directories(GetGamePath() + "mods/multiplayer");
if (!fs::exists(GetGamePath() / beammp_wide("mods/multiplayer"))) {
fs::create_directories(GetGamePath() / beammp_wide("mods/multiplayer"));
}
// Linux version of the game doesnt support uppercase letters in mod names
@ -590,7 +583,6 @@ void NewSyncResources(SOCKET Sock, const std::string& Mods, const std::vector<Mo
UpdateModUsage(FName);
}
WaitForConfirm();
++ModNo;
}
if (!Terminate) {
@ -634,7 +626,8 @@ void SyncResources(SOCKET Sock) {
Ret.clear();
int Amount = 0, Pos = 0;
std::string PathToSaveTo, t;
std::filesystem::path PathToSaveTo;
std::string t;
for (const std::string& name : FNames) {
if (!name.empty()) {
t += name.substr(name.find_last_of('/') + 1) + ";";
@ -662,7 +655,7 @@ void SyncResources(SOCKET Sock) {
for (auto FN = FNames.begin(), FS = FSizes.begin(); FN != FNames.end() && !Terminate; ++FN, ++FS) {
auto pos = FN->find_last_of('/');
if (pos != std::string::npos) {
PathToSaveTo = CachingDirectory + FN->substr(pos);
PathToSaveTo = CachingDirectory / std::filesystem::path(*FN).filename();
} else {
continue;
}
@ -672,21 +665,22 @@ void SyncResources(SOCKET Sock) {
if (FS->find_first_not_of("0123456789") != std::string::npos)
continue;
if (fs::file_size(PathToSaveTo) == FileSize) {
UpdateUl(false, std::to_string(Pos) + "/" + std::to_string(Amount) + ": " + PathToSaveTo.substr(PathToSaveTo.find_last_of('/')));
UpdateUl(false, std::to_string(Pos) + "/" + std::to_string(Amount) + ": " + PathToSaveTo.filename().string());
std::this_thread::sleep_for(std::chrono::milliseconds(50));
try {
if (!fs::exists(GetGamePath() + "mods/multiplayer")) {
fs::create_directories(GetGamePath() + "mods/multiplayer");
if (!fs::exists(GetGamePath() / beammp_wide("mods/multiplayer"))) {
fs::create_directories(GetGamePath() / beammp_wide("mods/multiplayer"));
}
auto modname = PathToSaveTo.substr(PathToSaveTo.find_last_of('/'));
auto modname = PathToSaveTo.filename().string();
#if defined(__linux__)
// Linux version of the game doesnt support uppercase letters in mod names
for (char& c : modname) {
c = ::tolower(c);
}
#endif
auto name = GetGamePath() + "mods/multiplayer" + modname;
auto tmp_name = name + ".tmp";
auto name = GetGamePath() / beammp_wide("mods/multiplayer") / Utils::ToWString(modname);
auto tmp_name = name;
tmp_name += L".tmp";
fs::copy_file(PathToSaveTo, tmp_name, fs::copy_options::overwrite_existing);
fs::rename(tmp_name, name);
UpdateModUsage(modname);
@ -698,12 +692,12 @@ void SyncResources(SOCKET Sock) {
WaitForConfirm();
continue;
} else
remove(PathToSaveTo.c_str());
fs::remove(PathToSaveTo.wstring());
}
CheckForDir();
std::string FName = PathToSaveTo.substr(PathToSaveTo.find_last_of('/'));
std::string FName = PathToSaveTo.filename().string();
do {
debug("Loading file '" + FName + "' to '" + PathToSaveTo + "'");
debug("Loading file '" + FName + "' to '" + PathToSaveTo.string() + "'");
TCPSend("f" + *FN, Sock);
std::string Data = TCPRcv(Sock);
@ -728,13 +722,13 @@ void SyncResources(SOCKET Sock) {
}
// 2. verify size
if (std::filesystem::file_size(PathToSaveTo) != DownloadedFile.size()) {
error("Failed to write the entire file '" + PathToSaveTo + "' correctly (file size mismatch)");
error(beammp_wide("Failed to write the entire file '") + beammp_fs_string(PathToSaveTo) + beammp_wide("' correctly (file size mismatch)"));
Terminate = true;
}
} while (fs::file_size(PathToSaveTo) != std::stoull(*FS) && !Terminate);
if (!Terminate) {
if (!fs::exists(GetGamePath() + "mods/multiplayer")) {
fs::create_directories(GetGamePath() + "mods/multiplayer");
if (!fs::exists(GetGamePath() / beammp_wide("mods/multiplayer"))) {
fs::create_directories(GetGamePath() / beammp_wide("mods/multiplayer"));
}
// Linux version of the game doesnt support uppercase letters in mod names
@ -744,7 +738,7 @@ void SyncResources(SOCKET Sock) {
}
#endif
fs::copy_file(PathToSaveTo, GetGamePath() + "mods/multiplayer" + FName, fs::copy_options::overwrite_existing);
fs::copy_file(PathToSaveTo, GetGamePath() / beammp_wide("mods/multiplayer") / Utils::ToWString(FName), fs::copy_options::overwrite_existing);
UpdateModUsage(FN->substr(pos));
}
WaitForConfirm();

View File

@ -81,7 +81,8 @@ std::string TCPRcv(SOCKET Sock) {
UUl("Invalid Socket");
return "";
}
int32_t Header, Temp;
int32_t Header;
int Temp;
std::vector<char> Data(sizeof(Header));
Temp = recv(Sock, Data.data(), sizeof(Header), MSG_WAITALL);
if (!CheckBytes(Temp)) {

View File

@ -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> Path to BeamNG's User Path\n"
"\t--game <args...> Passes ALL following arguments to the game, see also `--`\n"
<< std::flush;
exit(0);

View File

@ -7,7 +7,6 @@
#include <filesystem>
#if defined(_WIN32)
#include <windows.h>
#elif defined(__linux__)
#include "vdf_parser.hpp"
#include <pwd.h>
@ -15,6 +14,8 @@
#include <vector>
#endif
#include "Logger.h"
#include "Utils.h"
#include <fstream>
#include <string>
#include <thread>
@ -23,7 +24,7 @@
#define MAX_VALUE_NAME 16383
int TraceBack = 0;
std::string GameDir;
beammp_fs_string GameDir;
void lowExit(int code) {
TraceBack = 0;
@ -33,7 +34,7 @@ void lowExit(int code) {
exit(2);
}
std::string GetGameDir() {
beammp_fs_string GetGameDir() {
#if defined(_WIN32)
return GameDir.substr(0, GameDir.find_last_of('\\'));
#elif defined(__linux__)
@ -44,8 +45,8 @@ std::string GetGameDir() {
LONG OpenKey(HKEY root, const char* path, PHKEY hKey) {
return RegOpenKeyEx(root, reinterpret_cast<LPCSTR>(path), 0, KEY_READ, hKey);
}
std::string QueryKey(HKEY hKey, int ID) {
TCHAR achKey[MAX_KEY_LENGTH]; // buffer for subkey name
std::wstring QueryKey(HKEY hKey, int ID) {
wchar_t* achKey; // buffer for subkey name
DWORD cbName; // size of name string
TCHAR achClass[MAX_PATH] = TEXT(""); // buffer for class name
DWORD cchClassName = MAX_PATH; // size of class string
@ -60,7 +61,7 @@ std::string QueryKey(HKEY hKey, int ID) {
DWORD i, retCode;
TCHAR achValue[MAX_VALUE_NAME];
wchar_t* achValue = new wchar_t[MAX_VALUE_NAME];
DWORD cchValue = MAX_VALUE_NAME;
retCode = RegQueryInfoKey(
@ -82,9 +83,9 @@ std::string QueryKey(HKEY hKey, int ID) {
if (cSubKeys) {
for (i = 0; i < cSubKeys; i++) {
cbName = MAX_KEY_LENGTH;
retCode = RegEnumKeyEx(hKey, i, achKey, &cbName, nullptr, nullptr, nullptr, &ftLastWriteTime);
retCode = RegEnumKeyExW(hKey, i, achKey, &cbName, nullptr, nullptr, nullptr, &ftLastWriteTime);
if (retCode == ERROR_SUCCESS) {
if (strcmp(achKey, "Steam App 284160") == 0) {
if (wcscmp(achKey, L"Steam App 284160") == 0) {
return achKey;
}
}
@ -94,36 +95,37 @@ std::string QueryKey(HKEY hKey, int ID) {
for (i = 0, retCode = ERROR_SUCCESS; i < cValues; i++) {
cchValue = MAX_VALUE_NAME;
achValue[0] = '\0';
retCode = RegEnumValue(hKey, i, achValue, &cchValue, nullptr, nullptr, nullptr, nullptr);
retCode = RegEnumValueW(hKey, i, achValue, &cchValue, nullptr, nullptr, nullptr, nullptr);
if (retCode == ERROR_SUCCESS) {
DWORD lpData = cbMaxValueData;
buffer[0] = '\0';
LONG dwRes = RegQueryValueEx(hKey, achValue, nullptr, nullptr, buffer, &lpData);
std::string data = (char*)(buffer);
std::string key = achValue;
LONG dwRes = RegQueryValueExW(hKey, achValue, nullptr, nullptr, buffer, &lpData);
std::wstring data = (wchar_t*)(buffer);
std::wstring key = achValue;
switch (ID) {
case 1:
if (key == "SteamExe") {
auto p = data.find_last_of("/\\");
if (key == L"SteamExe") {
auto p = data.find_last_of(L"/\\");
if (p != std::string::npos) {
return data.substr(0, p);
}
}
break;
case 2:
if (key == "Name" && data == "BeamNG.drive")
if (key == L"Name" && data == L"BeamNG.drive")
return data;
break;
case 3:
if (key == "rootpath")
if (key == L"rootpath")
return data;
break;
case 4:
if (key == "userpath_override")
if (key == L"userpath_override")
return data;
case 5:
if (key == "Local AppData")
if (key == L"Local AppData")
return data;
default:
break;
@ -131,8 +133,9 @@ std::string QueryKey(HKEY hKey, int ID) {
}
}
}
delete[] achValue;
delete[] buffer;
return "";
return L"";
}
#endif
@ -159,7 +162,7 @@ void FileList(std::vector<std::string>& a, const std::string& Path) {
}
void LegitimacyCheck() {
#if defined(_WIN32)
std::string Result;
std::wstring Result;
std::string K3 = R"(Software\BeamNG\BeamNG.drive)";
HKEY hKey;
LONG dwRegOPenKey = OpenKey(HKEY_CURRENT_USER, K3.c_str(), &hKey);
@ -229,12 +232,9 @@ void LegitimacyCheck() {
}
#endif
}
std::string CheckVer(const std::string& dir) {
#if defined(_WIN32)
std::string temp, Path = dir + "\\integrity.json";
#elif defined(__linux__)
std::string temp, Path = dir + "/integrity.json";
#endif
std::string CheckVer(const beammp_fs_string& dir) {
std::string temp;
beammp_fs_string Path = dir + beammp_wide("\\integrity.json");
std::ifstream f(Path.c_str(), std::ios::binary);
int Size = int(std::filesystem::file_size(Path));
std::string vec(Size, 0);

View File

@ -12,20 +12,20 @@
#include <nlohmann/json.hpp>
#include <string>
#if defined(_WIN32)
#include <windows.h>
#elif defined(__linux__)
#include <unistd.h>
#endif
#include "Http.h"
#include "Logger.h"
#include "Network/network.hpp"
#include "Options.h"
#include "Security/Init.h"
#include "Startup.h"
#include "Utils.h"
#include "hashpp.h"
#include <filesystem>
#include <fstream>
#include <thread>
#include "Options.h"
extern int TraceBack;
int ProxyPort = 0;
@ -72,49 +72,45 @@ Version::Version(const std::array<uint8_t, 3>& v)
: Version(v[0], v[1], v[2]) {
}
std::string GetEN() {
#if defined(_WIN32)
return "BeamMP-Launcher.exe";
#elif defined(__linux__)
return "BeamMP-Launcher";
#endif
beammp_fs_string GetEN() {
return beammp_wide("BeamMP-Launcher.exe");
}
std::string GetVer() {
return "2.4";
return "2.5";
}
std::string GetPatch() {
return ".0";
}
std::string GetEP(const char* P) {
static std::string Ret = [&]() {
std::string path(P);
return path.substr(0, path.find_last_of("\\/") + 1);
beammp_fs_string GetEP(const beammp_fs_char* P) {
static beammp_fs_string Ret = [&]() {
beammp_fs_string path(P);
return path.substr(0, path.find_last_of(beammp_wide("\\/")) + 1);
}();
return Ret;
}
#if defined(_WIN32)
void ReLaunch() {
std::string Arg;
std::wstring Arg;
for (int c = 2; c <= options.argc; c++) {
Arg += options.argv[c - 1];
Arg += " ";
Arg += Utils::ToWString(options.argv[c - 1]);
Arg += L" ";
}
info("Relaunch!");
system("cls");
ShellExecute(nullptr, "runas", (GetEP() + GetEN()).c_str(), Arg.c_str(), nullptr, SW_SHOWNORMAL);
ShellExecuteW(nullptr, L"runas", (GetEP() + GetEN()).c_str(), Arg.c_str(), nullptr, SW_SHOWNORMAL);
ShowWindow(GetConsoleWindow(), 0);
std::this_thread::sleep_for(std::chrono::seconds(1));
exit(1);
}
void URelaunch() {
std::string Arg;
std::wstring Arg;
for (int c = 2; c <= options.argc; c++) {
Arg += options.argv[c - 1];
Arg += " ";
Arg += Utils::ToWString(options.argv[c - 1]);
Arg += L" ";
}
ShellExecute(nullptr, "open", (GetEP() + GetEN()).c_str(), Arg.c_str(), nullptr, SW_SHOWNORMAL);
ShellExecuteW(nullptr, L"open", (GetEP() + GetEN()).c_str(), Arg.c_str(), nullptr, SW_SHOWNORMAL);
ShowWindow(GetConsoleWindow(), 0);
std::this_thread::sleep_for(std::chrono::seconds(1));
exit(1);
@ -149,16 +145,16 @@ void URelaunch() {
void CheckName() {
#if defined(_WIN32)
std::string DN = GetEN(), CDir = options.executable_name, FN = CDir.substr(CDir.find_last_of('\\') + 1);
std::wstring DN = GetEN(), CDir = Utils::ToWString(options.executable_name), FN = CDir.substr(CDir.find_last_of('\\') + 1);
#elif defined(__linux__)
std::string DN = GetEN(), CDir = options.executable_name, FN = CDir.substr(CDir.find_last_of('/') + 1);
#endif
if (FN != DN) {
if (fs::exists(DN))
remove(DN.c_str());
fs::remove(DN.c_str());
if (fs::exists(DN))
ReLaunch();
std::rename(FN.c_str(), DN.c_str());
fs::rename(FN.c_str(), DN.c_str());
URelaunch();
}
}
@ -169,9 +165,9 @@ void CheckForUpdates(const std::string& CV) {
"https://backend.beammp.com/version/launcher?branch=" + Branch + "&pk=" + PublicKey);
transform(LatestHash.begin(), LatestHash.end(), LatestHash.begin(), ::tolower);
std::string EP(GetEP() + GetEN()), Back(GetEP() + "BeamMP-Launcher.back");
beammp_fs_string EP(GetEP() + GetEN()), Back(GetEP() + beammp_wide("BeamMP-Launcher.back"));
std::string FileHash = hashpp::get::getFileHash(hashpp::ALGORITHMS::SHA2_256, EP);
std::string FileHash = Utils::GetSha256HashReallyFast(EP);
if (FileHash != LatestHash && IsOutdated(Version(VersionStrToInts(GetVer() + GetPatch())), Version(VersionStrToInts(LatestVersion)))) {
if (!options.no_update) {
@ -251,7 +247,7 @@ size_t DirCount(const std::filesystem::path& path) {
return (size_t)std::distance(std::filesystem::directory_iterator { path }, std::filesystem::directory_iterator {});
}
void CheckMP(const std::string& Path) {
void CheckMP(const beammp_fs_string& Path) {
if (!fs::exists(Path))
return;
size_t c = DirCount(fs::path(Path));
@ -271,7 +267,7 @@ void CheckMP(const std::string& Path) {
}
void EnableMP() {
std::string File(GetGamePath() + "mods/db.json");
beammp_fs_string File(GetGamePath() / beammp_wide("mods/db.json"));
if (!fs::exists(File))
return;
auto Size = fs::file_size(File);
@ -294,18 +290,18 @@ void EnableMP() {
ofs << d.dump();
ofs.close();
} else {
error("Failed to write " + File);
error(beammp_wide("Failed to write ") + File);
}
}
}
}
void PreGame(const std::string& GamePath) {
void PreGame(const beammp_fs_string& GamePath) {
std::string GameVer = CheckVer(GamePath);
info("Game Version : " + GameVer);
CheckMP(GetGamePath() + "mods/multiplayer");
info("Game user path: " + GetGamePath());
CheckMP(GetGamePath() / beammp_wide("mods/multiplayer"));
info(beammp_wide("Game user path: ") + beammp_fs_string(GetGamePath()));
if (!options.no_download) {
std::string LatestHash = HTTP::Get("https://backend.beammp.com/sha/mod?branch=" + Branch + "&pk=" + PublicKey);
@ -315,21 +311,21 @@ void PreGame(const std::string& GamePath) {
LatestHash.end());
try {
if (!fs::exists(GetGamePath() + "mods/multiplayer")) {
fs::create_directories(GetGamePath() + "mods/multiplayer");
if (!fs::exists(GetGamePath() / beammp_wide("mods/multiplayer"))) {
fs::create_directories(GetGamePath() / beammp_wide("mods/multiplayer"));
}
EnableMP();
} catch (std::exception& e) {
fatal(e.what());
}
#if defined(_WIN32)
std::string ZipPath(GetGamePath() + R"(mods\multiplayer\BeamMP.zip)");
std::wstring ZipPath(GetGamePath() / LR"(mods\multiplayer\BeamMP.zip)");
#elif defined(__linux__)
// Linux version of the game cant handle mods with uppercase names
std::string ZipPath(GetGamePath() + R"(mods/multiplayer/beammp.zip)");
std::string ZipPath(GetGamePath() / R"(mods/multiplayer/beammp.zip)");
#endif
std::string FileHash = hashpp::get::getFileHash(hashpp::ALGORITHMS::SHA2_256, ZipPath);
std::string FileHash = fs::exists(ZipPath) ? Utils::GetSha256HashReallyFast(ZipPath) : "";
if (FileHash != LatestHash) {
info("Downloading BeamMP Update " + LatestHash);
@ -339,9 +335,9 @@ void PreGame(const std::string& GamePath) {
ZipPath);
}
std::string Target(GetGamePath() + "mods/unpacked/beammp");
beammp_fs_string Target(GetGamePath() / beammp_wide("mods/unpacked/beammp"));
if (fs::is_directory(Target)) {
if (fs::is_directory(Target) && !fs::is_directory(Target + beammp_wide("/.git"))) {
fs::remove_all(Target);
}
}

View File

@ -9,6 +9,7 @@
#include "Network/network.hpp"
#include "Security/Init.h"
#include "Startup.h"
#include "Utils.h"
#include <curl/curl.h>
#include <iostream>
#include <thread>
@ -37,7 +38,7 @@ int main(int argc, const char** argv) try {
curl_global_init(CURL_GLOBAL_ALL);
GetEP(argv[0]);
GetEP(Utils::ToWString(std::string(argv[0])).c_str());
InitLog();
ConfigInit();