Files
ReSkate/Launcher/main.cpp
T
AyeZeeBB 375605b9fe Launcher: ReSkate lives in a folder of its own, not Steam's skate. folder
Launcher: ReSkate lives in a folder of its own, not Steam's skate. folder
- The launcher refuses to start from a Steam library folder
  (...\steamapps\common\<game>): it shows an error saying to move
  ReSkateLauncher.exe and ReSkate.dll to a new, empty folder, then closes.
  Checked before anything is read or written there. Not under Wine: on
  Proton, Steam starts the launcher from the game's folder.
- The "not in your skate. folder" error no longer tells players to put the
  files beside Skate.exe in Steam's folder; it now says to keep them together
  in a folder of their own.
- README: Getting started says to extract into a new, empty folder, where the
  launcher installs its own copy of the game, so a Steam update cannot break
  it. The Proton section says why it is the one exception.
2026-10-13 15:00:19 -05:00

163 lines
7.7 KiB
C++

#include "Engine/Core/Log/logging.h"
#include "Engine/Core/Debug/trace.h"
#include "crash_reporter.h"
#include "gui.h"
#include "launch.h"
#include <Windows.h>
#include <shellapi.h>
#include <algorithm>
#include <filesystem>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
namespace fs = std::filesystem;
namespace {
namespace app = dingosdk::launcher_app;
namespace update = dingosdk::launcher_update;
using dingosdk::logging::Channel;
using dingosdk::logging::Level;
std::wstring widen(std::string_view value) {
if (value.empty()) return {};
auto length = MultiByteToWideChar(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), nullptr, 0);
if (length <= 0) return L"Unknown error";
std::wstring output(static_cast<std::size_t>(length), L'\0');
MultiByteToWideChar(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), output.data(), length);
return output;
}
// Removes a launcher-only flag so it never reaches Skate.exe.
bool take_flag(std::vector<std::wstring>& arguments, std::wstring_view flag) {
const auto found = std::find(arguments.begin(), arguments.end(), flag);
if (found == arguments.end()) return false;
arguments.erase(found);
return true;
}
// Scripted launches (`--no-gui`): update, validate and start Skate without a window.
// Returns true when the launcher replaced itself and restarted.
bool apply_headless_updates(const app::Session& session, const update::Config& config,
const std::vector<std::wstring>& original, bool relaunched) {
if (!update::binary_updates_enabled()) return false;
if (!config.runtime.url.empty() && !update::file_matches(session.paths.dll, config.runtime)) {
try {
update::replace_file(session.paths.dll, config.runtime);
dingosdk::logging::log(Level::info, Channel::launcher, "ReSkate.dll updated to {}.", config.runtime.version);
} catch (const std::exception& exception) {
dingosdk::logging::log(Level::warning, Channel::launcher, "ReSkate.dll update failed: {}", exception.what());
}
}
if (relaunched || config.launcher.url.empty() || update::file_matches(session.self, config.launcher)) return false;
try {
update::replace_running_launcher(session.self, config.launcher);
dingosdk::logging::log(Level::info, Channel::launcher, "Launcher updated to {}; restarting.", config.launcher.version);
auto arguments = original;
arguments.emplace_back(L"--reskate-updated");
app::relaunch(session.self, arguments);
return true;
} catch (const std::exception& exception) {
dingosdk::logging::log(Level::warning, Channel::launcher, "Launcher update failed: {}", exception.what());
return false;
}
}
void run_headless(const app::Session& session, std::vector<std::wstring> arguments,
const std::vector<std::wstring>& original) {
const bool relaunched = take_flag(arguments, L"--reskate-updated");
const bool updates = !take_flag(arguments, L"--no-update") && dingosdk::launcher_gui::updates_enabled(session);
// With no options of its own, the launch is the one the launcher window's Play would make.
// A map to start on is not one of those: "--no-gui -map <name>" is that launch, on that map.
auto map = dingosdk::launcher::take_map_option(arguments);
auto options = arguments.empty() ? dingosdk::launcher_gui::saved_launch_options(session)
: dingosdk::launcher::parse_launch_options(arguments);
if (!map.empty()) options.map = std::move(map);
update::remove_previous_launcher(session.self);
if (updates) {
const auto config = update::fetch_config();
if (config && apply_headless_updates(session, *config, original, relaunched)) return;
}
if (!app::game_files_supported(session.paths))
throw std::runtime_error("Skate.exe is missing or is not the supported build. "
"Open ReSkateLauncher.exe without --no-gui to download it from Steam.");
app::start_game(session, options);
}
// Whether the launcher was put in a game's folder of a Steam library (...\steamapps\common\<game>),
// which is where Steam keeps skate. and updates it: ReSkate has a folder of its own, with its
// own copy of the game. Not under Wine: on Proton, Steam starts the launcher from the game's
// folder in the game's place, and that is how it is set up there (README, "Linux (Proton)").
bool in_steam_library() {
const auto ntdll = GetModuleHandleW(L"ntdll.dll");
if (ntdll && GetProcAddress(ntdll, "wine_get_version")) return false;
std::wstring self(32768, L' ');
self.resize(GetModuleFileNameW(nullptr, self.data(), static_cast<DWORD>(self.size())));
if (self.empty()) return false;
const auto same = [](const fs::path& part, const wchar_t* name) { return _wcsicmp(part.c_str(), name) == 0; };
for (auto folder = fs::path(self).parent_path(); folder.has_parent_path() && folder != folder.parent_path(); folder = folder.parent_path())
if (same(folder.filename(), L"common") && same(folder.parent_path().filename(), L"steamapps")) return true;
return false;
}
} // namespace
int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) {
int argc{};
auto** argv = CommandLineToArgvW(GetCommandLineW(), &argc);
if (!argv) {
MessageBoxW(nullptr, L"Could not read the launcher command line.",
L"ReSkate Launcher", MB_OK | MB_ICONERROR | MB_SETFOREGROUND);
return 1;
}
if (argc > 1 && std::wstring_view(argv[1]) == L"--reskate-crash-helper") {
const auto result = dingosdk::crash_report::run_reporter(argc, argv);
LocalFree(argv);
return result;
}
const std::vector<std::wstring> original(argv + 1, argv + argc);
LocalFree(argv);
// (Before anything is read or written there: the only thing to do is to close.)
if (in_steam_library()) {
MessageBoxW(nullptr,
L"ReSkate cannot run from Steam's skate. folder.\n\n"
L"Move ReSkateLauncher.exe and ReSkate.dll to a new, empty folder of their own (for example C:\\Games\\ReSkate) "
L"and run the launcher from there. It installs its own copy of the game into that folder, so a Steam update "
L"cannot break it.\n\nThe launcher will now close.",
L"ReSkate Launcher", MB_OK | MB_ICONERROR | MB_SETFOREGROUND);
return 1;
}
auto arguments = original;
const bool headless = take_flag(arguments, L"--no-gui");
int result = 0;
fs::path logs;
try {
std::string log_level = "info";
if (headless) log_level = dingosdk::launcher::parse_launch_options(
[&] { auto copy = arguments; take_flag(copy, L"--no-update"); take_flag(copy, L"--reskate-updated"); return copy; }()).log_level;
dingosdk::launcher_gui::apply_crash_report_setting();
const auto session = app::open_session(log_level);
logs = session.paths.logs;
if (headless) run_headless(session, arguments, original);
else if (app::relaunch_without_mangohud(session.self, original)) dingosdk::logging::flush();
else result = dingosdk::launcher_gui::run(session, arguments);
} catch (const std::exception& exception) {
const auto message = widen(exception.what());
dingosdk::logging::write(Level::critical, Channel::launcher, message);
dingosdk::logging::flush();
std::wstring dialog = L"Could not start Skate.\n\n" + message;
if (!logs.empty()) dialog += L"\n\nDetails: " + (logs / L"ReSkate.log").wstring();
MessageBoxW(nullptr, dialog.c_str(), L"ReSkate Launcher", MB_OK | MB_ICONERROR | MB_SETFOREGROUND);
result = 1;
}
dingosdk::trace::flush(std::chrono::seconds(3));
dingosdk::logging::shutdown();
return result;
}