mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 19:47:08 +02:00
Crash dumps now go to the project's own Sentry server. The launcher's out-of-process helper is unchanged; only where it posts and what it expects back are: - The default endpoint is the Sentry project's minidump URL. Report attributes travel as Sentry tags, with the release as reskate@<version>. - An upload counts as accepted when the server answers with an event ID. - "backtrace" is "crash_report" throughout: the namespace, files, CMake targets, DINGOSDK_CRASH_REPORT_URL, DINGOSDK_BUILD_CRASH_REPORT_TESTS and the RESKATE_CRASH_REPORT_URL override. Performance timings are new, and off unless "Send performance timings" is ticked in the launcher's settings (RESKATE_TRACING=1). A small tracer (Engine/Core/Debug/trace.h) posts each finished step to Sentry as a transaction from a background thread; the launcher and the game it starts share one trace. Timed: the update check, the Steam download, Thunderstore installs, the mod merge, starting the game, the launch up to the first playable level, each level load, joining a session, fetching the identity lists, and the time spent in each level with its average frame rate and long frames. Turning crash reports off turns timings off too.
520 lines
23 KiB
C++
520 lines
23 KiB
C++
#include "gui_internal.h"
|
|
|
|
#include "depot_output.h"
|
|
|
|
#include "Engine/Core/Json/json.h"
|
|
#include "Engine/Core/Log/logging.h"
|
|
#include "Engine/Core/Debug/trace.h"
|
|
#include "Engine/Vfs/mod_catalog.h"
|
|
|
|
#include <shellapi.h>
|
|
|
|
#include <format>
|
|
#include <fstream>
|
|
#include <stdexcept>
|
|
|
|
namespace dingosdk::launcher_gui::detail {
|
|
namespace {
|
|
|
|
std::wstring widen(std::string_view value) {
|
|
if (value.empty()) return {};
|
|
const auto length = MultiByteToWideChar(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), nullptr, 0);
|
|
std::wstring output(static_cast<std::size_t>(std::max(length, 0)), L'\0');
|
|
MultiByteToWideChar(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), output.data(), length);
|
|
return output;
|
|
}
|
|
|
|
std::string format_bytes(std::uint64_t bytes) {
|
|
if (bytes >= 1024ull * 1024 * 1024) return std::format("{:.1f} GB", static_cast<double>(bytes) / (1024.0 * 1024 * 1024));
|
|
return std::format("{:.1f} MB", static_cast<double>(bytes) / (1024.0 * 1024));
|
|
}
|
|
|
|
fs::path settings_path(const launcher_app::Session& session) {
|
|
return session.self.parent_path() / L"ReSkateLauncher.settings.json";
|
|
}
|
|
|
|
Settings load_settings(const fs::path& path) {
|
|
Settings settings;
|
|
try {
|
|
std::ifstream input(path, std::ios::binary);
|
|
if (!input) return settings;
|
|
const std::string text{std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
|
|
const auto json = Json::parse(text);
|
|
settings.windowed = json.value("windowed", settings.windowed);
|
|
settings.width = std::clamp(json.value("width", settings.width), 320, 16384);
|
|
settings.height = std::clamp(json.value("height", settings.height), 200, 16384);
|
|
settings.loose_files = json.value("loose_files", settings.loose_files);
|
|
settings.gpu_diagnostics = json.value("gpu_diagnostics", settings.gpu_diagnostics);
|
|
settings.discord_status = json.value("discord_status", settings.discord_status);
|
|
settings.offline = json.value("offline", settings.offline);
|
|
settings.menu_key = json.value("menu_key", settings.menu_key);
|
|
settings.console_key = json.value("console_key", settings.console_key);
|
|
if (!launcher::bindable_key(static_cast<unsigned>(settings.menu_key)) ||
|
|
!launcher::bindable_key(static_cast<unsigned>(settings.console_key)) ||
|
|
settings.menu_key == settings.console_key) {
|
|
settings.menu_key = static_cast<int>(launcher::default_menu_key);
|
|
settings.console_key = static_cast<int>(launcher::default_console_key);
|
|
}
|
|
const auto level = json.value("log_level", std::string("info"));
|
|
for (std::size_t index = 0; index < log_levels.size(); ++index)
|
|
if (level == log_levels[index]) settings.log_level = static_cast<int>(index);
|
|
settings.arguments = json.value("arguments", std::string());
|
|
// Keep the original settings key so existing launcher preferences survive.
|
|
settings.keep_open_after_launch = !json.value("close_on_launch", !settings.keep_open_after_launch);
|
|
settings.updates = json.value("updates", settings.updates);
|
|
settings.crash_reports = json.value("crash_reports", settings.crash_reports);
|
|
settings.performance_timings = json.value("performance_timings", settings.performance_timings);
|
|
settings.steam_username = json.value("steam_username", std::string());
|
|
settings.steam_remember = json.value("steam_remember", settings.steam_remember);
|
|
settings.steam_prefer_code = json.value("steam_prefer_code", settings.steam_prefer_code);
|
|
} catch (const std::exception& exception) {
|
|
logging::log(logging::Level::warning, logging::Channel::launcher, "Ignoring launcher settings: {}", exception.what());
|
|
}
|
|
return settings;
|
|
}
|
|
|
|
void save_settings(const fs::path& path, const Settings& settings) {
|
|
auto json = Json::object();
|
|
json["windowed"] = settings.windowed;
|
|
json["width"] = settings.width;
|
|
json["height"] = settings.height;
|
|
json["loose_files"] = settings.loose_files;
|
|
json["gpu_diagnostics"] = settings.gpu_diagnostics;
|
|
json["discord_status"] = settings.discord_status;
|
|
json["offline"] = settings.offline;
|
|
json["menu_key"] = settings.menu_key;
|
|
json["console_key"] = settings.console_key;
|
|
json["log_level"] = log_levels[static_cast<std::size_t>(settings.log_level)];
|
|
json["arguments"] = settings.arguments;
|
|
json["close_on_launch"] = !settings.keep_open_after_launch;
|
|
json["updates"] = settings.updates;
|
|
json["crash_reports"] = settings.crash_reports;
|
|
json["performance_timings"] = settings.performance_timings;
|
|
json["steam_username"] = settings.steam_username;
|
|
json["steam_remember"] = settings.steam_remember;
|
|
json["steam_prefer_code"] = settings.steam_prefer_code;
|
|
std::ofstream output(path, std::ios::binary | std::ios::trunc);
|
|
output << json.dump(2) << '\n';
|
|
}
|
|
|
|
// Turns saved settings into the same flags the command line accepts, so both
|
|
// paths share parse_launch_options' validation.
|
|
launcher::LaunchOptions launch_options(const Settings& settings) {
|
|
std::vector<std::wstring> arguments;
|
|
if (settings.windowed) {
|
|
arguments.push_back(std::format(L"--width={}", settings.width));
|
|
arguments.push_back(std::format(L"--height={}", settings.height));
|
|
}
|
|
if (!settings.loose_files) arguments.emplace_back(L"--no-loose-files");
|
|
if (settings.gpu_diagnostics) arguments.emplace_back(L"--gpu-diagnostics");
|
|
if (!settings.discord_status) arguments.emplace_back(L"--no-discord");
|
|
if (settings.offline) arguments.emplace_back(L"-offline");
|
|
arguments.push_back(std::format(L"--menu-key={}", settings.menu_key));
|
|
arguments.push_back(std::format(L"--console-key={}", settings.console_key));
|
|
arguments.push_back(L"--log-level=" + widen(log_levels[static_cast<std::size_t>(settings.log_level)]));
|
|
if (settings.arguments.find_first_not_of(" \t") != std::string::npos) {
|
|
int count{};
|
|
const auto line = L"skate " + widen(settings.arguments);
|
|
if (auto** split = CommandLineToArgvW(line.c_str(), &count)) {
|
|
for (int index = 1; index < count; ++index) arguments.emplace_back(split[index]);
|
|
LocalFree(split);
|
|
}
|
|
}
|
|
return launcher::parse_launch_options(arguments);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Launcher::Launcher(const launcher_app::Session& session, std::vector<std::wstring> arguments)
|
|
: session_(session), arguments_(std::move(arguments)), settings_(load_settings(settings_path(session))) {
|
|
relaunched_ = std::find(arguments_.begin(), arguments_.end(), L"--reskate-updated") != arguments_.end();
|
|
binaries_ = std::find(arguments_.begin(), arguments_.end(), L"--no-update") == arguments_.end();
|
|
}
|
|
|
|
Launcher::~Launcher() {
|
|
cancel();
|
|
if (worker_.joinable()) worker_.join();
|
|
}
|
|
|
|
State Launcher::snapshot() {
|
|
std::lock_guard lock(mutex_);
|
|
return state_;
|
|
}
|
|
|
|
void Launcher::game_exited(bool seen) {
|
|
game_ = 0;
|
|
launched_ = false;
|
|
if (busy_ || snapshot().phase == Phase::failed) return;
|
|
if (seen) check();
|
|
else fail("Skate closed while it was starting.");
|
|
}
|
|
|
|
void Launcher::save() {
|
|
save_settings(settings_path(session_), settings_);
|
|
// The game inherits the launcher's environment, so this reaches the next launch; the
|
|
// launcher's own crash reporting follows the setting from its next start.
|
|
SetEnvironmentVariableW(L"RESKATE_CRASH_REPORTING", settings_.crash_reports ? nullptr : L"0");
|
|
SetEnvironmentVariableW(L"RESKATE_TRACING", settings_.performance_timings ? L"1" : L"0");
|
|
}
|
|
|
|
template<class Task> void Launcher::start(Task task) {
|
|
if (busy_) return;
|
|
if (worker_.joinable()) worker_.join();
|
|
busy_ = true;
|
|
cancel_ = false;
|
|
worker_ = std::thread([this, task] {
|
|
try { task(); }
|
|
catch (const std::exception& exception) { fail(exception.what()); }
|
|
busy_ = false;
|
|
});
|
|
}
|
|
|
|
void Launcher::check() { start([this] { run_check(); }); }
|
|
void Launcher::apply_updates() { start([this] { run_updates(); }); }
|
|
|
|
void Launcher::download(bool validate, bool qr, std::string password) {
|
|
{
|
|
std::lock_guard lock(mutex_);
|
|
wipe(password_);
|
|
password_ = std::move(password);
|
|
qr_login_ = qr;
|
|
}
|
|
start([this, validate] { run_download(validate); });
|
|
}
|
|
|
|
void Launcher::play() { start([this] { run_play(); }); }
|
|
|
|
void Launcher::cancel() {
|
|
cancel_ = true;
|
|
answered_.notify_all();
|
|
}
|
|
|
|
void Launcher::answer(std::optional<std::string> value) {
|
|
std::lock_guard lock(mutex_);
|
|
if (answer_) wipe(*answer_);
|
|
answer_ = std::move(value);
|
|
if (!answer_) cancel_ = true;
|
|
has_answer_ = true;
|
|
answered_.notify_all();
|
|
}
|
|
|
|
void Launcher::restart() {
|
|
auto arguments = arguments_;
|
|
if (std::find(arguments.begin(), arguments.end(), L"--reskate-updated") == arguments.end())
|
|
arguments.emplace_back(L"--reskate-updated");
|
|
launcher_app::relaunch(session_.self, arguments);
|
|
}
|
|
|
|
void Launcher::wipe(std::string& value) {
|
|
SecureZeroMemory(value.data(), value.size());
|
|
value.clear();
|
|
}
|
|
|
|
std::optional<std::string> Launcher::prompt(const update::Prompt& prompt) {
|
|
std::unique_lock lock(mutex_);
|
|
// Use the password from the sign-in panel once; ask again only if Steam re-asks.
|
|
if (prompt.kind == update::PromptKind::password && !password_.empty()) {
|
|
auto password = std::move(password_);
|
|
password_.clear();
|
|
return password;
|
|
}
|
|
state_.prompt = prompt;
|
|
has_answer_ = false;
|
|
answered_.wait(lock, [this] { return has_answer_ || cancel_; });
|
|
state_.prompt.reset();
|
|
if (cancel_) return std::nullopt;
|
|
auto value = std::move(answer_);
|
|
answer_.reset();
|
|
return value;
|
|
}
|
|
|
|
fs::path Launcher::mods_data_root() const {
|
|
// The game resolves -dataPath against its own folder (engine_data_root);
|
|
// the same argument can be typed into Settings, so follow it here too.
|
|
const auto& extra = settings_.arguments;
|
|
const auto found = extra.find("-dataPath");
|
|
if (found == std::string::npos) return session_.paths.directory;
|
|
auto rest = std::string_view(extra).substr(found + 9);
|
|
if (!rest.empty() && (rest.front() == '=' || rest.front() == ' ')) rest.remove_prefix(1);
|
|
while (!rest.empty() && rest.front() == ' ') rest.remove_prefix(1);
|
|
const auto end = rest.find_first_of(" ");
|
|
const auto value = rest.substr(0, end);
|
|
if (value.empty()) return session_.paths.directory;
|
|
std::error_code error;
|
|
const auto root = fs::weakly_canonical(session_.paths.directory / wide(value), error);
|
|
return error ? session_.paths.directory / wide(value) : root;
|
|
}
|
|
|
|
void Launcher::play_anyway() {
|
|
ignore_mod_problems_ = true;
|
|
play();
|
|
}
|
|
|
|
void Launcher::dismiss_mod_problems() {
|
|
std::lock_guard lock(mutex_);
|
|
if (state_.phase != Phase::mods_broken) return;
|
|
state_.phase = Phase::ready;
|
|
state_.status = "Ready to skate";
|
|
state_.detail = "Some mods are switched off or will not load.";
|
|
state_.mod_problems.clear();
|
|
}
|
|
|
|
void Launcher::set(Phase phase, std::string status, std::string detail, float progress) {
|
|
std::lock_guard lock(mutex_);
|
|
state_.phase = phase;
|
|
state_.status = std::move(status);
|
|
state_.detail = std::move(detail);
|
|
state_.progress = progress;
|
|
}
|
|
|
|
void Launcher::set_progress(std::string detail, float progress) {
|
|
std::lock_guard lock(mutex_);
|
|
state_.detail = std::move(detail);
|
|
state_.progress = progress;
|
|
}
|
|
|
|
void Launcher::fail(const std::string& message) {
|
|
logging::write(logging::Level::error, logging::Channel::launcher, message);
|
|
std::lock_guard lock(mutex_);
|
|
state_.phase = Phase::failed;
|
|
state_.status = message; // raw: the STATUS tile explains it and can copy it
|
|
state_.detail.clear();
|
|
state_.progress = -1;
|
|
state_.qr.clear();
|
|
}
|
|
|
|
std::optional<update::Config> Launcher::config() {
|
|
std::lock_guard lock(mutex_);
|
|
return state_.config;
|
|
}
|
|
|
|
update::Progress Launcher::progress_for(std::string label) {
|
|
return [this, label](std::uint64_t received, std::uint64_t total) {
|
|
set_progress(std::format("{} {} of {}", label, format_bytes(received), format_bytes(total)),
|
|
total ? static_cast<float>(static_cast<double>(received) / static_cast<double>(total)) : -1.0f);
|
|
};
|
|
}
|
|
|
|
bool Launcher::launcher_outdated(const update::Config& config) const {
|
|
return !relaunched_ && !config.launcher.url.empty() && !update::file_matches(session_.self, config.launcher);
|
|
}
|
|
|
|
bool Launcher::runtime_outdated(const update::Config& config) const {
|
|
return !config.runtime.url.empty() && !update::file_matches(session_.paths.dll, config.runtime);
|
|
}
|
|
|
|
void Launcher::run_check() {
|
|
set(Phase::checking, "Checking for updates");
|
|
update::remove_previous_launcher(session_.self);
|
|
auto config = update::fetch_config();
|
|
{
|
|
std::lock_guard lock(mutex_);
|
|
state_.config = config;
|
|
}
|
|
const bool newer = config && (launcher_outdated(*config) || runtime_outdated(*config));
|
|
if (newer && update::binary_updates_enabled() && binaries_ && settings_.updates) {
|
|
set(Phase::update_available, "A ReSkate update is ready",
|
|
std::format("Version {} will be downloaded and installed.",
|
|
config->runtime.version.empty() ? config->launcher.version : config->runtime.version));
|
|
return;
|
|
}
|
|
set(Phase::checking, "Checking game files");
|
|
if (!launcher_app::game_files_supported(session_.paths)) {
|
|
std::error_code error;
|
|
const bool installed = fs::is_regular_file(session_.paths.game, error);
|
|
if (!config) {
|
|
fail(installed ? "This Skate version is not supported, and the update server could not be reached."
|
|
: "Skate is not installed here, and the update server could not be reached.");
|
|
return;
|
|
}
|
|
if (!launcher_app::config_matches_build(*config)) {
|
|
fail("This launcher is out of date. Download the latest ReSkate release.");
|
|
return;
|
|
}
|
|
if (installed) set(Phase::game_outdated, "Steam updated Skate",
|
|
"ReSkate needs the build below. Only the files that differ are downloaded.");
|
|
else set(Phase::game_missing, "Skate is not installed here",
|
|
"Download the build below from Steam, about 14 GB, with an account that owns skate.");
|
|
return;
|
|
}
|
|
std::error_code error;
|
|
if (!fs::is_regular_file(session_.paths.dll, error)) {
|
|
fail("ReSkate.dll is missing from the game folder.");
|
|
return;
|
|
}
|
|
set(Phase::ready, "Ready to skate", !config ? "Offline: update check skipped."
|
|
: !settings_.updates ? (newer ? "ReSkate updates are off; keeping your ReSkate files." : "ReSkate updates are off.")
|
|
: "Up to date.");
|
|
}
|
|
|
|
void Launcher::run_updates() {
|
|
const auto config = this->config();
|
|
if (!config) return run_check();
|
|
set(Phase::updating, "Updating ReSkate");
|
|
if (runtime_outdated(*config))
|
|
update::replace_file(session_.paths.dll, config->runtime, progress_for("ReSkate.dll"));
|
|
if (launcher_outdated(*config)) {
|
|
update::replace_running_launcher(session_.self, config->launcher, progress_for("Launcher"));
|
|
logging::flush();
|
|
restart_ = true;
|
|
return;
|
|
}
|
|
run_check();
|
|
}
|
|
|
|
void Launcher::run_download(bool validate) {
|
|
const auto config = this->config();
|
|
if (!config) return run_check();
|
|
trace::Scope timing("launcher.game_download", "task");
|
|
timing.tag("mode", validate ? "verify" : "download");
|
|
set(Phase::downloading, "Preparing Steam download", {}, 0);
|
|
const auto depot_downloader = update::ensure_depot_downloader(config->depot_downloader,
|
|
progress_for("DepotDownloader"));
|
|
set(Phase::downloading, "Connecting to Steam");
|
|
DepotOutput output;
|
|
update::SteamLogin login;
|
|
login.username = settings_.steam_username;
|
|
login.remember = settings_.steam_remember;
|
|
login.prefer_code = settings_.steam_prefer_code;
|
|
{
|
|
std::lock_guard lock(mutex_);
|
|
if (qr_login_) login.username.clear();
|
|
}
|
|
if (!login.username.empty()) set(Phase::downloading, "Signing in to Steam", login.username);
|
|
const auto code = update::run_depot_downloader(depot_downloader, config->game,
|
|
session_.paths.directory, validate, login, [&](std::string_view line) {
|
|
// DepotDownloader names the account it signs in ("Logging 'name' into Steam3"); the log
|
|
// travels with crash reports, so the name is left out.
|
|
std::string logged(line);
|
|
if (!login.username.empty())
|
|
for (auto at = logged.find(login.username); at != std::string::npos; at = logged.find(login.username, at + 8))
|
|
logged.replace(at, login.username.size(), "<hidden>");
|
|
logging::write(logging::Level::debug, logging::Channel::launcher, logged);
|
|
output.feed(line);
|
|
std::lock_guard lock(mutex_);
|
|
if (line.find("Use the Steam Mobile App to confirm") != std::string_view::npos) {
|
|
state_.status = "Approve the sign-in in the Steam app";
|
|
state_.detail = "Open Steam on your phone and confirm. Waiting...";
|
|
return;
|
|
}
|
|
if (output.qr_update) {
|
|
state_.qr = *output.qr_update;
|
|
if (state_.qr.empty()) state_.status = "Signed in to Steam";
|
|
}
|
|
if (output.percent) {
|
|
state_.status = validate ? "Verifying Skate" : "Downloading Skate";
|
|
state_.detail = std::format("{:.1f}% {}", *output.percent, output.file);
|
|
state_.progress = *output.percent / 100.0f;
|
|
} else if (state_.qr.empty() && !output.message.empty() && output.message != state_.detail) {
|
|
state_.detail = output.message;
|
|
}
|
|
}, [this](const update::Prompt& prompt) { return this->prompt(prompt); }, cancel_);
|
|
{
|
|
std::lock_guard lock(mutex_);
|
|
state_.qr.clear();
|
|
state_.prompt.reset();
|
|
wipe(password_);
|
|
}
|
|
if (cancel_) {
|
|
timing.tag("cancelled", "true");
|
|
run_check();
|
|
std::lock_guard lock(mutex_);
|
|
state_.detail = "Download cancelled. It will resume where it stopped.";
|
|
return;
|
|
}
|
|
if (launcher_app::game_files_supported(session_.paths)) {
|
|
set(Phase::ready, "Ready to skate", "Skate build installed.");
|
|
return;
|
|
}
|
|
timing.fail();
|
|
fail(std::format("The Steam download did not finish (exit code {}). {}", code, output.message));
|
|
}
|
|
|
|
// The merge the game does behind its splash, done here instead, so a mod that
|
|
// cannot be merged is a question the launcher can ask rather than a surprise
|
|
// three minutes into a loading screen. The game reuses what this builds.
|
|
bool Launcher::run_mod_merge() {
|
|
const auto root = mods_data_root();
|
|
std::error_code error;
|
|
if (!fs::is_directory(root / mods::mods_folder, error)) return true;
|
|
set(Phase::merging, "Preparing mods", "Merging what is installed", 0);
|
|
trace::Scope timing("launcher.mod_merge", "task");
|
|
auto catalog = mods::load_catalog(root, [this](const mods::MergeProgress& progress) {
|
|
set_progress(progress.step, progress.total
|
|
? static_cast<float>(progress.done) / static_cast<float>(progress.total) : -1.0f);
|
|
});
|
|
timing.tag("mods", std::to_string(catalog.mods.size()));
|
|
timing.tag("mods.excluded", std::to_string(catalog.excluded.size()));
|
|
if (!catalog.issue.empty()) timing.fail();
|
|
for (const auto& note : catalog.notes)
|
|
logging::write(logging::Level::info, logging::Channel::launcher, note);
|
|
for (const auto& warning : catalog.warnings)
|
|
logging::write(logging::Level::warning, logging::Channel::launcher, warning);
|
|
|
|
std::vector<ModProblem> problems;
|
|
if (!catalog.issue.empty())
|
|
problems.push_back({{}, "The Mods folder", catalog.issue});
|
|
for (const auto& mod : catalog.excluded) {
|
|
// Mods built for another game version say so for themselves, in game
|
|
// and in the mod manager; they do not hold up a launch.
|
|
if (!mod.outdated.empty()) continue;
|
|
problems.push_back({mod.name, mod.title.empty() ? mod.name : mod.title,
|
|
mod.problems.empty() ? std::string("it could not be merged cleanly") : mod.problems.front()});
|
|
}
|
|
if (problems.empty()) return true;
|
|
std::lock_guard lock(mutex_);
|
|
state_.phase = Phase::mods_broken;
|
|
state_.status = problems.size() == 1 ? "A mod could not be merged"
|
|
: std::format("{} mods could not be merged", problems.size());
|
|
state_.detail.clear();
|
|
state_.progress = -1;
|
|
state_.mod_problems = std::move(problems);
|
|
return false;
|
|
}
|
|
|
|
void Launcher::run_play() {
|
|
if (!ignore_mod_problems_ && !run_mod_merge()) return;
|
|
ignore_mod_problems_ = false;
|
|
set(Phase::launching, "Starting Skate");
|
|
save();
|
|
auto options = launch_options(settings_);
|
|
// The launcher's own command line can request offline mode too.
|
|
if (std::find(arguments_.begin(), arguments_.end(), L"-offline") != arguments_.end() ||
|
|
std::find(arguments_.begin(), arguments_.end(), L"--offline") != arguments_.end())
|
|
options.offline = true;
|
|
// And a map to start on, over one in the saved launch options.
|
|
if (auto arguments = arguments_; true)
|
|
if (auto map = launcher::take_map_option(arguments); !map.empty()) options.map = std::move(map);
|
|
launched_ = false;
|
|
launcher_app::start_game(session_, options, [this](DWORD id) { game_ = id; });
|
|
launched_ = true;
|
|
set(Phase::launching, "Skate is starting", options.offline || !launcher_app::steam_signed_in() ?
|
|
"Offline mode: playing without Steam as Unknown Player; multiplayer is hidden." :
|
|
settings_.keep_open_after_launch ? "The launcher hides while you play and returns when Skate closes." :
|
|
"The launcher closes now; Skate keeps starting on its own.");
|
|
}
|
|
|
|
} // namespace dingosdk::launcher_gui::detail
|
|
|
|
namespace dingosdk::launcher_gui {
|
|
bool updates_enabled(const launcher_app::Session& session) {
|
|
return detail::load_settings(detail::settings_path(session)).updates;
|
|
}
|
|
launcher::LaunchOptions saved_launch_options(const launcher_app::Session& session) {
|
|
return detail::launch_options(detail::load_settings(detail::settings_path(session)));
|
|
}
|
|
void apply_crash_report_setting() noexcept {
|
|
try {
|
|
std::wstring path(32768, L'\0');
|
|
const auto length = GetModuleFileNameW(nullptr, path.data(), static_cast<DWORD>(path.size()));
|
|
if (!length || length >= path.size()) return;
|
|
path.resize(length);
|
|
const auto settings = detail::load_settings(std::filesystem::path(path).parent_path() / L"ReSkateLauncher.settings.json");
|
|
if (!settings.crash_reports) SetEnvironmentVariableW(L"RESKATE_CRASH_REPORTING", L"0");
|
|
// A RESKATE_TRACING already set (to test with, or to refuse) is left alone.
|
|
if (settings.performance_timings && !GetEnvironmentVariableW(L"RESKATE_TRACING", nullptr, 0))
|
|
SetEnvironmentVariableW(L"RESKATE_TRACING", L"1");
|
|
} catch (...) {}
|
|
}
|
|
} // namespace dingosdk::launcher_gui
|