move new protocol code into main repo

This commit is contained in:
Lion Kortlepel
2024-01-19 17:45:00 +01:00
parent 3fc74285e6
commit 12ba13d9b4
33 changed files with 5979 additions and 0 deletions

57
src/Compression.h Normal file
View File

@@ -0,0 +1,57 @@
#pragma once
#include <array>
#include <zlib.h>
#include <algorithm>
#define Biggest 30000
template <typename T>
inline T Comp(const T& Data) {
std::array<char, Biggest> C {};
// obsolete
C.fill(0);
z_stream defstream;
defstream.zalloc = nullptr;
defstream.zfree = nullptr;
defstream.opaque = nullptr;
defstream.avail_in = uInt(Data.size());
defstream.next_in = const_cast<Bytef*>(reinterpret_cast<const Bytef*>(&Data[0]));
defstream.avail_out = Biggest;
defstream.next_out = reinterpret_cast<Bytef*>(C.data());
deflateInit(&defstream, Z_BEST_COMPRESSION);
deflate(&defstream, Z_SYNC_FLUSH);
deflate(&defstream, Z_FINISH);
deflateEnd(&defstream);
size_t TotalOut = defstream.total_out;
T Ret;
Ret.resize(TotalOut);
std::fill(Ret.begin(), Ret.end(), 0);
std::copy_n(C.begin(), TotalOut, Ret.begin());
return Ret;
}
template <typename T>
inline T DeComp(const T& Compressed) {
std::array<char, Biggest> C {};
// not needed
C.fill(0);
z_stream infstream;
infstream.zalloc = nullptr;
infstream.zfree = nullptr;
infstream.opaque = nullptr;
infstream.avail_in = Biggest;
infstream.next_in = const_cast<Bytef*>(reinterpret_cast<const Bytef*>(&Compressed[0]));
infstream.avail_out = Biggest;
infstream.next_out = const_cast<Bytef*>(reinterpret_cast<const Bytef*>(C.data()));
inflateInit(&infstream);
inflate(&infstream, Z_SYNC_FLUSH);
inflate(&infstream, Z_FINISH);
inflateEnd(&infstream);
size_t TotalOut = infstream.total_out;
T Ret;
Ret.resize(TotalOut);
std::fill(Ret.begin(), Ret.end(), 0);
std::copy_n(C.begin(), TotalOut, Ret.begin());
return Ret;
}

41
src/Config.cpp Executable file
View File

@@ -0,0 +1,41 @@
#include "Config.h"
#include <boost/iostreams/device/mapped_file.hpp>
#include <filesystem>
#include <fstream>
#include <nlohmann/json.hpp>
Config::Config() {
if (std::filesystem::exists("Launcher.cfg")) {
boost::iostreams::mapped_file cfg("Launcher.cfg", boost::iostreams::mapped_file::mapmode::readonly);
nlohmann::json d = nlohmann::json::parse(cfg.const_data(), nullptr, false);
if (d.is_discarded()) {
is_valid = false;
}
// parse config
if (d["Port"].is_number()) {
port = d["Port"].get<int>();
}
if (d["Build"].is_string()) {
branch = d["Build"].get<std::string>();
for (char& c : branch) {
c = char(tolower(c));
}
}
if (d["GameDir"].is_string()) {
game_dir = d["GameDir"].get<std::string>();
}
} else {
write_to_file();
}
}
void Config::write_to_file() const {
nlohmann::json d {
{ "Port", port },
{ "Branch", branch },
{ "GameDir", game_dir },
};
std::ofstream of("Launcher.cfg", std::ios::trunc);
of << d.dump(4);
}

14
src/Config.h Normal file
View File

@@ -0,0 +1,14 @@
#pragma once
#include <filesystem>
struct Config {
Config();
void write_to_file() const;
bool is_valid = true;
int port = 4444;
std::string branch = "Default";
std::string game_dir = "";
};

35
src/Hashing.cpp Normal file
View File

@@ -0,0 +1,35 @@
#include "Hashing.h"
#include <boost/iostreams/device/mapped_file.hpp>
#include <cryptopp/hex.h>
#include <cryptopp/sha.h>
#include <cstdio>
#include <filesystem>
#include <fmt/format.h>
#include <errno.h>
std::string sha256_file(const std::string& path) {
CryptoPP::byte digest[CryptoPP::SHA256::DIGESTSIZE];
digest[sizeof(digest) - 1] = 0;
FILE* file = std::fopen(path.c_str(), "rb");
if (!file) {
throw std::runtime_error(fmt::format("Failed to open {}: {}", path, std::strerror(errno)));
}
std::vector<uint8_t> buffer{};
buffer.resize(std::filesystem::file_size(path));
std::fread(buffer.data(), 1, buffer.size(), file);
std::fclose(file);
CryptoPP::SHA256().CalculateDigest(
digest,
buffer.data(), buffer.size());
CryptoPP::HexEncoder encoder;
encoder.Put(digest, sizeof(digest));
encoder.MessageEnd();
std::string encoded {};
if (auto size = encoder.MaxRetrievable(); size != 0)
{
encoded.resize(size);
encoder.Get(reinterpret_cast<CryptoPP::byte*>(&encoded[0]), encoded.size());
}
return encoded;
}

5
src/Hashing.h Normal file
View File

@@ -0,0 +1,5 @@
#pragma once
#include <string>
std::string sha256_file(const std::string& path);

115
src/Http.cpp Executable file
View File

@@ -0,0 +1,115 @@
#define CPPHTTPLIB_OPENSSL_SUPPORT
#include <iostream>
#include <fstream>
#include <spdlog/spdlog.h>
#include "Http.h"
#include <mutex>
#include <cmath>
#include <httplib.h>
bool HTTP::isDownload = false;
std::string HTTP::Get(const std::string &IP) {
static std::mutex Lock;
std::scoped_lock Guard(Lock);
auto pos = IP.find('/',10);
httplib::Client cli(IP.substr(0, pos).c_str());
cli.set_connection_timeout(std::chrono::seconds(10));
cli.set_follow_location(true);
auto res = cli.Get(IP.substr(pos).c_str(), ProgressBar);
std::string Ret;
if(res){
if(res->status == 200){
Ret = res->body;
}else spdlog::error(res->reason);
}else{
if(isDownload) {
std::cout << "\n";
}
spdlog::error("HTTP Get failed on " + to_string(res.error()));
}
return Ret;
}
std::string HTTP::Post(const std::string& IP, const std::string& Fields) {
static std::mutex Lock;
std::scoped_lock Guard(Lock);
auto pos = IP.find('/',10);
httplib::Client cli(IP.substr(0, pos).c_str());
cli.set_connection_timeout(std::chrono::seconds(10));
std::string Ret;
if(!Fields.empty()) {
httplib::Result res = cli.Post(IP.substr(pos).c_str(), Fields, "application/json");
if(res) {
if (res->status != 200) {
spdlog::error(res->reason);
}
Ret = res->body;
}else{
spdlog::error("HTTP Post failed on " + to_string(res.error()));
}
}else{
httplib::Result res = cli.Post(IP.substr(pos).c_str());
if(res) {
if (res->status != 200) {
spdlog::error(res->reason);
}
Ret = res->body;
}else{
spdlog::error("HTTP Post failed on " + to_string(res.error()));
}
}
if(Ret.empty())return "-1";
else return Ret;
}
bool HTTP::ProgressBar(size_t c, size_t t){
if(isDownload) {
static double last_progress, progress_bar_adv;
progress_bar_adv = round(c / double(t) * 25);
std::cout << "\r";
std::cout << "Progress : [ ";
std::cout << round(c / double(t) * 100);
std::cout << "% ] [";
int i;
for (i = 0; i <= progress_bar_adv; i++)std::cout << "#";
for (i = 0; i < 25 - progress_bar_adv; i++)std::cout << ".";
std::cout << "]";
last_progress = round(c / double(t) * 100);
}
return true;
}
bool HTTP::Download(const std::string &IP, const std::string &Path) {
static std::mutex Lock;
std::scoped_lock Guard(Lock);
isDownload = true;
std::string Ret = Get(IP);
isDownload = false;
if(Ret.empty())return false;
std::ofstream File(Path, std::ios::binary);
if(File.is_open()) {
File << Ret;
File.close();
std::cout << "\n";
spdlog::info("Download Complete!");
}else{
spdlog::error("Failed to open file directory: " + Path);
return false;
}
return true;
}

