diff --git a/Engine/Game/Build/supported_build.h b/Engine/Game/Build/supported_build.h index d49886d..88c658d 100644 --- a/Engine/Game/Build/supported_build.h +++ b/Engine/Game/Build/supported_build.h @@ -23,6 +23,10 @@ inline constexpr std::uint32_t steam_app_id = 3354750; inline constexpr std::uint32_t steam_depot_id = 3354751; inline constexpr std::string_view steam_manifest_id = "4621099302092747785"; inline constexpr std::string_view steam_build_id = "25414733"; +// The build number this build's data files carry (Data\layout.toc, "pipelineCodeChangelists"): +// what mods made for it carry too. Data of another number beside a supported Skate.exe is a +// game that was updated or copied over in part, and has to be verified. +inline constexpr std::string_view game_data_changelist = "29544144"; // The live game's HTTP content cache for this build (catalogue chunks and CDN // images), published as a ReSkateCache release and installed by the launcher. inline constexpr std::wstring_view content_cache_url = diff --git a/Engine/Vfs/mod_catalog.cpp b/Engine/Vfs/mod_catalog.cpp index d0cd48d..39d0dfc 100644 --- a/Engine/Vfs/mod_catalog.cpp +++ b/Engine/Vfs/mod_catalog.cpp @@ -3,6 +3,7 @@ #include "mod_merge.h" #include "native_db.h" #include "Engine/Core/Json/json.h" +#include "Engine/Game/Build/supported_build.h" #include #include @@ -402,7 +403,8 @@ Catalog load_catalog(const std::filesystem::path& data_root, const MergeObserver auto exclusions = read_exclusions(result.root); const auto sdk = sdk_identity(); std::map> still_excluded; - const auto leave_out = [&](std::size_t index, std::vector problems, const std::string& fingerprint) { + // `remembered`: it stays out on later launches without a retry, until its files or ReSkate change. + const auto leave_out = [&](std::size_t index, std::vector problems, const std::string& fingerprint, bool remembered = true) { auto mod = std::move(result.mods[index]); result.mods.erase(result.mods.begin() + static_cast(index)); // Level bundles decide whether a map loads at all, so their problems lead. @@ -410,7 +412,7 @@ Catalog load_catalog(const std::filesystem::path& data_root, const MergeObserver return problem.find("levels/game/") != std::string::npos; }); mod.problems = std::move(problems); - still_excluded[mod.name] = {fingerprint, sdk, mod.problems}; + if (remembered) still_excluded[mod.name] = {fingerprint, sdk, mod.problems}; result.excluded.push_back(std::move(mod)); }; for (std::size_t i = result.mods.size(); i-- > 0;) { @@ -418,6 +420,10 @@ Catalog load_catalog(const std::filesystem::path& data_root, const MergeObserver if (found == list.excluded.end()) continue; const auto& previous = exclusions.at(result.mods[i].name); if (sdk.empty() || previous.sdk != sdk) continue; // a new ReSkate may merge it now + // One left out because the game was of another build is looked at again below: the + // game may have been put right since (its files verified), with the mod untouched. + // Remembered, it stayed out for good with the game fixed (seen 2026-10-13). + if (!found->second.empty() && found->second.front().starts_with("made for game build ")) continue; result.notes.push_back("Leaving out " + result.mods[i].name + ": it could not be merged cleanly before and its files have not changed since"); leave_out(i, found->second, previous.fingerprint); @@ -438,9 +444,13 @@ Catalog load_catalog(const std::filesystem::path& data_root, const MergeObserver if (build.empty()) continue; result.notes.push_back("Leaving out " + result.mods[i].name + ": it was made for game build " + build + ", the installed game is " + game_build); + // (When it is the game that is of another build than ReSkate's, the mod is not what + // needs seeing to.) leave_out(i, {"made for game build " + build + ", not the installed " + game_build + - "; rebuild it with ReSkate Studio for this game build"}, - mod_fingerprint(result.mods[i].directory)); + (game_build != supported_build::game_data_changelist + ? "; your game files are not the version ReSkate supports: in the launcher, open Settings and press Verify game files" + : "; rebuild it with ReSkate Studio for this game build")}, + mod_fingerprint(result.mods[i].directory), false); // (checked on every launch: the game's build can change under it) } for (std::size_t i = result.inactive.size(); i-- > 0;) { const auto build = made_for(result.inactive[i]); diff --git a/Launcher/gui_launcher.cpp b/Launcher/gui_launcher.cpp index 4dd8140..e745ace 100644 --- a/Launcher/gui_launcher.cpp +++ b/Launcher/gui_launcher.cpp @@ -181,7 +181,9 @@ void Launcher::download(bool validate, bool qr, std::string password) { password_ = std::move(password); qr_login_ = qr; } - start([this, validate] { run_download(validate); }); + // Data of another build is put right by checking every file, not only fetching what is missing. + const bool verify = validate || launcher_app::game_data_mismatched(session_.paths); + start([this, verify] { run_download(verify); }); } void Launcher::play() { start([this] { run_play(); }); } @@ -333,7 +335,13 @@ void Launcher::run_check() { fail("This launcher is out of date. Download the latest ReSkate release."); return; } - if (installed) set(Phase::game_outdated, "Steam updated Skate", + // (Data of another build under a Skate.exe that may itself be right: every file is + // checked, which download() sees to.) + if (installed && launcher_app::game_data_mismatched(session_.paths)) + set(Phase::game_outdated, "Game files need verifying", + "Some of the game's files are from another version of skate. (Steam updated it, or a download did not finish). " + "Press DOWNLOAD: every file is checked and only the wrong ones are downloaded."); + else 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."); diff --git a/Launcher/launch.cpp b/Launcher/launch.cpp index c52f354..40f330b 100644 --- a/Launcher/launch.cpp +++ b/Launcher/launch.cpp @@ -20,6 +20,8 @@ #include #include #include +#include "Engine/Game/Build/supported_build.h" +#include #include #include #include @@ -468,6 +470,33 @@ Session open_session(const std::string& log_level) { return session; } +std::string game_data_build(const launcher::SiblingPaths& paths) { + try { + std::ifstream file(paths.directory / L"Data" / L"layout.toc", std::ios::binary); + if (!file) return {}; + const std::string bytes{std::istreambuf_iterator(file), std::istreambuf_iterator()}; + // The number follows its name as text, after a few bytes that say what kind of value it is. + constexpr std::string_view name = "pipelineCodeChangelists"; + const auto at = bytes.find(name); + if (at == std::string::npos) return {}; + const auto is_digit = [](char c) { return c >= '0' && c <= '9'; }; + auto from = at + name.size(); + const auto limit = std::min(bytes.size(), from + 16); + while (from < limit && !is_digit(bytes[from])) ++from; + auto to = from; + while (to < bytes.size() && to - from < 16 && is_digit(bytes[to])) ++to; + return to - from >= 6 ? bytes.substr(from, to - from) : std::string{}; + } catch (...) { + return {}; + } +} +bool game_data_mismatched(const launcher::SiblingPaths& paths) { + std::error_code error; + if (!fs::is_regular_file(paths.directory / L"Data" / L"layout.toc", error)) return true; + const auto build = game_data_build(paths); + return !build.empty() && build != supported_build::game_data_changelist; +} + bool game_files_supported(const launcher::SiblingPaths& paths) { std::error_code error; if (!fs::is_regular_file(paths.game, error)) { @@ -477,6 +506,14 @@ bool game_files_supported(const launcher::SiblingPaths& paths) { try { launcher::validate_game_file(paths.game); launcher::validate_steam_api_file(paths.steam_api); + // The right Skate.exe over another build's data is no supported game: its mods are + // left out and it may not load (seen 2026-10-13, after a Steam update of the game). + if (game_data_mismatched(paths)) { + const auto build = game_data_build(paths); + logging::log(logging::Level::warning, logging::Channel::launcher, "Game files are not the supported build: Skate.exe is, but the game's data is {} (supported: {}).", + build.empty() ? std::string("missing") : "build " + build, supported_build::game_data_changelist); + return false; + } return true; } catch (const std::exception& exception) { logging::log(logging::Level::warning, logging::Channel::launcher, diff --git a/Launcher/launch.h b/Launcher/launch.h index 15c2cb6..a0d206d 100644 --- a/Launcher/launch.h +++ b/Launcher/launch.h @@ -19,6 +19,14 @@ struct Session { Session open_session(const std::string& log_level); // Skate.exe and steam_api64.dll are present and exactly the supported build. bool game_files_supported(const launcher::SiblingPaths& paths); +// The build number the installed game's data says it is (Data\layout.toc), or empty when +// there is none or it cannot be read. +std::string game_data_build(const launcher::SiblingPaths& paths); +// Whether the game's data is missing or of another build than the supported one, whatever +// Skate.exe is: Steam updated the game under it, a copy of an updated game was put here, or a +// download stopped part-way. Such a game has to be verified against the supported build, file +// by file, not only topped up. False when the data's build cannot be told. +bool game_data_mismatched(const launcher::SiblingPaths& paths); // The config's depot/manifest ships the build this launcher was compiled for. bool config_matches_build(const launcher_update::Config& config); void relaunch(const std::filesystem::path& self, const std::vector& arguments);