mirror of
https://github.com/BeamMP/BeamMP-Launcher.git
synced 2026-04-07 16:26:02 +00:00
move new protocol code into main repo
This commit is contained in:
57
src/Compression.h
Normal file
57
src/Compression.h
Normal 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
41
src/Config.cpp
Executable 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
14
src/Config.h
Normal 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
35
src/Hashing.cpp
Normal 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
5
src/Hashing.h
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
std::string sha256_file(const std::string& path);
|
||||
115
src/Http.cpp
Executable file
115
src/Http.cpp
Executable 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
18
src/Http.h
Executable 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
114
src/Identity.cpp
Normal 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
18
src/Identity.h
Normal 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
888
src/Launcher.cpp
Normal 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
113
src/Launcher.h
Normal 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
14
src/Platform.h
Normal 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
49
src/PlatformLinux.cpp
Normal 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
177
src/PlatformWindows.cpp
Normal 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
161
src/ServerNetwork.cpp
Normal 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
45
src/ServerNetwork.h
Normal 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
11
src/Sync.h
Normal 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
37
src/Version.cpp
Normal 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
16
src/Version.h
Normal 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
125
src/main.cpp
Normal 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...)
|
||||
}
|
||||
*/
|
||||
Reference in New Issue
Block a user