18
src/Http.h Executable file
View File

@@ -0,0 +1,18 @@
// Copyright (c) 2019-present Anonymous275.
// BeamMP Launcher code is not in the public domain and is not free software.
// One must be granted explicit permission by the copyright holder in order to modify or distribute any part of the source or binaries.
// Anything else is prohibited. Modified works may not be published and have be upstreamed to the official repository.
///
/// Created by Anonymous275 on 7/18/2020
///
#pragma once
#include <string>
class HTTP {
public:
static bool Download(const std::string &IP, const std::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);
public:
static bool isDownload;
};

114
src/Identity.cpp Normal file
View File

@@ -0,0 +1,114 @@
#include "Identity.h"
#include "Http.h"
#include <filesystem>
#include <fstream>
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
namespace fs = std::filesystem;
Identity::Identity() {
check_local_key();
}
void Identity::check_local_key() {
if (fs::exists("key") && fs::file_size("key") < 100) {
std::ifstream Key("key");
if (Key.is_open()) {
auto Size = fs::file_size("key");
std::string Buffer(Size, 0);
Key.read(&Buffer[0], static_cast<long>(Size));
Key.close();
for (char& c : Buffer) {
if (!std::isalnum(c) && c != '-') {
update_key(nullptr);
return;
}
}
Buffer = HTTP::Post("https://auth.beammp.com/userlogin", R"({"pk":")" + Buffer + "\"}");
nlohmann::json d = nlohmann::json::parse(Buffer, nullptr, false);
if (Buffer == "-1" || Buffer.at(0) != '{' || d.is_discarded()) {
spdlog::error(Buffer);
spdlog::info("Invalid answer from authentication servers.");
update_key(nullptr);
}
if (d["success"].get<bool>()) {
LoginAuth = true;
spdlog::info("{}", d["message"].get<std::string>());
update_key(d["private_key"].get<std::string>().c_str());
PublicKey = d["public_key"].get<std::string>();
Role = d["role"].get<std::string>();
} else {
spdlog::info("Auto-Authentication unsuccessful please re-login!");
update_key(nullptr);
}
} else {
spdlog::warn("Could not open saved key!");
update_key(nullptr);
}
} else
update_key(nullptr);
}
void Identity::update_key(const char* newKey) {
if (newKey && std::isalnum(newKey[0])) {
PrivateKey = newKey;
std::ofstream Key("key");
if (Key.is_open()) {
Key << newKey;
Key.close();
} else {
spdlog::error("Cannot write key to disk!");
}
} else if (fs::exists("key")) {
fs::remove("key");
}
}
static std::string GetFail(const std::string& R) {
std::string DRet = R"({"success":false,"message":)";
DRet += "\"" + R + "\"}";
spdlog::error(R);
return DRet;
}
std::string Identity::login(const std::string& fields) {
spdlog::debug("Logging in with {}", fields);
if (fields == "LO") {
LoginAuth = false;
update_key(nullptr);
return "";
}
spdlog::info("Attempting to authenticate...");
std::string Buffer = HTTP::Post("https://auth.beammp.com/userlogin", fields);
if (Buffer == "-1") {
return GetFail("Failed to communicate with the auth system!");
}
nlohmann::json d = nlohmann::json::parse(Buffer, nullptr, false);
if (Buffer.at(0) != '{' || d.is_discarded()) {
spdlog::error(Buffer);
return GetFail("Invalid answer from authentication servers, please try again later!");
}
if (d.contains("success") && d["success"].get<bool>()) {
LoginAuth = true;
if (d.contains("private_key")) {
update_key(d["private_key"].get<std::string>().c_str());
}
if (d.contains("public_key")) {
PublicKey = d["public_key"].get<std::string>();
}
spdlog::info("Authentication successful!");
} else {
spdlog::info("Authentication failed!");
}
d.erase("private_key");
d.erase("public_key");
return d.dump();
}

18
src/Identity.h Normal file
View File

@@ -0,0 +1,18 @@
#pragma once
#include <string>
struct Identity {
Identity();
void check_local_key();
bool LoginAuth { false };
std::string PublicKey;
std::string PrivateKey;
std::string Role;
std::string login(const std::string& fields);
private:
void update_key(const char* newKey);
};

888
src/Launcher.cpp Normal file
View File

