mirror of
https://github.com/Awakenchan/LanternEngine.git
synced 2026-11-10 17:02:57 -10:00
268 lines
14 KiB
C++
268 lines
14 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
#include "online.h"
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
namespace start {
|
|
namespace {
|
|
|
|
const char* const kGameStatusOrigin = "https://ss4.scej-network.jp:20443";
|
|
const char* const kHuntersDream = "https://thehuntersdream.com";
|
|
const char* const kPlayLogHosts[] = {"bb-playlog-prod.s3.amazonaws.com", "bb-playlog-test.s3.amazonaws.com"};
|
|
constexpr int kGameApiPort = 18671;
|
|
const char* const kShadnetHost = "srv.shadps4.net";
|
|
constexpr int kShadnetPort = 31313;
|
|
constexpr int kDefaultP2p = 3658;
|
|
constexpr int kDefaultSignTimeout = 30;
|
|
const char* const kThdEnv[] = {"BB_NET_BACKEND", "BB_THD_SERVER", "BB_THD_AUTH", "BB_THD_TOKEN", "BB_THD_ONLINE_ID",
|
|
"BB_THD_RULESET"};
|
|
const char* const kSeamlessServer = "https://185-244-28-175.sslip.io";
|
|
const char* const kDirectPort = "38765";
|
|
const char* const kRoomEnv[] = {"BB_ROOM_SERVER", "BB_ROOM_NAME", "BB_ROOM_PLAYER", "BB_ROOM_HOST", "BB_ROOM_HTTP_PORT", "BB_ROOM_LOCAL_PORT", "BB_ROOM_VISITOR"};
|
|
const char* const kOnlineEnv[] = {"BB_ONLINE", "BB_SHADNET_SERVER", "BB_SHADNET_WEBAPI", "BB_SHADNET_NPID",
|
|
"BB_SHADNET_PASSWORD", "BB_SHADNET_TOKEN", "BB_UPNP", "BB_P2P_PORT",
|
|
"SHADPS4_HTTP_HOST_OVERRIDES_JSON", "BB_SIGN_TIMEOUT"};
|
|
|
|
struct Url {
|
|
std::string scheme, host, path, query, fragment;
|
|
std::optional<int> port;
|
|
bool credentials = false;
|
|
};
|
|
|
|
/// urllib's urlsplit, for the origins typed here.
|
|
Url Parse(const std::string& text) {
|
|
Url url;
|
|
std::string rest = text;
|
|
const auto colon = rest.find("://");
|
|
if (colon != std::string::npos) {
|
|
url.scheme = Lower(rest.substr(0, colon));
|
|
rest = rest.substr(colon + 3);
|
|
}
|
|
if (const auto hash = rest.find('#'); hash != std::string::npos) url.fragment = rest.substr(hash + 1), rest.resize(hash);
|
|
if (const auto q = rest.find('?'); q != std::string::npos) url.query = rest.substr(q + 1), rest.resize(q);
|
|
const auto slash = rest.find('/');
|
|
std::string authority = rest.substr(0, slash);
|
|
url.path = slash == std::string::npos ? "" : rest.substr(slash);
|
|
if (const auto at = authority.rfind('@'); at != std::string::npos) {
|
|
url.credentials = true;
|
|
authority = authority.substr(at + 1);
|
|
}
|
|
std::string host = authority;
|
|
if (!host.empty() && host[0] == '[') {
|
|
const auto close = host.find(']');
|
|
url.host = Lower(host.substr(1, close - 1));
|
|
host = close == std::string::npos ? "" : host.substr(close + 1);
|
|
if (!host.empty() && host[0] == ':') {
|
|
const std::string port = host.substr(1);
|
|
if (!port.empty()) url.port = std::stoi(port);
|
|
}
|
|
} else {
|
|
const auto c = host.rfind(':');
|
|
url.host = Lower(host.substr(0, c));
|
|
if (c != std::string::npos) {
|
|
const std::string port = host.substr(c + 1);
|
|
if (!port.empty()) {
|
|
if (port.find_first_not_of("0123456789") != std::string::npos || port.size() > 6)
|
|
throw Error("Invalid online API port");
|
|
url.port = std::stoi(port);
|
|
}
|
|
}
|
|
}
|
|
return url;
|
|
}
|
|
|
|
std::string GameServer(std::string text) {
|
|
text = Trim(text);
|
|
if (text.empty()) return kHuntersDream;
|
|
if (text.find("://") == std::string::npos) text = "https://" + text;
|
|
if (Parse(text).host == kShadnetHost) return kHuntersDream;
|
|
return text;
|
|
}
|
|
|
|
std::string HttpBase(const std::string& text) {
|
|
if (text.empty() || std::any_of(text.begin(), text.end(), [](char c) { return std::uint8_t(c) < 32; }))
|
|
throw Error("Online requires an explicit WebAPI / game API URL");
|
|
const Url value = Parse(text);
|
|
if ((value.scheme != "http" && value.scheme != "https") || value.host.empty() || value.credentials ||
|
|
!value.query.empty() || !value.fragment.empty())
|
|
throw Error("Online API must be an HTTP(S) URL without credentials, query or fragment");
|
|
if (value.port && (*value.port < 1 || *value.port > 65535)) throw Error("Invalid online API port");
|
|
std::string out = text;
|
|
while (!out.empty() && out.back() == '/') out.pop_back();
|
|
return out;
|
|
}
|
|
|
|
fs::path WriteOverrides(const fs::path& data, const nlohmann::ordered_json& overrides) {
|
|
const fs::path destination = data / "network" / "host_overrides.json";
|
|
WriteText(destination, overrides.dump(2));
|
|
return fs::absolute(destination);
|
|
}
|
|
|
|
std::string P2pPort(const std::map<std::string, std::string>& settings) {
|
|
const std::string p2p = Trim(Setting(settings, "online_p2p_port").value_or(""));
|
|
if (p2p.empty()) return std::to_string(kDefaultP2p);
|
|
int port = 0;
|
|
try {
|
|
port = std::stoi(p2p);
|
|
} catch (const std::exception&) {
|
|
throw Error("Local P2P port must be between 1 and 65535, or empty for the default (3658)");
|
|
}
|
|
if (port < 1 || port > 65535) throw Error("Local P2P port must be between 1 and 65535, or empty for the default (3658)");
|
|
return std::to_string(port);
|
|
}
|
|
|
|
void SetSignTimeout(const std::map<std::string, std::string>& settings) {
|
|
const std::string timeout = Trim(Setting(settings, "online_sign_timeout").value_or(""));
|
|
if (!timeout.empty() && timeout.find_first_not_of("0123456789") == std::string::npos && timeout.size() < 10)
|
|
SetEnv("BB_SIGN_TIMEOUT", std::to_string(std::clamp(std::stoi(timeout), 0, 600)));
|
|
else
|
|
SetEnv("BB_SIGN_TIMEOUT", std::to_string(kDefaultSignTimeout));
|
|
}
|
|
|
|
bool ConfigureHuntersDream(const std::map<std::string, std::string>& settings, const fs::path& data) {
|
|
const std::string game_api = HttpBase(GameServer(Setting(settings, "online_game_api").value_or("")));
|
|
const Url origin = Parse(game_api);
|
|
if (!origin.path.empty() && origin.path != "/") throw Error("The game server needs an origin without a path");
|
|
if (Env("BB_THD_TOKEN", "").empty() || Env("BB_THD_ONLINE_ID", "").empty())
|
|
throw Error("Sign in to The Hunter's Dream with Discord on the launcher's Online page");
|
|
const std::string base = origin.scheme + "://" + origin.host;
|
|
nlohmann::ordered_json overrides;
|
|
overrides["*.scej-network.jp"] = base;
|
|
for (const char* host : kPlayLogHosts) overrides[host] = base + ":" + std::to_string(kGameApiPort);
|
|
const fs::path destination = WriteOverrides(data, overrides);
|
|
for (const char* key : {"BB_SHADNET_SERVER", "BB_SHADNET_WEBAPI", "BB_SHADNET_NPID", "BB_SHADNET_PASSWORD",
|
|
"BB_SHADNET_TOKEN", "BB_UPNP"})
|
|
UnsetEnv(key);
|
|
SetEnv("BB_ONLINE", "1");
|
|
SetEnv("BB_NET_BACKEND", "thd");
|
|
SetEnv("BB_THD_SERVER", base + ":" + std::to_string(kGameApiPort));
|
|
SetEnv("BB_THD_AUTH", base);
|
|
const std::string ruleset = Trim(Setting(settings, "online_thd_ruleset").value_or(""));
|
|
SetEnv("BB_THD_RULESET", ruleset.empty() ? "vanilla" : ruleset);
|
|
SetEnv("BB_P2P_PORT", P2pPort(settings));
|
|
SetEnv("SHADPS4_HTTP_HOST_OVERRIDES_JSON", Utf8(destination));
|
|
SetSignTimeout(settings);
|
|
return true;
|
|
}
|
|
|
|
bool ConfigureSeamless(const std::map<std::string, std::string>& settings, const fs::path& data) {
|
|
const auto get = [&](const char* key) { return Trim(Setting(settings, key).value_or("")); };
|
|
std::string server = get("seamless_server");
|
|
if (server.empty()) server = kSeamlessServer;
|
|
const bool host = get("seamless_role") == "host";
|
|
const std::string room = get(host ? "seamless_host_room" : "seamless_room");
|
|
std::string player;
|
|
for (const char c : get("seamless_player")) {
|
|
const auto u = std::uint8_t(c);
|
|
if (u >= 0x20 && u < 0x7f && c != '"' && c != '\\' &&!(c == ' ' && (player.empty() || player.back() == ' ')))
|
|
player.push_back(c);
|
|
if (player.size() == 16) break;
|
|
}
|
|
while (!player.empty() && player.back() == ' ') player.pop_back();
|
|
if (player.empty()) player = "Hunter";
|
|
if (room.empty())
|
|
throw Error(host ? "Open the launcher's Online page once to make your room" : "Paste the host's invite code on the launcher's Online page");
|
|
|
|
const int p2p = std::stoi(P2pPort(settings));
|
|
const int http = p2p < 65535 ? p2p + 1 : p2p - 1;
|
|
const std::string local = "http://127.0.0.1:" + std::to_string(http);
|
|
nlohmann::ordered_json overrides;
|
|
overrides["*.scej-network.jp"] = local;
|
|
overrides[kGameStatusOrigin] = local;
|
|
for (const char* host : kPlayLogHosts) overrides[host] = local;
|
|
const fs::path destination = WriteOverrides(data, overrides);
|
|
for (const char* key : kOnlineEnv) UnsetEnv(key);
|
|
for (const char* key : kThdEnv) UnsetEnv(key);
|
|
SetEnv("BB_ONLINE", "1");
|
|
SetEnv("BB_NET_BACKEND", "room");
|
|
const std::string direct_port = get("seamless_direct_port").empty() ? std::string(kDirectPort) : get("seamless_direct_port");
|
|
const bool direct = get("seamless_direct") == "1";
|
|
if (direct && !host) {
|
|
std::string address = get("seamless_direct_host");
|
|
if (address.empty()) throw Error("Paste the host's invite code, or type the host's address on the launcher's Online page");
|
|
if (address.find(':') == std::string::npos) address += ":" + direct_port;
|
|
server = "ws://" + address;
|
|
UnsetEnv("BB_ROOM_LOCAL_PORT");
|
|
} else if (direct) {
|
|
const int port = std::atoi(direct_port.c_str());
|
|
if (port < 1024 || port > 65535) throw Error("The direct room's port must be between 1024 and 65535");
|
|
server = "ws://127.0.0.1:" + std::to_string(port);
|
|
SetEnv("BB_ROOM_LOCAL_PORT", std::to_string(port));
|
|
} else {
|
|
UnsetEnv("BB_ROOM_LOCAL_PORT");
|
|
if (server.rfind("ws://", 0) == 0) server = kSeamlessServer;
|
|
}
|
|
SetEnv("BB_ROOM_SERVER", server);
|
|
SetEnv("BB_ROOM_NAME", room);
|
|
SetEnv("BB_ROOM_PLAYER", player);
|
|
SetEnv("BB_ROOM_HOST", host ? "1" : "0");
|
|
if (!host && !direct && get("seamless_visitor") == "1") SetEnv("BB_ROOM_VISITOR", "1");
|
|
else UnsetEnv("BB_ROOM_VISITOR");
|
|
if (host) SetEnv("BB_ROOM_PASSWORD", Env("BB_ROOM_HOST_PASSWORD", ""));
|
|
SetEnv("BB_ROOM_HTTP_PORT", std::to_string(http));
|
|
SetEnv("BB_P2P_PORT", std::to_string(p2p));
|
|
SetEnv("SHADPS4_HTTP_HOST_OVERRIDES_JSON", Utf8(destination));
|
|
SetSignTimeout(settings);
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool ConfigureOnline(const std::map<std::string, std::string>& settings, const fs::path& data) {
|
|
const auto get = [&](const char* key, const char* fallback = "") { return Setting(settings, key).value_or(fallback); };
|
|
if (get("seamless_enabled") == "1" && get("online_enabled") == "1") return ConfigureSeamless(settings, data);
|
|
for (const char* key : kRoomEnv) UnsetEnv(key);
|
|
if (get("online_pool") == "huntersdream" && get("online_enabled") == "1" && get("online_connected", "1") == "1")
|
|
return ConfigureHuntersDream(settings, data);
|
|
const bool paused = Env("BB_ONLINE_PAUSED", "0") == "1";
|
|
if (paused && get("online_enabled") == "1")
|
|
Log("Online: paused in this version while we rework the connection together with the shadNet and The Hunter's "
|
|
"Dream teams; the game starts offline");
|
|
if (paused || get("online_enabled") != "1" || get("online_connected", "1") != "1") {
|
|
for (const char* key : kOnlineEnv) UnsetEnv(key);
|
|
for (const char* key : kThdEnv) UnsetEnv(key);
|
|
SetEnv("BB_ONLINE", "0");
|
|
return false;
|
|
}
|
|
for (const char* key : kThdEnv) UnsetEnv(key);
|
|
std::string host = Trim(get("online_host"));
|
|
while (!host.empty() && (host.front() == '[' || host.front() == ']')) host.erase(0, 1);
|
|
while (!host.empty() && (host.back() == '[' || host.back() == ']')) host.pop_back();
|
|
if (host.empty()) host = kShadnetHost;
|
|
if (std::any_of(host.begin(), host.end(),
|
|
[](char c) { return std::isspace(std::uint8_t(c)) || std::uint8_t(c) < 32 || std::strchr("/@?#:", c); }))
|
|
throw Error("Enter a shadNet server name without a scheme, path or port");
|
|
int port = kShadnetPort;
|
|
if (const std::string p = get("online_port"); !p.empty()) port = std::stoi(p);
|
|
if (port < 1 || port > 65535) throw Error("shadNet TCP port must be between 1 and 65535");
|
|
const std::string default_webapi = "http://" + host + ":31315";
|
|
const std::string typed_webapi = Trim(get("online_webapi"));
|
|
const std::string webapi = HttpBase(typed_webapi.empty() ? default_webapi : typed_webapi);
|
|
const std::string game_api = HttpBase(GameServer(get("online_game_api")));
|
|
const std::string webapi_path = Parse(webapi).path, game_path = Parse(game_api).path;
|
|
if ((!webapi_path.empty() && webapi_path != "/") || (!game_path.empty() && game_path != "/"))
|
|
throw Error("WebAPI and game API require a server origin without an extra path");
|
|
const std::string username = get("online_username");
|
|
if (username.empty() || username.size() > 16 ||
|
|
std::any_of(username.begin(), username.end(), [](char c) { return std::isspace(std::uint8_t(c)) || std::uint8_t(c) < 32; }))
|
|
throw Error("Enter the shadNet account name (up to 16 characters)");
|
|
if (Env("BB_SHADNET_PASSWORD", "").empty()) throw Error("Sign in with the account password in the launcher");
|
|
const std::string p2p = P2pPort(settings);
|
|
const Url api = Parse(game_api);
|
|
nlohmann::ordered_json overrides;
|
|
overrides[kGameStatusOrigin] = game_api;
|
|
for (const char* h : kPlayLogHosts) overrides[h] = api.scheme + "://" + api.host + ":" + std::to_string(kGameApiPort);
|
|
const fs::path destination = WriteOverrides(data, overrides);
|
|
SetEnv("BB_ONLINE", "1");
|
|
SetEnv("BB_SHADNET_SERVER", host + ":" + std::to_string(port));
|
|
SetEnv("BB_SHADNET_WEBAPI", webapi);
|
|
SetEnv("BB_SHADNET_NPID", username);
|
|
SetEnv("BB_UPNP", get("online_upnp") == "0" ? "0" : "1");
|
|
SetEnv("BB_P2P_PORT", p2p);
|
|
SetEnv("SHADPS4_HTTP_HOST_OVERRIDES_JSON", Utf8(destination));
|
|
SetSignTimeout(settings);
|
|
return true;
|
|
}
|
|
|
|
} // namespace start
|