mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 17:27:19 +02:00
Launcher: a setting to test pre-releases
Settings > Launcher > "Test pre-releases" (off by default). On, the update check takes the newest GitHub release that is not a draft and carries a launcher.json, pre-release or not, in place of the latest full release: a pre-release newer than the last release is installed, pinned by size and SHA-256 as any release is. Turned off, the next check goes back to the latest release, whose files then replace the pre-release's. - The update notice says "Pre-release" for one, and the log names which kind of release was taken. - The changelog shows the release the setting picks, marked as a pre-release, and is fetched again when the setting changes. - --no-gui launches follow the same setting.
This commit is contained in:
1 parent
94a616feb9
commit
310922556c
9 files changed
+77
-25
No files matched your search
@@ -15,6 +15,8 @@ int run(const launcher_app::Session& session, const std::vector<std::wstring>& a
|
||||
|
||||
// The Settings page's "Install ReSkate updates" choice; scripted (--no-gui) launches honour it too.
|
||||
bool updates_enabled(const launcher_app::Session& session);
|
||||
// Whether the saved settings ask for pre-releases (the "Test pre-releases" setting).
|
||||
bool prereleases_enabled(const launcher_app::Session& session);
|
||||
|
||||
// What pressing Play would start the game with: the Settings page's saved choices. A scripted
|
||||
// launch that names none of its own (a bare --no-gui, as Discord runs for a join) uses these.
|
||||
|
||||
@@ -10,11 +10,12 @@ void start_fetch(Changelog& log) {
|
||||
if (log.loading.exchange(true)) return;
|
||||
if (log.worker.joinable()) log.worker.join();
|
||||
log.error.clear();
|
||||
log.worker = std::thread([&log] {
|
||||
log.fetched_prereleases = log.prereleases;
|
||||
log.worker = std::thread([&log, prereleases = log.prereleases] {
|
||||
update::ReleaseNote notes;
|
||||
std::string error;
|
||||
try {
|
||||
notes = update::fetch_release_note();
|
||||
notes = update::fetch_release_note(prereleases);
|
||||
} catch (const std::exception& failure) {
|
||||
error = failure.what();
|
||||
logging::write(logging::Level::warning, logging::Channel::launcher,
|
||||
@@ -45,6 +46,8 @@ void changelog_window(const Fonts& fonts, ImVec2 size, Ui& ui) {
|
||||
log.incoming = {};
|
||||
}
|
||||
}
|
||||
// (The pre-release setting changed since: the notes are fetched again for it.)
|
||||
if (log.loaded && !log.loading && log.fetched_prereleases != log.prereleases) log.loaded = false;
|
||||
if (!log.loaded && !log.loading && log.error.empty()) start_fetch(log);
|
||||
|
||||
const auto panel = begin_panel("##changelog_panel", size, ImVec2(S(880), S(680)));
|
||||
@@ -64,7 +67,7 @@ void changelog_window(const Fonts& fonts, ImVec2 size, Ui& ui) {
|
||||
ImGui::PushFont(fonts.heading);
|
||||
ImGui::TextUnformatted(label.c_str());
|
||||
ImGui::PopFont();
|
||||
if (!note.date.empty()) ImGui::TextDisabled("Released %s", note.date.c_str());
|
||||
if (!note.date.empty()) ImGui::TextDisabled("%s %s", note.prerelease ? "Pre-release," : "Released", note.date.c_str());
|
||||
ImGui::Dummy(ImVec2(0, S(6)));
|
||||
if (log.text.lines.empty()) ImGui::TextDisabled("This release has no patch notes.");
|
||||
else draw_readme(fonts, log.text);
|
||||
|
||||
@@ -531,7 +531,10 @@ void frame(Launcher& launcher, const Fonts& fonts, HWND window, Ui& ui, ModsPane
|
||||
else if (qr_open) qr_window(launcher, fonts, size, state.qr);
|
||||
else if (ui.sign_in) sign_in_window(launcher, fonts, size, ui);
|
||||
else if (ui.settings) settings_window(launcher, fonts, size, ui, window);
|
||||
else if (ui.changelog) changelog_window(fonts, size, ui);
|
||||
else if (ui.changelog) {
|
||||
ui.notes.prereleases = launcher.settings().prereleases;
|
||||
changelog_window(fonts, size, ui);
|
||||
}
|
||||
else if (ui.mods) mods_window(launcher, fonts, size, ui, mods_panel, window);
|
||||
else if (state.phase == Phase::mods_broken)
|
||||
mods_broken_window(launcher, fonts, size, ui, mods_panel, state.mod_problems);
|
||||
|
||||
@@ -194,6 +194,9 @@ struct Settings {
|
||||
bool crash_reports{true};
|
||||
// On: launches send how long their steps took (RESKATE_TRACING=1 for the launcher and the game).
|
||||
bool performance_timings{};
|
||||
// On: update to the newest pre-release on GitHub when there is one newer than the latest
|
||||
// release. Turned off, the next check goes back to the latest release.
|
||||
bool prereleases{};
|
||||
std::string steam_username; // never the password
|
||||
bool steam_remember{true};
|
||||
bool steam_prefer_code{};
|
||||
@@ -318,6 +321,8 @@ struct Changelog {
|
||||
std::string error; // why the last fetch failed
|
||||
update::ReleaseNote note;
|
||||
thunderstore::Readme text;
|
||||
// Whether pre-releases are wanted (the setting), and whether `note` was fetched with them.
|
||||
bool prereleases{}, fetched_prereleases{};
|
||||
|
||||
~Changelog() {
|
||||
if (worker.joinable()) worker.join();
|
||||
|
||||
@@ -66,6 +66,7 @@ Settings load_settings(const fs::path& path) {
|
||||
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.prereleases = json.value("prereleases", settings.prereleases);
|
||||
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);
|
||||
@@ -92,6 +93,7 @@ void save_settings(const fs::path& path, const Settings& settings) {
|
||||
json["updates"] = settings.updates;
|
||||
json["crash_reports"] = settings.crash_reports;
|
||||
json["performance_timings"] = settings.performance_timings;
|
||||
json["prereleases"] = settings.prereleases;
|
||||
json["steam_username"] = settings.steam_username;
|
||||
json["steam_remember"] = settings.steam_remember;
|
||||
json["steam_prefer_code"] = settings.steam_prefer_code;
|
||||
@@ -310,7 +312,7 @@ bool Launcher::runtime_outdated(const update::Config& config) const {
|
||||
void Launcher::run_check() {
|
||||
set(Phase::checking, "Checking for updates");
|
||||
update::remove_previous_launcher(session_.self);
|
||||
auto config = update::fetch_config();
|
||||
auto config = update::fetch_config(settings_.prereleases);
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
state_.config = config;
|
||||
@@ -318,7 +320,7 @@ void Launcher::run_check() {
|
||||
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.",
|
||||
std::format("{} {} will be downloaded and installed.", config->prerelease ? "Pre-release" : "Version",
|
||||
config->runtime.version.empty() ? config->launcher.version : config->runtime.version));
|
||||
return;
|
||||
}
|
||||
@@ -521,6 +523,9 @@ namespace dingosdk::launcher_gui {
|
||||
bool updates_enabled(const launcher_app::Session& session) {
|
||||
return detail::load_settings(detail::settings_path(session)).updates;
|
||||
}
|
||||
bool prereleases_enabled(const launcher_app::Session& session) {
|
||||
return detail::load_settings(detail::settings_path(session)).prereleases;
|
||||
}
|
||||
launcher::LaunchOptions saved_launch_options(const launcher_app::Session& session) {
|
||||
return detail::launch_options(detail::load_settings(detail::settings_path(session)));
|
||||
}
|
||||
|
||||
@@ -435,6 +435,11 @@ void settings_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, Ui& ui
|
||||
"Turn off to keep the ReSkate.dll and launcher you have, such as a test build someone gave you. "
|
||||
"Skate's own Steam updates are not affected.",
|
||||
settings.updates);
|
||||
setting_check("Test pre-releases",
|
||||
"Update to the newest pre-release on GitHub when there is one, to try what is coming before it "
|
||||
"is released. Pre-releases are less tested. Turn off to go back to the latest release; the "
|
||||
"launcher picks a change up when it next starts.",
|
||||
settings.prereleases);
|
||||
ImGui::Spacing();
|
||||
section_caption(fonts, "TROUBLESHOOTING");
|
||||
ImGui::AlignTextToFramePadding();
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ void run_headless(const app::Session& session, std::vector<std::wstring> argumen
|
||||
if (!map.empty()) options.map = std::move(map);
|
||||
update::remove_previous_launcher(session.self);
|
||||
if (updates) {
|
||||
const auto config = update::fetch_config();
|
||||
const auto config = update::fetch_config(dingosdk::launcher_gui::prereleases_enabled(session));
|
||||
if (config && apply_headless_updates(session, *config, original, relaunched)) return;
|
||||
}
|
||||
if (!app::game_files_supported(session.paths))
|
||||
|
||||
+37
-15
@@ -325,7 +325,30 @@ Config parse_config(std::string_view text) {
|
||||
return config;
|
||||
}
|
||||
|
||||
std::optional<Config> fetch_config() {
|
||||
namespace {
|
||||
// The release the launcher updates from: GitHub's latest full release, or with `prereleases`
|
||||
// the newest one that is not a draft and carries a launcher.json, pre-release or not (GitHub
|
||||
// lists them newest first). Only these lookups use GitHub's rate-limited API.
|
||||
Json release_to_use(bool prereleases) {
|
||||
const auto repo = std::wstring(L"https://api.github.com/repos/") + launcher_release_repo;
|
||||
std::string text;
|
||||
if (!prereleases) {
|
||||
http_get(repo + L"/releases/latest", 4 * 1024 * 1024, 8000, [&](const char* data, DWORD count) { text.append(data, count); });
|
||||
return Json::parse(text);
|
||||
}
|
||||
http_get(repo + L"/releases?per_page=10", 4 * 1024 * 1024, 8000, [&](const char* data, DWORD count) { text.append(data, count); });
|
||||
auto releases = Json::parse(text);
|
||||
if (!releases.is_array()) fail("GitHub's list of releases is not a list");
|
||||
for (const auto& release : releases) {
|
||||
if (!release.is_object() || release.value("draft", false) || !release.contains("assets")) continue;
|
||||
for (const auto& asset : release.at("assets"))
|
||||
if (asset.value("name", "") == "launcher.json") return release;
|
||||
}
|
||||
fail("None of the newest releases has a launcher.json asset");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::optional<Config> fetch_config(bool prereleases) {
|
||||
std::array<wchar_t, 32768> local{};
|
||||
const auto local_length = GetEnvironmentVariableW(L"RESKATE_LAUNCHER_CONFIG_FILE", local.data(), static_cast<DWORD>(local.size()));
|
||||
if (local_length && local_length < local.size()) {
|
||||
@@ -346,28 +369,29 @@ std::optional<Config> fetch_config() {
|
||||
// Public releases: the newest release carries launcher.json and the files it pins. Only
|
||||
// this lookup uses GitHub's rate-limited API; the files come from the release's download links.
|
||||
trace::Scope timing("launcher.update_check", "http.client");
|
||||
const auto latest = std::wstring(L"https://api.github.com/repos/") + launcher_release_repo + L"/releases/latest";
|
||||
logging::write(logging::Level::info, logging::Channel::launcher, L"Checking GitHub releases: " + std::wstring(launcher_release_repo));
|
||||
std::string text;
|
||||
http_get(latest, 4 * 1024 * 1024, 8000, [&](const char* data, DWORD count) { text.append(data, count); });
|
||||
const auto release = Json::parse(text);
|
||||
logging::write(logging::Level::info, logging::Channel::launcher, L"Checking GitHub releases: " + std::wstring(launcher_release_repo) +
|
||||
(prereleases ? L" (pre-releases included)" : L""));
|
||||
const auto release = release_to_use(prereleases);
|
||||
std::map<std::string, std::wstring, std::less<>> assets;
|
||||
if (release.contains("assets"))
|
||||
for (const auto& asset : release.at("assets"))
|
||||
assets[asset.value("name", "")] = widen(asset.value("browser_download_url", ""));
|
||||
const auto manifest = assets.find("launcher.json");
|
||||
if (manifest == assets.end()) fail("The latest release has no launcher.json asset");
|
||||
if (manifest == assets.end()) fail("The release has no launcher.json asset");
|
||||
std::string json;
|
||||
http_get(manifest->second, max_config_bytes, 8000, [&](const char* data, DWORD count) { json.append(data, count); });
|
||||
auto config = parse_config(json);
|
||||
for (auto* file : {&config.launcher, &config.runtime, &config.depot_downloader, &config.server}) {
|
||||
if (!file->url.starts_with(L"asset:")) continue;
|
||||
const auto found = assets.find(utf8(std::wstring_view(file->url).substr(6)));
|
||||
if (found == assets.end()) fail("The latest release is missing asset " + utf8(file->url.substr(6)));
|
||||
if (found == assets.end()) fail("The release is missing asset " + utf8(file->url.substr(6)));
|
||||
file->url = found->second;
|
||||
}
|
||||
config.tag = release.value("tag_name", "");
|
||||
config.prerelease = release.value("prerelease", false);
|
||||
logging::log(logging::Level::info, logging::Channel::launcher,
|
||||
"Release {}: launcher {}, runtime {}, game depot {} manifest {}.", release.value("tag_name", "?"),
|
||||
"{} {}: launcher {}, runtime {}, game depot {} manifest {}.", config.prerelease ? "Pre-release" : "Release",
|
||||
config.tag.empty() ? "?" : config.tag,
|
||||
config.launcher.version.empty() ? "-" : config.launcher.version,
|
||||
config.runtime.version.empty() ? "-" : config.runtime.version,
|
||||
config.game.depot_id, config.game.manifest_id);
|
||||
@@ -480,18 +504,16 @@ std::vector<ReleaseNote> parse_release_feed(std::string_view feed) {
|
||||
return notes;
|
||||
}
|
||||
|
||||
ReleaseNote fetch_release_note() {
|
||||
ReleaseNote fetch_release_note(bool prereleases) {
|
||||
// The description as it was written, markdown, from GitHub's API.
|
||||
try {
|
||||
const auto url = std::wstring(L"https://api.github.com/repos/") + launcher_release_repo + L"/releases/latest";
|
||||
std::string text;
|
||||
http_get(url, 4 * 1024 * 1024, 8000, [&](const char* data, DWORD count) { text.append(data, count); });
|
||||
const auto release = Json::parse(text);
|
||||
const auto release = release_to_use(prereleases);
|
||||
// (A release made without a title or a description has null there.)
|
||||
const auto field = [&](std::string_view key) {
|
||||
return release.contains(key) && release.at(key).is_string() ? release.at(key).string() : std::string();
|
||||
};
|
||||
ReleaseNote note{field("tag_name"), field("name"), field("published_at").substr(0, 10), field("body")};
|
||||
ReleaseNote note{field("tag_name"), field("name"), field("published_at").substr(0, 10), field("body"),
|
||||
release.value("prerelease", false)};
|
||||
if (!note.tag.empty() && note.notes.find_first_not_of(" \t\r\n") != std::string::npos) return note;
|
||||
} catch (const std::exception& exception) {
|
||||
logging::log(logging::Level::info, logging::Channel::launcher,
|
||||
|
||||
+10
-3
@@ -39,23 +39,30 @@ struct Config {
|
||||
// it: a server whose own exe differs is out of date. Empty when not published.
|
||||
RemoteFile server;
|
||||
std::string server_exe_sha256;
|
||||
// The release it came from, and whether GitHub marks it a pre-release.
|
||||
std::string tag;
|
||||
bool prerelease{};
|
||||
};
|
||||
|
||||
// Release builds replace ReSkateLauncher.exe and ReSkate.dll; local builds only report.
|
||||
bool binary_updates_enabled() noexcept;
|
||||
Config parse_config(std::string_view text);
|
||||
// Returns nothing when offline or the config is invalid; the reason is logged.
|
||||
std::optional<Config> fetch_config();
|
||||
// `prereleases`: the newest release that carries a launcher.json, pre-release or not (the
|
||||
// launcher's "Test pre-releases" setting); otherwise GitHub's latest full release.
|
||||
std::optional<Config> fetch_config(bool prereleases = false);
|
||||
|
||||
// One GitHub release, for the launcher's changelogs: its patch notes are what its page on
|
||||
// GitHub shows, the release's description or, without one, its commit's message.
|
||||
struct ReleaseNote {
|
||||
std::string tag, title, date, notes; // date: YYYY-MM-DD; notes: markdown
|
||||
bool prerelease{};
|
||||
};
|
||||
// The releases page's Atom feed (<repo>/releases.atom), newest first as it comes.
|
||||
std::vector<ReleaseNote> parse_release_feed(std::string_view feed);
|
||||
// The newest release of the repo the launcher updates from. Throws when GitHub cannot be reached.
|
||||
ReleaseNote fetch_release_note();
|
||||
// The newest release of the repo the launcher updates from (with `prereleases`, the newest
|
||||
// pre-release when it is newer). Throws when GitHub cannot be reached.
|
||||
ReleaseNote fetch_release_note(bool prereleases = false);
|
||||
// That repo's releases page on GitHub.
|
||||
std::string releases_page();
|
||||
|
||||
|
||||
Reference in new issue
Block a user