@@ -0,0 +1,888 @@
#include "Launcher.h"
#include "Compression.h"
#include "Hashing.h"
#include "Http.h"
#include "Platform.h"
#include "Version.h"
#include <boost/asio.hpp>
#include <boost/iostreams/device/mapped_file.hpp>
#include <boost/process.hpp>
#include <chrono>
#include <filesystem>
#include <fmt/format.h>
#include <httplib.h>
#include <limits>
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
#include <vector>
using namespace boost::asio;
namespace fs = std::filesystem;
Launcher::Launcher()
: m_game_socket(m_io)
, m_core_socket(m_io)
, m_tcp_socket(m_io)
, m_udp_socket(m_io) {
spdlog::debug("Launcher startup");
m_config = Config {};
if (!m_config->is_valid) {
spdlog::error("Launcher config invalid!");
}
}
/// Sets shared headers for all backend proxy messages
static void proxy_set_headers(httplib::Response& res) {
res.set_header("Access-Control-Allow-Origin", "*");
res.set_header("Access-Control-Request-Method", "POST, OPTIONS, GET");
res.set_header("Access-Control-Request-Headers", "X-API-Version");
}
void Launcher::proxy_main() {
httplib::Server HTTPProxy;
httplib::Headers headers = {
{ "User-Agent", fmt::format("BeamMP-Launcher/{}.{}.{}", PRJ_VERSION_MAJOR, PRJ_VERSION_MINOR, PRJ_VERSION_PATCH) },
{ "Accept", "*/*" }
};
std::string pattern = "/:any1";
for (int i = 2; i <= 4; i++) {
HTTPProxy.Get(pattern, [&](const httplib::Request& req, httplib::Response& res) {
httplib::Client cli("https://backend.beammp.com");
proxy_set_headers(res);
if (req.has_header("X-BMP-Authentication")) {
headers.emplace("X-BMP-Authentication", m_identity->PrivateKey);
}
if (req.has_header("X-API-Version")) {
headers.emplace("X-API-Version", req.get_header_value("X-API-Version"));
}
if (auto cli_res = cli.Get(req.path, headers); cli_res) {
res.set_content(cli_res->body, cli_res->get_header_value("Content-Type"));
} else {
res.set_content(to_string(cli_res.error()), "text/plain");
}
});
HTTPProxy.Post(pattern, [&](const httplib::Request& req, httplib::Response& res) {
httplib::Client cli("https://backend.beammp.com");
proxy_set_headers(res);
if (req.has_header("X-BMP-Authentication")) {
headers.emplace("X-BMP-Authentication", m_identity->PrivateKey);
}
if (req.has_header("X-API-Version")) {
headers.emplace("X-API-Version", req.get_header_value("X-API-Version"));
}
if (auto cli_res = cli.Post(req.path, headers, req.body,
req.get_header_value("Content-Type"));
cli_res) {
res.set_content(cli_res->body, cli_res->get_header_value("Content-Type"));
} else {
res.set_content(to_string(cli_res.error()), "text/plain");
}
});
pattern += "/:any" + std::to_string(i);
}
m_proxy_port = HTTPProxy.bind_to_any_port("0.0.0.0");
spdlog::debug("http proxy started on port {}", m_proxy_port.get());
HTTPProxy.listen_after_bind();
}
Launcher::~Launcher() {
m_proxy_thread.interrupt();
m_game_thread.detach();
}
void Launcher::parse_config() {
}
void Launcher::set_port(int p) {
spdlog::warn("Using custom port {}", p);
m_config->port = p;
}
void Launcher::check_for_updates(int argc, char** argv) {
std::string LatestHash = HTTP::Get(fmt::format("https://backend.beammp.com/sha/launcher?branch={}&pk={}", m_config->branch, m_identity->PublicKey));
std::string LatestVersion = HTTP::Get(fmt::format("https://backend.beammp.com/version/launcher?branch={}&pk={}", m_config->branch, m_identity->PublicKey));
std::string DownloadURL = fmt::format("https://backend.beammp.com/builds/launcher?download=true"
"&pk={}"
"&branch={}",
m_identity->PublicKey, m_config->branch);
spdlog::debug("Latest hash: {}", LatestHash);
spdlog::debug("Latest version: {}", LatestVersion);
transform(LatestHash.begin(), LatestHash.end(), LatestHash.begin(), ::tolower);
std::string EP = (m_exe_path.get() / m_exe_name.get()).generic_string();
std::string Back = m_exe_path.get() / "BeamMP-Launcher.back";
std::string FileHash = sha256_file(EP);
if (FileHash != LatestHash
&& Version(PRJ_VERSION_MAJOR, PRJ_VERSION_MINOR, PRJ_VERSION_PATCH).is_outdated(Version(LatestVersion))) {
spdlog::info("Launcher update found!");
fs::remove(Back);
fs::rename(EP, Back);
spdlog::info("Downloading Launcher update " + LatestHash);
HTTP::Download(DownloadURL, EP);
plat::URelaunch(argc, argv);
} else {
spdlog::info("Launcher version is up to date");
}
}
void Launcher::find_game() {
// try to find the game by multiple means
spdlog::info("Locating game");
// 0. config!
if (!m_config->game_dir.empty()
&& std::filesystem::exists(std::filesystem::path(m_config->game_dir) / "BeamNG.drive.exe")) {
spdlog::debug("Found game directory in config: '{}'", m_config->game_dir);
return;
}
// 1. magic
auto game_dir = plat::get_game_dir_magically();
if (!game_dir.empty()) {
m_config->game_dir = game_dir;
m_config->write_to_file();
return;
} else {
spdlog::debug("Couldn't magically find game directory");
}
// 2. ask
m_config->game_dir = plat::ask_for_folder();
spdlog::debug("Located game at '{}'", m_config->game_dir);
m_config->write_to_file();
}
static std::string check_game_version(const std::filesystem::path& dir) {
std::string temp;
std::string Path = (dir / "integrity.json").generic_string();
boost::iostreams::mapped_file file(Path);
auto json = nlohmann::json::parse(file.const_begin(), file.const_end());
return json["version"].is_string() ? json["version"].get<std::string>() : "";
}
void Launcher::pre_game() {
std::string GameVer = check_game_version(m_config->game_dir);
if (GameVer.empty()) {
spdlog::error("Game version is empty!");
}
spdlog::info("Game Version: " + GameVer);
check_mp((std::filesystem::path(m_config->game_dir) / "mods/multiplayer").generic_string());
std::string LatestHash = HTTP::Get(fmt::format("https://backend.beammp.com/sha/mod?branch={}&pk={}", m_config->branch, m_identity->PublicKey));
transform(LatestHash.begin(), LatestHash.end(), LatestHash.begin(), ::tolower);
LatestHash.erase(std::remove_if(LatestHash.begin(), LatestHash.end(),
[](auto const& c) -> bool { return !std::isalnum(c); }),
LatestHash.end());
try {
if (!fs::exists(std::filesystem::path(m_config->game_dir) / "mods/multiplayer")) {
fs::create_directories(std::filesystem::path(m_config->game_dir) / "mods/multiplayer");
}
enable_mp();
} catch (std::exception& e) {
spdlog::error("Fatal: {}", e.what());
std::exit(1);
}
auto ZipPath(std::filesystem::path(m_config->game_dir) / "mods/multiplayer/BeamMP.zip");
std::string FileHash = sha256_file(ZipPath);
if (FileHash != LatestHash) {
spdlog::info("Downloading BeamMP Update " + LatestHash);
HTTP::Download(fmt::format("https://backend.beammp.com/builds/client?download=true&pk={}&branch={}", m_identity->PublicKey, m_config->branch), ZipPath);
}
auto Target = std::filesystem::path(m_config->game_dir) / "mods/unpacked/beammp";
if (fs::is_directory(Target)) {
fs::remove_all(Target);
}
}
static size_t count_items_in_dir(const std::filesystem::path& path) {
return static_cast<size_t>(std::distance(std::filesystem::directory_iterator { path }, std::filesystem::directory_iterator {}));
}
void Launcher::check_mp(const std::string& path) {
if (!fs::exists(path))
return;
size_t c = count_items_in_dir(fs::path(path));
try {
for (auto& p : fs::directory_iterator(path)) {
if (p.exists() && !p.is_directory()) {
std::string name = p.path().filename().string();
for (char& Ch : name)
Ch = char(tolower(Ch));
if (name != "beammp.zip")
fs::remove(p.path());
}
}
} catch (...) {
spdlog::error("We were unable to clean the multiplayer mods folder! Is the game still running or do you have something open in that folder?");
std::this_thread::sleep_for(std::chrono::seconds(5));
std::exit(1);
}
}
void Launcher::enable_mp() {
std::string File = (std::filesystem::path(m_config->game_dir) / "mods/db.json").generic_string();
if (!fs::exists(File))
return;
auto Size = fs::file_size(File);
if (Size < 2)
return;
std::ifstream db(File);
if (db.is_open()) {
std::string Data(Size, 0);
db.read(&Data[0], Size);
db.close();
nlohmann::json d = nlohmann::json::parse(Data, nullptr, false);
if (Data.at(0) != '{' || d.is_discarded()) {
// spdlog::error("Failed to parse " + File); //TODO illegal formatting
return;
}
if (d.contains("mods") && d["mods"].contains("multiplayerbeammp")) {
d["mods"]["multiplayerbeammp"]["active"] = true;
std::ofstream ofs(File);
if (ofs.is_open()) {
ofs << d.dump();
ofs.close();
} else {
spdlog::error("Failed to write " + File);
}
}
}
}
void Launcher::game_main() {
auto path = std::filesystem::current_path();
std::filesystem::current_path(m_config->game_dir);
#if defined(PLATFORM_LINUX)
auto game_path = (std::filesystem::path(m_config->game_dir) / "BinLinux/BeamNG.drive.x64").generic_string();
#elif defined(PLATFORM_WINDOWS)
auto game_path = (m_game_dir.get() / "Bin64/BeamNG.drive.x64.exe").generic_string();
#endif
boost::process::child game(game_path, boost::process::std_out > boost::process::null);
std::filesystem::current_path(path);
if (game.running()) {
spdlog::info("Game launched!");
game.wait();
spdlog::info("Game closed! Launcher closing soon");
} else {
spdlog::error("Failed to launch the game! Please start the game manually.");
}
}
void Launcher::start_game() {
m_game_thread = boost::scoped_thread<>(&Launcher::game_main, this);
}
void Launcher::start_network() {
while (true) {
try {
net_core_main();
} catch (const std::exception& e) {
spdlog::error("Error: {}", e.what());
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
void Launcher::reset_status() {
*m_m_status = " ";
*m_ul_status = "Ulstart";
m_conf_list->clear();
*m_ping = -1;
}
void Launcher::net_core_main() {
ip::tcp::endpoint listen_ep(ip::address::from_string("0.0.0.0"), static_cast<uint16_t>(m_config->port));
ip::tcp::socket listener(m_io);
boost::system::error_code ec;
listener.open(listen_ep.protocol(), ec);
if (ec) {
spdlog::error("Failed to open socket: {}", ec.message());
return;
}
socket_base::linger linger_opt {};
linger_opt.enabled(false);
listener.set_option(linger_opt, ec);
if (ec) {
spdlog::error("Failed to set up listening socket to not linger / reuse address. "
"This may cause the socket to refuse to bind(). spdlog::error: {}",
ec.message());
}
ip::tcp::acceptor acceptor(m_io, listen_ep);
acceptor.listen(socket_base::max_listen_connections, ec);
if (ec) {
spdlog::error("listen() failed, which is needed for the launcher to operate. "
"Shutting down. spdlog::error: {}",
ec.message());
std::this_thread::sleep_for(std::chrono::seconds(3));
std::exit(1);
}
do {
try {
spdlog::info("Waiting for the game to connect");
ip::tcp::endpoint game_ep;
m_game_socket = acceptor.accept(game_ep, ec);
if (ec) {
spdlog::error("Failed to accept: {}", ec.message());
}
reset_status();
spdlog::info("Game connected!");
spdlog::debug("Game: [{}]:{}", game_ep.address().to_string(), game_ep.port());
game_loop();
spdlog::warn("Game disconnected!");
} catch (const std::exception& e) {
spdlog::error("Fatal error in core network: {}", e.what());
}
} while (!*m_shutdown);
}
void Launcher::game_loop() {
size_t Size;
size_t Temp;
size_t Rcv;
char Header[10] = { 0 };
boost::system::error_code ec;
do {
Rcv = 0;
do {
Temp = boost::asio::read(m_game_socket, buffer(&Header[Rcv], 1), ec);
if (ec) {
spdlog::error("(Core) Failed to receive from game: {}", ec.message());
break;
}
if (Temp < 1)
break;
if (!isdigit(Header[Rcv]) && Header[Rcv] != '>') {
spdlog::error("(Core) Invalid lua communication: '{}'", std::string(Header, Rcv));
break;
}
} while (Header[Rcv++] != '>');
if (Temp < 1)
break;
if (std::from_chars(Header, &Header[Rcv], Size).ptr[0] != '>') {
spdlog::debug("(Core) Invalid lua Header: '{}'", std::string(Header, Rcv));
break;
}
std::vector<char> Ret(Size, 0);
Rcv = 0;
Temp = boost::asio::read(m_game_socket, buffer(Ret, Size), ec);
if (ec) {
spdlog::error("(Core) Failed to receive from game: {}", ec.message());
break;
}
handle_core_packet(Ret);
} while (Temp > 0);
if (Temp == 0) {
spdlog::debug("(Core) Connection closing");
}
// TODO: NetReset
}
static bool IsAllowedLink(const std::string& Link) {
std::regex link_pattern(R"(https:\/\/(?:\w+)?(?:\.)?(?:beammp\.com|discord\.gg))");
std::smatch link_match;
return std::regex_search(Link, link_match, link_pattern) && link_match.position() == 0;
}
void Launcher::handle_core_packet(const std::vector<char>& RawData) {
std::string Data(RawData.begin(), RawData.end());
char Code = Data.at(0), SubCode = 0;
if (Data.length() > 1)
SubCode = Data.at(1);
switch (Code) {
case 'A':
Data = Data.substr(0, 1);
break;
case 'B':
Data = Code + HTTP::Get("https://backend.beammp.com/servers-info");
break;
case 'C':
m_list_of_mods->clear();
if (!start_sync(Data)) {
// TODO: Handle
spdlog::error("start_sync failed, spdlog::error case not implemented");
}
while (m_list_of_mods->empty() && !*m_shutdown) {
std::this_thread::sleep_for(std::chrono::seconds(1));
}
if (m_list_of_mods.get() == "-")
Data = "L";
else
Data = "L" + m_list_of_mods.get();
break;
case 'O': // open default browser with URL
if (IsAllowedLink(Data.substr(1))) {
spdlog::info("Opening Link \"" + Data.substr(1) + "\"");
boost::process::child c(std::string("open '") + Data.substr(1) + "'", boost::process::std_out > boost::process::null);
c.detach();
}
Data.clear();
break;
case 'P':
Data = Code + std::to_string(*m_proxy_port);
break;
case 'U':
if (SubCode == 'l')
Data = *m_ul_status;
if (SubCode == 'p') {
if (*m_ping > 800) {
Data = "Up-2";
} else
Data = "Up" + std::to_string(*m_ping);
}
if (!SubCode) {
std::string Ping;
if (*m_ping > 800)
Ping = "-2";
else
Ping = std::to_string(*m_ping);
Data = std::string(*m_ul_status) + "\n" + "Up" + Ping;
}
break;
case 'M':
Data = *m_m_status;
break;
case 'Q':
if (SubCode == 'S') {
spdlog::debug("Shutting down via QS");
*m_shutdown = true;
*m_ping = -1;
}
if (SubCode == 'G') {
spdlog::debug("Shutting down via QG");
*m_shutdown = true;
}
Data.clear();
break;
case 'R': // will send mod name
if (m_conf_list->find(Data) == m_conf_list->end()) {
m_conf_list->insert(Data);
m_mod_loaded = true;
}
Data.clear();
break;
case 'Z':
Data = fmt::format("Z{}.{}", PRJ_VERSION_MAJOR, PRJ_VERSION_MINOR);
break;
case 'N':
if (SubCode == 'c') {
Data = "N{\"Auth\":" + std::to_string(m_identity->LoginAuth) + "}";
} else {
Data = "N" + m_identity->login(Data.substr(Data.find(':') + 1));
}
break;
default:
Data.clear();
break;
}
if (!Data.empty() && m_game_socket.is_open()) {
boost::system::error_code ec;
boost::asio::write(m_game_socket, buffer((Data + "\n").c_str(), Data.size() + 1), ec);
if (ec) {
spdlog::error("(Core) send failed with error: {}", ec.message());
}
}
}
static void compress_properly(std::vector<uint8_t>& Data) {
constexpr std::string_view ABG = "ABG:";
auto CombinedData = std::vector<uint8_t>(ABG.begin(), ABG.end());
auto CompData = Comp(Data);
CombinedData.resize(ABG.size() + CompData.size());
std::copy(CompData.begin(), CompData.end(), CombinedData.begin() + ABG.size());
Data = CombinedData;
}
void Launcher::send_large(const std::string& Data) {
std::vector<uint8_t> vec(Data.data(), Data.data() + Data.size());
compress_properly(vec);
tcp_send(vec);
}
void Launcher::server_send(const std::string& Data, bool Rel) {
if (Data.empty())
return;
if (Data.find("Zp") != std::string::npos && Data.size() > 500) {
abort();
}
char C = 0;
bool Ack = false;
int DLen = int(Data.length());
if (DLen > 3) {
C = Data.at(0);
}
if (C == 'O' || C == 'T') {
Ack = true;
}
if (C == 'N' || C == 'W' || C == 'Y' || C == 'V' || C == 'E' || C == 'C') {
Rel = true;
}
if (Ack || Rel) {
if (Ack || DLen > 1000) {
send_large(Data);
} else {
tcp_send(Data);
}
} else {
udp_send(Data);
}
if (DLen > 1000) {
spdlog::debug("(Launcher->Server) Bytes sent: " + std::to_string(Data.length()) + " : "
+ Data.substr(0, 10)
+ Data.substr(Data.length() - 10));
} else if (C == 'Z') {
// spdlog::debug("(Game->Launcher) : " + Data);
}
}
void Launcher::tcp_game_main() {
spdlog::debug("Game server starting on port {}", m_config->port + 1);
ip::tcp::endpoint listen_ep(ip::address::from_string("0.0.0.0"), static_cast<uint16_t>(m_config->port + 1));
ip::tcp::socket g_listener(m_io);
g_listener.open(listen_ep.protocol());
socket_base::linger linger_opt {};
linger_opt.enabled(false);
g_listener.set_option(linger_opt);
ip::tcp::acceptor g_acceptor(m_io, listen_ep);
g_acceptor.listen(socket_base::max_listen_connections);
spdlog::debug("Game server listening");
while (!*m_shutdown) {
spdlog::debug("Main loop");
// socket is connected at this point, spawn thread
m_client_thread = boost::scoped_thread<>(&Launcher::tcp_client_main, this);
spdlog::debug("Client thread spawned");
m_core_socket = g_acceptor.accept();
spdlog::debug("Game connected (tcp game)!");
spdlog::info("Successfully connected");
m_ping_thread = boost::scoped_thread<>(&Launcher::auto_ping, this);
m_udp_thread = boost::scoped_thread<>(&Launcher::udp_main, this);
int32_t Temp = 0;
do {
boost::system::error_code ec;
int32_t Rcv = 0;
int32_t Size = 0;
char Header[10] = { 0 };
do {
Temp = boost::asio::read(m_core_socket, buffer(&Header[Rcv], 1), ec);
if (ec) {
spdlog::error("(Game) Failed to receive from game: {}", ec.message());
break;
}
if (Temp < 1)
break;
if (!isdigit(Header[Rcv]) && Header[Rcv] != '>') {
spdlog::error("(Game) Invalid lua communication");
break;
}
} while (Header[Rcv++] != '>');
if (Temp < 1)
break;
if (std::from_chars(Header, &Header[Rcv], Size).ptr[0] != '>') {
spdlog::debug("(Game) Invalid lua Header -> " + std::string(Header, Rcv));
break;
}
std::vector<char> Ret(Size, 0);
Rcv = 0;
Temp = boost::asio::read(m_core_socket, buffer(Ret, Size), ec);
if (ec) {
spdlog::error("(Game) Failed to receive from game: {}", ec.message());
break;
}
spdlog::debug("Got {} from the game, sending to server", Ret[0]);
server_send(std::string(Ret.begin(), Ret.end()), false);
} while (Temp > 0);
if (Temp == 0) {
spdlog::debug("(Proxy) Connection closing");
} else {
spdlog::debug("(Proxy) Recv failed");
}
}
spdlog::debug("Game server exiting");
}
bool Launcher::start_sync(const std::string& Data) {
ip::tcp::resolver resolver(m_io);
const auto sep = Data.find_last_of(':');
if (sep == std::string::npos || sep == Data.length() - 1) {
spdlog::error("Invalid host:port string: '{}'", Data);
return false;
}
const auto host = Data.substr(1, sep - 1);
const auto service = Data.substr(sep + 1);
boost::system::error_code ec;
auto resolved = resolver.resolve(host, service, ec);
if (ec) {
spdlog::error("Failed to resolve '{}': {}", Data.substr(1), ec.message());
return false;
}
bool connected = false;
for (const auto& addr : resolved) {
m_tcp_socket.connect(addr.endpoint(), ec);
if (!ec) {
spdlog::info("Resolved and connected to '[{}]:{}'",
addr.endpoint().address().to_string(),
addr.endpoint().port());
connected = true;
if (addr.endpoint().address().is_v4()) {
m_udp_socket.open(ip::udp::v4());
} else {
m_udp_socket.open(ip::udp::v6());
}
m_udp_endpoint = ip::udp::endpoint(m_tcp_socket.remote_endpoint().address(), m_tcp_socket.remote_endpoint().port());
break;
}
}
if (!connected) {
spdlog::error("Failed to connect to '{}'", Data);
return false;
}
reset_status();
m_identity->check_local_key();
*m_ul_status = "UlLoading...";
auto thread = boost::scoped_thread<>(&Launcher::tcp_game_main, this);
m_tcp_game_thread.swap(thread);
return true;
}
void Launcher::tcp_send(const std::vector<uint8_t>& data) {
const auto Size = int32_t(data.size());
std::vector<uint8_t> ToSend;
ToSend.resize(data.size() + sizeof(Size));
std::memcpy(ToSend.data(), &Size, sizeof(Size));
std::memcpy(ToSend.data() + sizeof(Size), data.data(), data.size());
boost::system::error_code ec;
spdlog::debug("tcp sending {} bytes to the server", data.size());
boost::asio::write(m_tcp_socket, buffer(ToSend), ec);
if (ec) {
spdlog::debug("write(): {}", ec.message());
throw std::runtime_error(fmt::format("write() failed: {}", ec.message()));
}
spdlog::debug("tcp sent {} bytes to the server", data.size());
}
void Launcher::tcp_send(const std::string& data) {
tcp_send(std::vector<uint8_t>(data.begin(), data.end()));
}
std::string Launcher::tcp_recv() {
int32_t Header {};
boost::system::error_code ec;
std::array<uint8_t, sizeof(Header)> HeaderData {};
boost::asio::read(m_tcp_socket, buffer(HeaderData), ec);
if (ec) {
throw std::runtime_error(fmt::format("read() failed: {}", ec.message()));
}
Header = *reinterpret_cast<int32_t*>(HeaderData.data());
if (Header < 0) {
throw std::runtime_error("read() failed: Negative TCP header");
}
std::vector<uint8_t> Data;
// 100 MiB is super arbitrary but what can you do.
if (Header < int32_t(100 * (1024 * 1024))) {
Data.resize(Header);
} else {
throw std::runtime_error("Header size limit exceeded");
}
auto N = boost::asio::read(m_tcp_socket, buffer(Data), ec);
if (ec) {
throw std::runtime_error(fmt::format("read() failed: {}", ec.message()));
}
if (N != Header) {
throw std::runtime_error(fmt::format("read() failed: Expected {} byte(s), got {} byte(s) instead", Header, N));
}
constexpr std::string_view ABG = "ABG:";
if (Data.size() >= ABG.size() && std::equal(Data.begin(), Data.begin() + ABG.size(), ABG.begin(), ABG.end())) {
Data.erase(Data.begin(), Data.begin() + ABG.size());
Data = DeComp(Data);
}
return { reinterpret_cast<const char*>(Data.data()), Data.size() };
}
void Launcher::game_send(const std::string& data) {
auto to_send = data + "\n";
boost::asio::write(m_core_socket, buffer(reinterpret_cast<const uint8_t*>(to_send.data()), to_send.size()));
}
void Launcher::tcp_client_main() {
spdlog::debug("Client starting");
boost::system::error_code ec;
try {
{
// send C to say hi
boost::asio::write(m_tcp_socket, buffer("C", 1), ec);
// client version
tcp_send(fmt::format("VC{}.{}", PRJ_VERSION_MAJOR, PRJ_VERSION_MINOR));
// auth
auto res = tcp_recv();
if (res.empty() || res[0] == 'E') {
throw std::runtime_error("Kicked!");
} else if (res[0] == 'K') {
if (res.size() > 1) {
throw std::runtime_error(fmt::format("Kicked: {}", res.substr(1)));
} else {
throw std::runtime_error("Kicked!");
}
}
tcp_send(m_identity->PublicKey);
res = tcp_recv();
if (res.empty() || res[0] != 'P') {
throw std::runtime_error("Expected 'P'");
}
if (res.size() > 1 && std::all_of(res.begin() + 1, res.end(), [](char c) { return std::isdigit(c); })) {
*m_client_id = std::stoi(res.substr(1));
} else {
// auth failed
throw std::runtime_error("Authentication failed");
}
tcp_send("SR");
res = tcp_recv();
if (res.empty() || res[0] == 'E') {
throw std::runtime_error("Kicked!");
} else if (res[0] == 'K') {
if (res.size() > 1) {
throw std::runtime_error(fmt::format("Kicked: {}", res.substr(1)));
} else {
throw std::runtime_error("Kicked!");
}
}
if (res == "-") {
spdlog::info("Didn't receive any mods");
*m_list_of_mods = "-";
tcp_send("Done");
spdlog::info("Done!");
}
// auth done!
}
if (m_list_of_mods.get() != "-") {
spdlog::info("Checking resources");
if (!std::filesystem::exists("Resources")) {
std::filesystem::create_directories("Resources");
}
throw std::runtime_error("Mod loading not yet implemented");
}
std::string res;
while (!*m_shutdown) {
res = tcp_recv();
server_parse(res);
}
game_send("T");
} catch (const std::exception& e) {
spdlog::error("Fatal error: {}", e.what());
*m_shutdown = true;
}
spdlog::debug("Client stopping");
}
void Launcher::server_parse(const std::string& data) {
if (data.empty()) {
return;
}
switch (data[0]) {
case 'p':
*m_ping_end = std::chrono::high_resolution_clock::now();
if (m_ping_start.get() > m_ping_end.get()) {
*m_ping = 0;
} else {
*m_ping = int(std::chrono::duration_cast<std::chrono::milliseconds>(m_ping_end.get() - m_ping_start.get()).count());
}
break;
case 'M':
*m_m_status = data;
*m_ul_status = "Uldone";
break;
default:
game_send(data);
}
}
std::string Launcher::udp_recv() {
// the theoretical maximum udp message is 64 KiB, so we save one buffer per thread for it
static thread_local std::vector<char> s_local_buf(size_t(64u * 1024u));
auto n = m_udp_socket.receive_from(buffer(s_local_buf), m_udp_endpoint);
return { s_local_buf.data(), n };
}
void Launcher::udp_main() {
spdlog::info("UDP starting");
try {
game_send(std::string("P") + std::to_string(*m_client_id));
tcp_send("H");
udp_send("p");
while (!*m_shutdown) {
auto msg = udp_recv();
if (!msg.empty() && msg.length() > 4 && msg.substr(0, 4) == "ABG:") {
server_parse(std::string(DeComp(msg)));
} else {
server_parse(std::string(msg));
}
}
} catch (const std::exception& e) {
spdlog::error("Error in udp_main: {}", e.what());
}
spdlog::info("UDP stopping");
}
void Launcher::auto_ping() {
spdlog::debug("Ping thread started");
while (!*m_shutdown) {
udp_send("p");
*m_ping_start = std::chrono::high_resolution_clock::now();
std::this_thread::sleep_for(std::chrono::seconds(1));
}
spdlog::debug("Ping thread stopped");
}
void Launcher::udp_send(const std::string& data) {
auto vec = std::vector<uint8_t>(data.begin(), data.end());
if (data.length() > 400) {
compress_properly(vec);
}
std::vector<uint8_t> to_send = { uint8_t(*m_client_id), uint8_t(':') };
to_send.insert(to_send.end(), vec.begin(), vec.end());
m_udp_socket.send_to(buffer(to_send.data(), to_send.size()), m_udp_endpoint);
}

113
src/Launcher.h Normal file
View File

@@ -0,0 +1,113 @@
#pragma once
#include "Config.h"
#include "Identity.h"
#include "Sync.h"
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ip/udp.hpp>
#include <boost/thread/scoped_thread.hpp>
#include <filesystem>
#include <set>
#include <string>
#include <thread>
class Launcher {
public:
Launcher();
~Launcher();
void set_port(int p);
void check_for_updates(int argc, char** argv);
void set_exe_name(const std::string& name) { m_exe_name = name; }
void set_exe_path(const std::filesystem::path& path) { m_exe_path = path; }
void find_game();
void pre_game();
void start_game();
void start_network();
private:
/// Thread main function for the http(s) proxy thread.
void proxy_main();
/// Thread main function for the game thread.
void game_main();
/// Thread main function for the clien thread (the one that talks to the server)
void tcp_client_main();
void tcp_game_main();
void udp_main();
void auto_ping();
void parse_config();
static void check_mp(const std::string& path);
void enable_mp();
void net_core_main();
void reset_status();
void game_loop();
void handle_core_packet(const std::vector<char>& RawData);
bool start_sync(const std::string& Data);
void server_parse(const std::string& data);
void udp_send(const std::string& data);
void server_send(const std::string& data, bool Res);
void tcp_send(const std::string& data);
void send_large(const std::string& data);
void tcp_send(const std::vector<uint8_t>& data);
std::string tcp_recv();
std::string udp_recv();
void game_send(const std::string& data);
Sync<int> m_proxy_port;
Sync<int> m_ping;
Sync<int> m_client_id;
Sync<bool> m_mod_loaded { false };
Sync<Config> m_config;
boost::scoped_thread<> m_proxy_thread { &Launcher::proxy_main, this };
boost::scoped_thread<> m_game_thread;
boost::scoped_thread<> m_client_thread;
boost::scoped_thread<> m_tcp_game_thread;
boost::scoped_thread<> m_udp_thread;
boost::scoped_thread<> m_ping_thread;
Sync<Identity> m_identity {};
Sync<std::string> m_exe_name;
Sync<std::filesystem::path> m_exe_path;
boost::asio::io_context m_io {};
boost::asio::ip::tcp::socket m_game_socket;
boost::asio::ip::tcp::socket m_core_socket;
boost::asio::ip::tcp::socket m_tcp_socket;
boost::asio::ip::udp::socket m_udp_socket;
boost::asio::ip::udp::endpoint m_udp_endpoint;
Sync<bool> m_shutdown { false };
Sync<std::chrono::high_resolution_clock::time_point> m_ping_start;
Sync<std::chrono::high_resolution_clock::time_point> m_ping_end;
Sync<std::string> m_m_status {};
Sync<std::string> m_ul_status {};
Sync<std::set<std::string>> m_conf_list;
Sync<std::string> m_list_of_mods {};
};

14
src/Platform.h Normal file
View File

@@ -0,0 +1,14 @@
#pragma once
/// Platform-specific code
#include <string>
namespace plat {
void clear_screen();
void set_console_title(const std::string& title);
void URelaunch(int argc,char* args[]);
void ReLaunch(int argc,char*args[]);
std::string get_game_dir_magically();
std::string ask_for_folder();
}

49
src/PlatformLinux.cpp Normal file
View File

@@ -0,0 +1,49 @@
#include "Platform.h"
#if defined(PLATFORM_LINUX)
#include <fmt/core.h>
#include <spdlog/spdlog.h>
#include <filesystem>
void plat::ReLaunch(int argc, char* args[]) {
spdlog::warn("Not implemented: {}", __func__);
}
void plat::URelaunch(int argc, char** argv) {
spdlog::warn("Not implemented: {}", __func__);
}
void plat::set_console_title(const std::string& title) {
//fmt::print("\x1b]2;{}\0", title);
}
void plat::clear_screen() {
// unwanted on linux, due to the ability to run this as a command in an existing console.
}
std::string plat::get_game_dir_magically() {
spdlog::warn("Not implemented: {}", __func__);
for (const auto& path : { "" }) {
if (std::filesystem::exists(std::filesystem::path(path) / "BeamNG.drive.exe")) {
return path;
}
}
return "";
}
#include <iostream>
#include <filesystem>
std::string plat::ask_for_folder() {
spdlog::info("Asking user for path");
fmt::print("====\n"
"Couldn't find game directory, please enter the path to the game (the folder that contains 'BeamNG.drive.exe')\n"
"Path: ");
std::string folder;
std::getline(std::cin, folder);
while (!std::filesystem::exists(std::filesystem::path(folder) / "BeamNG.drive.exe")) {
fmt::print("This folder ('{}') doesn't contain 'BeamNG.drive.exe', please try again.\n", folder);
fmt::print("Path: ");
std::getline(std::cin, folder);
}
fmt::print("Thank you!\n====\n");
spdlog::info("Game path found");
return folder;
}
#endif

177
src/PlatformWindows.cpp Normal file
View File

@@ -0,0 +1,177 @@
#include "Platform.h"
#if defined(PLATFORM_WINDOWS)
#include <spdlog/spdlog.h>
#include <windows.h>
void plat::ReLaunch(int argc, char** argv) {
std::string Arg;
for (int c = 2; c <= argc; c++) {
Arg += " ";
Arg += argv[c - 1];
}
system("cls");
ShellExecute(nullptr, "runas", argv[0], Arg.c_str(), nullptr, SW_SHOWNORMAL);
ShowWindow(GetConsoleWindow(), 0);
std::this_thread::sleep_for(std::chrono::seconds(1));
exit(1);
}
void plat::URelaunch(int argc, char** argv) {
std::string Arg;
for (int c = 2; c <= argc; c++) {
Arg += " ";
Arg += argv[c - 1];
}
ShellExecute(nullptr, "open", argv[0], Arg.c_str(), nullptr, SW_SHOWNORMAL);
ShowWindow(GetConsoleWindow(), 0);
std::this_thread::sleep_for(std::chrono::seconds(1));
exit(1);
}
void plat::set_console_title(const std::string& title) {
SetConsoleTitleA(title);
}
void plat::clear_screen() {
system("cls");
}
LONG OpenKey(HKEY root,const char* path,PHKEY hKey){
return RegOpenKeyEx(root, reinterpret_cast<LPCSTR>(path), 0, KEY_READ, hKey);
}
#define MAX_KEY_LENGTH 255
#define MAX_VALUE_NAME 16383
std::string QueryKey(HKEY hKey,int ID){
TCHAR achKey[MAX_KEY_LENGTH]; // 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
DWORD cSubKeys=0; // number of subkeys
DWORD cbMaxSubKey; // longest subkey size
DWORD cchMaxClass; // longest class string
DWORD cValues; // number of values for key
DWORD cchMaxValue; // longest value name
DWORD cbMaxValueData; // longest value data
DWORD cbSecurityDescriptor; // size of security descriptor
FILETIME ftLastWriteTime; // last write time
DWORD i, retCode;
TCHAR achValue[MAX_VALUE_NAME];
DWORD cchValue = MAX_VALUE_NAME;
retCode = RegQueryInfoKey(
hKey, // key handle
achClass, // buffer for class name
&cchClassName, // size of class string
nullptr, // reserved
&cSubKeys, // number of subkeys
&cbMaxSubKey, // longest subkey size
&cchMaxClass, // longest class string
&cValues, // number of values for this key
&cchMaxValue, // longest value name
&cbMaxValueData, // longest value data
&cbSecurityDescriptor, // security descriptor
&ftLastWriteTime); // last write time
BYTE* buffer = new BYTE[cbMaxValueData];
ZeroMemory(buffer, cbMaxValueData);
if (cSubKeys){
for (i=0; i<cSubKeys; i++){
cbName = MAX_KEY_LENGTH;
retCode = RegEnumKeyEx(hKey, i,achKey,&cbName,nullptr,nullptr,nullptr,&ftLastWriteTime);
if (retCode == ERROR_SUCCESS){
if(strcmp(achKey,"Steam App 284160") == 0){
return achKey;
}
}
}
}
if (cValues){
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);
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;
switch (ID){
case 1: if(key == "SteamExe"){
auto p = data.find_last_of("/\\");
if(p != std::string::npos){
return data.substr(0,p);
}
}
break;
case 2: if(key == "Name" && data == "BeamNG.drive")return data;break;
case 3: if(key == "rootpath")return data;break;
case 4: if(key == "userpath_override")return data;
case 5: if(key == "Local AppData")return data;
default: break;
}
}
}
}
delete [] buffer;
return "";
}
std::string plat::get_game_dir_magically() {
std::string Result;
std::string K3 = R"(Software\BeamNG\BeamNG.drive)";
HKEY hKey;
LONG dwRegOPenKey = OpenKey(HKEY_CURRENT_USER, K3.c_str(), &hKey);
if (dwRegOPenKey == ERROR_SUCCESS) {
Result = QueryKey(hKey, 3);
if (Result.empty()) {
spdlog::error("Couldn't query key from HKEY_CURRENT_USER\\Software\\BeamNG\\BeamNG.drive");
Result = "";
}
} else {
spdlog::error("Couldn't open HKEY_CURRENT_USER\\Software\\BeamNG\\BeamNG.drive");
}
RegCloseKey(hKey);
return Result;
}
static int CALLBACK BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData) {
if (uMsg == BFFM_INITIALIZED) {
std::string tmp = (const char*)lpData;
std::cout << "path: " << tmp << std::endl;
SendMessage(hwnd, BFFM_SETSELECTION, TRUE, lpData);
}
return 0;
}
std::string plat::ask_for_folder() {
TCHAR path[MAX_PATH];
const char* path_param = saved_path.c_str();
BROWSEINFO bi = { 0 };
bi.lpszTitle = ("Browse for folder...");
bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE;
bi.lpfn = BrowseCallbackProc;
bi.lParam = (LPARAM)path_param;
LPITEMIDLIST pidl = SHBrowseForFolder(&bi);
if (pidl != 0) {
// get the name of the folder and put it in path
SHGetPathFromIDList(pidl, path);
// free memory used
IMalloc* imalloc = 0;
if (SUCCEEDED(SHGetMalloc(&imalloc))) {
imalloc->Free(pidl);
imalloc->Release();
}
return path;
}
return "";
}
#endif

161
src/ServerNetwork.cpp Normal file
View File

@@ -0,0 +1,161 @@
#include "ServerNetwork.h"
#include "ClientInfo.h"
#include "ImplementationInfo.h"
#include "Launcher.h"
#include "ProtocolVersion.h"
#include "ServerInfo.h"
#include <spdlog/spdlog.h>
ServerNetwork::ServerNetwork(Launcher& launcher, const ip::tcp::endpoint& ep)
: m_launcher(launcher)
, m_tcp_ep(ep) {
spdlog::debug("Server network created");
}
ServerNetwork::~ServerNetwork() {
spdlog::debug("Server network destroyed");
}
void ServerNetwork::run() {
boost::system::error_code ec;
m_tcp_socket.connect(m_tcp_ep, ec);
if (ec) {
spdlog::error("Failed to connect to [{}]:{}: {}", m_tcp_ep.address().to_string(), m_tcp_ep.port(), ec.message());
throw std::runtime_error(ec.message());
}
// start the connection by going to the identification state and sending
// the first packet in the identification protocol (protocol version)
m_state = bmp::State::Identification;
bmp::Packet version_packet {
.purpose = bmp::Purpose::ProtocolVersion,
.raw_data = std::vector<uint8_t>(6),
};
struct bmp::ProtocolVersion version {
.version = {
.major = 1,
.minor = 0,
.patch = 0,
},
};
version.serialize_to(version_packet.raw_data);
tcp_write(version_packet);
// main tcp recv loop
while (true) {
auto packet = tcp_read();
handle_packet(packet);
}
}
void ServerNetwork::handle_packet(const bmp::Packet& packet) {
switch (m_state) {
case bmp::State::None:
m_state = bmp::State::Identification;
[[fallthrough]];
case bmp::State::Identification:
handle_identification(packet);
break;
case bmp::State::Authentication:
break;
case bmp::State::ModDownload:
break;
case bmp::State::SessionSetup:
break;
case bmp::State::Playing:
break;
case bmp::State::Leaving:
break;
}
}
void ServerNetwork::handle_identification(const bmp::Packet& packet) {
switch (packet.purpose) {
case bmp::Purpose::ProtocolVersionOk: {
spdlog::debug("Protocol version ok");
bmp::Packet ci_packet {
.purpose = bmp::Purpose::ClientInfo,
.raw_data = std::vector<uint8_t>(1024),
};
// TODO: Game and mod version
struct bmp::ClientInfo ci {
.program_version = { .major = PRJ_VERSION_MAJOR, .minor = PRJ_VERSION_MINOR, .patch = PRJ_VERSION_PATCH },
.game_version = { .major = 4, .minor = 5, .patch = 6 },
.mod_version = { .major = 1, .minor = 2, .patch = 3 },
.implementation = bmp::ImplementationInfo {
.value = "Official BeamMP Launcher",
},
};
auto sz = ci.serialize_to(ci_packet.raw_data);
ci_packet.raw_data.resize(sz);
tcp_write(ci_packet);
break;
}
case bmp::Purpose::ProtocolVersionBad:
spdlog::error("The server rejected our protocol version!");
throw std::runtime_error("Protocol version rejected");
break;
case bmp::Purpose::ServerInfo: {
struct bmp::ServerInfo si { };
si.deserialize_from(packet.get_readable_data());
spdlog::debug("Connected to server implementation '{}' v{}.{}.{}",
si.implementation.value,
si.program_version.major,
si.program_version.minor,
si.program_version.patch);
break;
}
case bmp::Purpose::StateChangeAuthentication: {
spdlog::debug("Starting authentication");
m_state = bmp::State::Authentication;
break;
}
default:
spdlog::error("Got 0x{:x} in state {}. This is not allowed. Disconnecting", uint16_t(packet.purpose), int(m_state));
// todo: disconnect gracefully
break;
}
}
bmp::Packet ServerNetwork::tcp_read() {
bmp::Packet packet {};
std::vector<uint8_t> header_buffer(bmp::Header::SERIALIZED_SIZE);
read(m_tcp_socket, buffer(header_buffer));
bmp::Header hdr {};
hdr.deserialize_from(header_buffer);
// vector eaten up by now, recv again
packet.raw_data.resize(hdr.size);
read(m_tcp_socket, buffer(packet.raw_data));
packet.purpose = hdr.purpose;
packet.flags = hdr.flags;
return packet;
}
void ServerNetwork::tcp_write(bmp::Packet& packet) {
// finalize the packet (compress etc) and produce header
auto header = packet.finalize();
// serialize header
std::vector<uint8_t> header_data(bmp::Header::SERIALIZED_SIZE);
header.serialize_to(header_data);
// write header and packet data
write(m_tcp_socket, buffer(header_data));
write(m_tcp_socket, buffer(packet.raw_data));
}
bmp::Packet ServerNetwork::udp_read(ip::udp::endpoint& out_ep) {
// maximum we can ever expect from udp
static thread_local std::vector<uint8_t> s_buffer(std::numeric_limits<uint16_t>::max());
m_udp_socket.receive_from(buffer(s_buffer), out_ep, {});
bmp::Packet packet;
bmp::Header header {};
auto offset = header.deserialize_from(s_buffer);
packet.raw_data.resize(header.size);
std::copy(s_buffer.begin() + offset, s_buffer.begin() + offset + header.size, packet.raw_data.begin());
return packet;
}
void ServerNetwork::udp_write(bmp::Packet& packet) {
auto header = packet.finalize();
std::vector<uint8_t> data(header.size + bmp::Header::SERIALIZED_SIZE);
auto offset = header.serialize_to(data);
std::copy(packet.raw_data.begin(), packet.raw_data.end(), data.begin() + static_cast<long>(offset));
m_udp_socket.send_to(buffer(data), m_udp_ep, {});
}

45
src/ServerNetwork.h Normal file
View File

@@ -0,0 +1,45 @@
#pragma once
#include "Packet.h"
#include "State.h"
#include <boost/asio.hpp>
using namespace boost::asio;
class Launcher;
class ServerNetwork {
public:
ServerNetwork(Launcher& launcher, const ip::tcp::endpoint& ep);
~ServerNetwork();
/// Starts and runs the connection to the server.
/// Calls back to the Launcher.
/// Blocking!
void run();
private:
/// Reads a single packet from the TCP stream. Blocks all other reads (not writes).
bmp::Packet tcp_read();
/// Writes the packet to the TCP stream. Blocks all other writes.
void tcp_write(bmp::Packet& packet);
/// Reads a packet from the given UDP socket, returning the client's endpoint as an out-argument.
bmp::Packet udp_read(ip::udp::endpoint& out_ep);
/// Sends a packet to the specified UDP endpoint via the UDP socket.
void udp_write(bmp::Packet& packet);
void handle_packet(const bmp::Packet& packet);
void handle_identification(const bmp::Packet& packet);
io_context m_io {};
ip::tcp::socket m_tcp_socket { m_io };
ip::udp::socket m_udp_socket { m_io };
bmp::State m_state {};
Launcher& m_launcher;
ip::tcp::endpoint m_tcp_ep;
ip::udp::endpoint m_udp_ep;
};

11
src/Sync.h Normal file
View File

@@ -0,0 +1,11 @@
#pragma once
#include <boost/thread/synchronized_value.hpp>
#include <mutex>
/// This header provides convenience aliases for synchronization primitives.
template<typename T>
using Sync = boost::synchronized_value<T, std::recursive_mutex>;

37
src/Version.cpp Normal file
View File

@@ -0,0 +1,37 @@
#include "Version.h"
#include <charconv>
#include <fmt/format.h>
#include <sstream>
Version::Version(uint8_t major, uint8_t minor, uint8_t patch)
: major(major)
, minor(minor)
, patch(patch) { }
Version::Version(const std::array<uint8_t, 3>& v)
: Version(v[0], v[1], v[2]) {
}
std::string Version::to_string() {
return fmt::format("{:d}.{:d}.{:d}", major, minor, patch);
}
bool Version::is_outdated(const Version& new_version) {
if (new_version.major > major) {
return true;
} else if (new_version.major == major && new_version.minor > minor) {
return true;
} else if (new_version.major == major && new_version.minor == minor && new_version.patch > patch) {
return true;
} else {
return false;
}
}
Version::Version(const std::string& str) {
std::stringstream ss(str);
std::string Part;
std::getline(ss, Part, '.');
std::from_chars(&*Part.begin(), &*Part.begin() + Part.size(), major);
std::from_chars(&*Part.begin(), &*Part.begin() + Part.size(), minor);
std::from_chars(&*Part.begin(), &*Part.begin() + Part.size(), patch);
}

16
src/Version.h Normal file
View File

@@ -0,0 +1,16 @@
#pragma once
#include <cstdint>
#include <array>
#include <string>
struct Version {
uint8_t major {};
uint8_t minor {};
uint8_t patch {};
Version(uint8_t major, uint8_t minor, uint8_t patch);
Version(const std::array<uint8_t, 3>& v);
explicit Version(const std::string& str);
std::string to_string();
bool is_outdated(const Version& new_version);
};

125
src/main.cpp Normal file
View File

@@ -0,0 +1,125 @@
#include "Launcher.h"
#include "Platform.h"
#include <iostream>
#include <thread>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/spdlog.h>
/// Sets up a file- and console logger and makes it the default spdlog logger.
static void setup_logger(bool debug = false);
static std::shared_ptr<spdlog::logger> default_logger {};
int main(int argc, char** argv) {
bool enable_debug = false;
bool enable_dev = false;
int custom_port = 0;
std::string_view invalid_arg;
for (int i = 1; i < argc; ++i) {
std::string_view arg(argv[i]);
std::string_view next(i + 1 < argc ? argv[i + 1] : "");
// --debug flag enables debug printing in console
if (arg == "--debug") {
enable_debug = true;
// --dev enables developer mode (game is not started)
} else if (arg == "--dev") {
enable_dev = true;
} else if (arg == "0" && next == "0") {
enable_dev = true;
++i;
// an argument that is all digits and not 0 is a custom port, backwards-compat
} else if (std::all_of(arg.begin(), arg.end(), [](char c) { return std::isdigit(c); })) {
custom_port = std::stoi(arg.data());
} else {
invalid_arg = arg;
}
}
setup_logger(enable_debug || enable_dev);
spdlog::debug("Debug enabled");
if (!invalid_arg.empty()) {
spdlog::warn("Invalid argument passed via commandline switches: '{}'. This argument was ignored.", invalid_arg);
}
plat::clear_screen();
plat::set_console_title(fmt::format("BeamMP Launcher v{}.{}.{}", PRJ_VERSION_MAJOR, PRJ_VERSION_MINOR, PRJ_VERSION_PATCH));
spdlog::info("BeamMP Launcher v{}.{}.{} is a PRE-RELEASE build. Please report any errors immediately at https://github.com/BeamMP/BeamMP-Launcher.",
PRJ_VERSION_MAJOR, PRJ_VERSION_MINOR, PRJ_VERSION_PATCH);
Launcher launcher {};
std::filesystem::path arg0(argv[0]);
launcher.set_exe_name(arg0.filename().generic_string());
launcher.set_exe_path(arg0.parent_path());
if (custom_port > 0) {
launcher.set_port(custom_port);
}
if (!enable_dev) {
launcher.check_for_updates(argc, argv);
} else {
spdlog::debug("Skipping update check due to dev mode");
}
launcher.find_game();
if (!enable_dev) {
launcher.pre_game();
launcher.start_game();
}
launcher.start_network();
spdlog::info("Shutting down.");
}
void setup_logger(bool debug) {
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
console_sink->set_pattern("[%H:%M:%S] [%^%l%$] %v");
if (debug) {
console_sink->set_level(spdlog::level::debug);
} else {
console_sink->set_level(spdlog::level::info);
}
auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("Launcher.log", true);
file_sink->set_level(spdlog::level::trace);
default_logger = std::make_shared<spdlog::logger>(spdlog::logger("default", { console_sink, file_sink }));
default_logger->set_level(spdlog::level::trace);
default_logger->flush_on(spdlog::level::info);
spdlog::set_default_logger(default_logger);
spdlog::debug("Logger initialized");
}
/*
int oldmain(int argc, char* argv[]) {
#ifdef DEBUG
std::thread th(flush);
th.detach();
#endif
GetEP(argv[0]);
InitLauncher(argc, argv);
try {
LegitimacyCheck();
} catch (std::exception& e) {
fatal("Main 1 : " + std::string(e.what()));
}
StartProxy();
PreGame(GetGameDir());
InitGame(GetGameDir());
CoreNetwork();
/// TODO: make sure to use argv[0] for everything that should be in the same dir (mod down ect...)
}
*/