skate3recomp-multiplayer/src/skate3_title_update_installer.cpp
2026-06-10 05:56:35 +01:00

879 lines
30 KiB
C++

#include "skate3_title_update_installer.h"
#include <algorithm>
#include <array>
#include <atomic>
#include <cctype>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <memory>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
#include <rex/cvar.h>
#include <rex/logging.h>
#include <rex/ui/overlay/acquire_wizard_overlay.h>
#include <rex/ui/windowed_app_context.h>
#include "third_party/rexglue-sdk/thirdparty/crypto/sha256.h"
#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <commdlg.h>
#include <windows.h>
#include <winhttp.h>
#include <rex/ui/window_win.h>
#elif defined(__APPLE__)
#else
#include <gtk/gtk.h>
#endif
REXCVAR_DEFINE_STRING(skate3_title_update_url,
"https://xboxunity.net/Resources/Lib/TitleUpdate.php?tuid=21774",
"Skate 3",
"Download URL for the Skate 3 Title Update 3 package (TU3)");
namespace skate3 {
#if defined(__APPLE__)
std::filesystem::path PickTitleUpdateFileMacOS();
#endif
namespace {
// The recompilation is generated against exactly these payloads
// (Skate 3 title 454108E6, base 3.0.0.0 -> 3.0.3.0, media ID 5C087C2C).
struct TitleUpdatePayload {
std::string_view container_path; // path inside the TU STFS package
std::string_view staged_path; // path relative to the game root
uint64_t size;
std::string_view sha256; // lowercase hex
};
constexpr std::array<TitleUpdatePayload, 2> kPayloads = {{
{"default.xexp", "default.xexp", 1701888,
"eb9ef9109dfa6d940df2e156e7eaeda4603d2b2319ca6451f324b1c27f2b1f4c"},
{"data/webkit/EAWebkit.xexp", "data/webkit/EAWebkit.xexp", 4096,
"5d4a308d2a6c768fc27c8b62ccb6661171dc504f8c0011a3e116cf0074e09438"},
}};
// Size of the known TU3 STFS container; used as the progress estimate when the
// HTTP response carries no Content-Length, and as a sanity cap for downloads.
constexpr uint64_t kContainerSize = 1773568;
constexpr uint64_t kMaxPackageSize = 256ull * 1024 * 1024;
std::string ToLowerCopy(std::string value) {
std::transform(value.begin(), value.end(), value.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return value;
}
std::string Sha256OfData(const uint8_t* data, size_t size) {
sha256::SHA256 hasher;
return hasher(data, size);
}
bool ReadWholeFile(const std::filesystem::path& path, std::vector<uint8_t>& out,
std::string& error) {
std::error_code ec;
const auto size = std::filesystem::file_size(path, ec);
if (ec) {
error = "Unable to read " + path.string() + ".";
return false;
}
if (size > kMaxPackageSize) {
error = "The selected file is too large to be a Skate 3 title update.";
return false;
}
std::ifstream file(path, std::ios::binary);
if (!file) {
error = "Unable to open " + path.string() + ".";
return false;
}
out.resize(static_cast<size_t>(size));
file.read(reinterpret_cast<char*>(out.data()), static_cast<std::streamsize>(out.size()));
if (!file) {
error = "Failed to read " + path.string() + ".";
return false;
}
return true;
}
const TitleUpdatePayload* MatchPayload(const std::vector<uint8_t>& data) {
for (const auto& payload : kPayloads) {
if (data.size() == payload.size && Sha256OfData(data.data(), data.size()) == payload.sha256) {
return &payload;
}
}
return nullptr;
}
bool StagePayload(const TitleUpdatePayload& payload, const std::vector<uint8_t>& data,
const std::filesystem::path& game_root, std::string& error) {
const auto target = game_root / std::filesystem::path(std::string(payload.staged_path));
std::error_code ec;
std::filesystem::create_directories(target.parent_path(), ec);
if (ec) {
error = "Unable to create " + target.parent_path().string() + ".";
return false;
}
std::ofstream out(target, std::ios::binary | std::ios::trunc);
if (!out) {
error = "Unable to create " + target.string() + ".";
return false;
}
out.write(reinterpret_cast<const char*>(data.data()), static_cast<std::streamsize>(data.size()));
if (!out) {
error = "Failed to write " + target.string() + ".";
return false;
}
REXLOG_INFO("Staged Skate 3 title update payload {}", target.string());
return true;
}
// ----------------------------------------------------------------------------
// Minimal read-only STFS (CON/LIVE/PIRS) package reader, enough to pull the
// xexp payloads out of a title update package.
// ----------------------------------------------------------------------------
uint16_t Be16(const uint8_t* p) {
return static_cast<uint16_t>((p[0] << 8) | p[1]);
}
uint32_t Be32(const uint8_t* p) {
return (static_cast<uint32_t>(p[0]) << 24) | (static_cast<uint32_t>(p[1]) << 16) |
(static_cast<uint32_t>(p[2]) << 8) | static_cast<uint32_t>(p[3]);
}
uint16_t Le16(const uint8_t* p) {
return static_cast<uint16_t>(p[0] | (p[1] << 8));
}
uint32_t U24Le(const uint8_t* p) {
return static_cast<uint32_t>(p[0]) | (static_cast<uint32_t>(p[1]) << 8) |
(static_cast<uint32_t>(p[2]) << 16);
}
class StfsPackageReader {
public:
static constexpr uint32_t kBlockSize = 0x1000;
static constexpr uint32_t kEndOfChain = 0xFFFFFF;
struct Entry {
std::string path;
bool is_dir = false;
uint32_t start_block = 0;
uint32_t length = 0;
};
static bool LooksLikeStfs(const std::vector<uint8_t>& data) {
if (data.size() < 4) {
return false;
}
const std::string_view magic(reinterpret_cast<const char*>(data.data()), 4);
return magic == "CON " || magic == "LIVE" || magic == "PIRS";
}
bool Open(std::vector<uint8_t> data, std::string& error) {
data_ = std::move(data);
if (data_.size() < 0x400 || !LooksLikeStfs(data_)) {
error = "The file is not an Xbox 360 content package.";
return false;
}
header_size_ = Be32(at(0x340, 4));
metadata_offset_ = 0x344;
volume_descriptor_offset_ = metadata_offset_ + 0x35;
const uint32_t volume_type = Be32(at(metadata_offset_ + 0x65, 4));
if (volume_type != 0) {
error = "The content package is not an STFS volume.";
return false;
}
const uint8_t flags = *at(volume_descriptor_offset_ + 2, 1);
blocks_per_hash_table_ = (flags & 1) ? 1 : 2;
return true;
}
bool ListEntries(std::vector<Entry>& entries, std::string& error) {
entries.clear();
const uint8_t* descriptor = at(volume_descriptor_offset_, 8);
if (!descriptor) {
error = "The content package header is truncated.";
return false;
}
uint16_t table_block_count = Le16(descriptor + 3);
uint32_t table_block = U24Le(descriptor + 5);
for (uint32_t table = 0; table < table_block_count; ++table) {
const uint64_t table_offset = BlockToOffset(table_block);
for (uint32_t index = 0; index < 0x40; ++index) {
const uint8_t* entry = at(table_offset + index * 0x40, 0x40);
if (!entry) {
error = "The content package file table is truncated.";
return false;
}
if (entry[0] == 0) {
break;
}
const uint8_t flags = entry[40];
const uint32_t name_length = flags & 0x3F;
if (name_length == 0 || name_length > 40) {
error = "The content package contains an invalid file name.";
return false;
}
Entry parsed;
std::string name(reinterpret_cast<const char*>(entry), name_length);
parsed.is_dir = (flags & 0x80) != 0;
parsed.start_block = U24Le(entry + 47);
const uint16_t parent_index = Be16(entry + 50);
parsed.length = Be32(entry + 52);
std::string parent_path;
if (parent_index != 0xFFFF) {
if (parent_index >= entries.size()) {
error = "The content package directory tree is invalid.";
return false;
}
parent_path = entries[parent_index].path;
}
parsed.path = parent_path.empty() ? name : parent_path + "/" + name;
entries.push_back(std::move(parsed));
}
const uint32_t next = NextBlock(table_block);
if (next == kEndOfChain) {
break;
}
table_block = next;
}
return true;
}
bool ReadFile(const Entry& entry, std::vector<uint8_t>& out, std::string& error) {
out.clear();
out.reserve(entry.length);
uint32_t block_index = entry.start_block;
uint64_t remaining = entry.length;
while (remaining > 0 && block_index != kEndOfChain) {
const uint64_t chunk = std::min<uint64_t>(kBlockSize, remaining);
const uint8_t* block = at(BlockToOffset(block_index), chunk);
if (!block) {
error = "The content package data is truncated.";
return false;
}
out.insert(out.end(), block, block + chunk);
remaining -= chunk;
block_index = NextBlock(block_index);
}
if (remaining > 0) {
error = "The content package block chain ended unexpectedly.";
return false;
}
return true;
}
private:
const uint8_t* at(uint64_t offset, uint64_t size) const {
if (offset + size > data_.size()) {
return nullptr;
}
return data_.data() + offset;
}
uint64_t RoundUp(uint64_t value, uint64_t alignment) const {
return (value + alignment - 1) & ~(alignment - 1);
}
uint64_t BlockToOffset(uint32_t block_index) const {
uint64_t block = block_index;
uint64_t base = 170;
for (int i = 0; i < 3; ++i) {
block += ((block_index + base) / base) * blocks_per_hash_table_;
if (block_index < base) {
break;
}
base *= 170;
}
return RoundUp(header_size_, kBlockSize) + (block << 12);
}
uint32_t HashBlockNumber(uint32_t block_index, int hash_level) const {
const uint32_t block_step0 = 170 + blocks_per_hash_table_;
const uint32_t block_step1 = 28900 + (170 + 1) * blocks_per_hash_table_;
if (hash_level == 0) {
if (block_index < 170) {
return 0;
}
uint32_t block = (block_index / 170) * block_step0;
block += ((block_index / 28900) + 1) * blocks_per_hash_table_;
if (block_index < 28900) {
return block;
}
return block + blocks_per_hash_table_;
}
if (hash_level == 1) {
if (block_index < 28900) {
return block_step0;
}
const uint32_t block = (block_index / 28900) * block_step1;
return block + blocks_per_hash_table_;
}
return block_step1;
}
uint64_t HashOffset(uint32_t block_index, int hash_level) const {
return RoundUp(header_size_, kBlockSize) +
(static_cast<uint64_t>(HashBlockNumber(block_index, hash_level)) << 12);
}
uint32_t NextBlock(uint32_t block_index) const {
const uint64_t hash_offset = HashOffset(block_index, 0);
const uint8_t* hash_entry = at(hash_offset + (block_index % 170) * 0x18, 0x18);
if (!hash_entry) {
return kEndOfChain;
}
return Be32(hash_entry + 0x14) & 0xFFFFFF;
}
std::vector<uint8_t> data_;
uint32_t header_size_ = 0;
uint32_t metadata_offset_ = 0;
uint32_t volume_descriptor_offset_ = 0;
uint32_t blocks_per_hash_table_ = 2;
};
// ----------------------------------------------------------------------------
// Download
// ----------------------------------------------------------------------------
#if defined(_WIN32)
std::wstring Widen(const std::string& value) {
if (value.empty()) {
return {};
}
const int length =
MultiByteToWideChar(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), nullptr, 0);
std::wstring wide(static_cast<size_t>(length), L'\0');
MultiByteToWideChar(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), wide.data(),
length);
return wide;
}
bool DownloadToFile(const std::string& url, const std::filesystem::path& destination,
std::atomic<uint64_t>& copied_bytes, std::atomic<uint64_t>& total_bytes,
std::string& error) {
std::wstring wide_url = Widen(url);
URL_COMPONENTS components{};
components.dwStructSize = sizeof(components);
components.dwSchemeLength = static_cast<DWORD>(-1);
components.dwHostNameLength = static_cast<DWORD>(-1);
components.dwUrlPathLength = static_cast<DWORD>(-1);
components.dwExtraInfoLength = static_cast<DWORD>(-1);
if (!WinHttpCrackUrl(wide_url.c_str(), 0, 0, &components)) {
error = "The configured title update URL is invalid.";
return false;
}
const std::wstring host(components.lpszHostName, components.dwHostNameLength);
std::wstring path(components.lpszUrlPath, components.dwUrlPathLength);
if (components.lpszExtraInfo && components.dwExtraInfoLength) {
path.append(components.lpszExtraInfo, components.dwExtraInfoLength);
}
const bool secure = components.nScheme == INTERNET_SCHEME_HTTPS;
HINTERNET session = WinHttpOpen(L"skate3-recomp/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
if (!session) {
error = "Unable to initialize the download (WinHttpOpen failed).";
return false;
}
// Many hosts (xboxunity.net included) publish both A and AAAA records. Unlike
// browsers, WinHTTP connects to addresses sequentially, so on a machine with
// broken IPv6 routing it stalls on the dead IPv6 connect for ~15s before
// falling back to IPv4. Enable "fast fallback" (Happy Eyeballs, RFC 6555) so
// WinHTTP races both families in parallel and uses whichever connects first.
#ifndef WINHTTP_OPTION_IPV6_FAST_FALLBACK
#define WINHTTP_OPTION_IPV6_FAST_FALLBACK 140
#endif
BOOL fast_fallback = TRUE;
WinHttpSetOption(session, WINHTTP_OPTION_IPV6_FAST_FALLBACK, &fast_fallback,
sizeof(fast_fallback));
// Backstop for OS versions without fast fallback: bound the connect attempt so
// a dead address family cannot hang the installer indefinitely.
WinHttpSetTimeouts(session, /*resolve=*/10000, /*connect=*/10000, /*send=*/30000,
/*receive=*/60000);
auto fail = [&](const char* what) {
error = std::string("Download failed (") + what + ", error " +
std::to_string(GetLastError()) + "). Check your internet connection.";
return false;
};
bool ok = false;
HINTERNET connection = nullptr;
HINTERNET request = nullptr;
std::ofstream out;
do {
connection = WinHttpConnect(session, host.c_str(), components.nPort, 0);
if (!connection) {
fail("connect");
break;
}
request = WinHttpOpenRequest(connection, L"GET", path.c_str(), nullptr, WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES, secure ? WINHTTP_FLAG_SECURE : 0);
if (!request) {
fail("open request");
break;
}
if (!WinHttpSendRequest(request, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0,
0, 0) ||
!WinHttpReceiveResponse(request, nullptr)) {
fail("request");
break;
}
DWORD status_code = 0;
DWORD status_size = sizeof(status_code);
WinHttpQueryHeaders(request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX, &status_code, &status_size,
WINHTTP_NO_HEADER_INDEX);
if (status_code != 200) {
error = "The download server returned HTTP " + std::to_string(status_code) + ".";
break;
}
DWORD content_length = 0;
DWORD length_size = sizeof(content_length);
if (WinHttpQueryHeaders(request, WINHTTP_QUERY_CONTENT_LENGTH | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX, &content_length, &length_size,
WINHTTP_NO_HEADER_INDEX) &&
content_length > 0) {
total_bytes.store(content_length, std::memory_order_relaxed);
}
out.open(destination, std::ios::binary | std::ios::trunc);
if (!out) {
error = "Unable to create the download file at " + destination.string() + ".";
break;
}
std::vector<char> buffer(64 * 1024);
uint64_t received = 0;
bool read_failed = false;
for (;;) {
DWORD available = 0;
if (!WinHttpQueryDataAvailable(request, &available)) {
fail("read");
read_failed = true;
break;
}
if (available == 0) {
break;
}
while (available > 0) {
const DWORD chunk = std::min<DWORD>(available, static_cast<DWORD>(buffer.size()));
DWORD read = 0;
if (!WinHttpReadData(request, buffer.data(), chunk, &read) || read == 0) {
fail("read");
read_failed = true;
break;
}
out.write(buffer.data(), static_cast<std::streamsize>(read));
received += read;
available -= read;
copied_bytes.fetch_add(read, std::memory_order_relaxed);
if (received > kMaxPackageSize) {
error = "The download is unexpectedly large; aborting.";
read_failed = true;
break;
}
}
if (read_failed) {
break;
}
}
if (read_failed) {
break;
}
out.flush();
if (!out) {
error = "Failed to write the downloaded file.";
break;
}
ok = received > 0;
if (!ok) {
error = "The download was empty.";
}
} while (false);
if (request) {
WinHttpCloseHandle(request);
}
if (connection) {
WinHttpCloseHandle(connection);
}
WinHttpCloseHandle(session);
return ok;
}
#else
bool DownloadToFile(const std::string& url, const std::filesystem::path& destination,
std::atomic<uint64_t>& copied_bytes, std::atomic<uint64_t>& total_bytes,
std::string& error) {
if (url.find('\'') != std::string::npos || url.find('\\') != std::string::npos) {
error = "The configured title update URL is invalid.";
return false;
}
const std::string destination_str = destination.string();
if (destination_str.find('\'') != std::string::npos) {
error = "The temporary download path is not usable.";
return false;
}
const std::string command = "curl -fsSL --connect-timeout 30 --max-time 600 -o '" +
destination_str + "' '" + url + "'";
REXLOG_INFO("Downloading Skate 3 title update via curl");
const int status = std::system(command.c_str());
if (status != 0) {
error = "The download failed (curl exit status " + std::to_string(status) +
"). Check your internet connection, or select the title update file manually.";
return false;
}
std::error_code ec;
const auto size = std::filesystem::file_size(destination, ec);
if (ec || size == 0) {
error = "The download produced no data.";
return false;
}
total_bytes.store(size, std::memory_order_relaxed);
copied_bytes.store(size, std::memory_order_relaxed);
return true;
}
#endif // defined(_WIN32)
// ----------------------------------------------------------------------------
// File picker
// ----------------------------------------------------------------------------
#if defined(_WIN32)
std::filesystem::path PickTitleUpdateFile() {
wchar_t filename[MAX_PATH] = {};
OPENFILENAMEW ofn{};
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = GetActiveWindow();
ofn.lpstrFile = filename;
ofn.nMaxFile = static_cast<DWORD>(std::size(filename));
ofn.lpstrFilter = L"Title update package (*.*)\0*.*\0";
ofn.lpstrTitle = L"Select the Skate 3 Title Update 3 package";
ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR |
OFN_DONTADDTORECENT;
if (!GetOpenFileNameW(&ofn)) {
return {};
}
return filename;
}
#elif defined(__APPLE__)
std::filesystem::path PickTitleUpdateFile() {
return skate3::PickTitleUpdateFileMacOS();
}
#else
std::filesystem::path PickTitleUpdateFile() {
GtkWidget* dialog = gtk_file_chooser_dialog_new(
"Select the Skate 3 Title Update 3 package", nullptr, GTK_FILE_CHOOSER_ACTION_OPEN,
"_Cancel", GTK_RESPONSE_CANCEL, "_Open", GTK_RESPONSE_ACCEPT, nullptr);
if (!dialog) {
return {};
}
GtkFileFilter* all_filter = gtk_file_filter_new();
gtk_file_filter_set_name(all_filter, "All files");
gtk_file_filter_add_pattern(all_filter, "*");
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), all_filter);
std::filesystem::path result;
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
char* filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
if (filename) {
result = filename;
g_free(filename);
}
}
gtk_widget_destroy(dialog);
while (gtk_events_pending()) {
gtk_main_iteration_do(FALSE);
}
return result;
}
#endif
bool DownloadAndStageTitleUpdate(const std::filesystem::path& game_root,
std::atomic<uint64_t>& copied_bytes,
std::atomic<uint64_t>& total_bytes, std::string& error) {
total_bytes.store(kContainerSize, std::memory_order_relaxed);
std::error_code ec;
auto temp_dir = std::filesystem::temp_directory_path(ec);
if (ec) {
temp_dir = game_root;
}
const auto temp_file = temp_dir / "skate3_title_update_download.tmp";
const std::string url = REXCVAR_GET(skate3_title_update_url);
REXLOG_INFO("Downloading Skate 3 title update from {}", url);
const bool downloaded = DownloadToFile(url, temp_file, copied_bytes, total_bytes, error);
bool staged = false;
if (downloaded) {
staged = StageTitleUpdateFromFile(temp_file, game_root, error);
}
std::filesystem::remove(temp_file, ec);
return downloaded && staged;
}
} // namespace
bool IsTitleUpdateInstalled(const std::filesystem::path& game_root) {
for (const auto& payload : kPayloads) {
const auto staged = game_root / std::filesystem::path(std::string(payload.staged_path));
std::error_code ec;
if (!std::filesystem::is_regular_file(staged, ec) ||
std::filesystem::file_size(staged, ec) != payload.size || ec) {
return false;
}
std::vector<uint8_t> data;
std::string error;
if (!ReadWholeFile(staged, data, error) ||
Sha256OfData(data.data(), data.size()) != payload.sha256) {
return false;
}
}
return true;
}
bool StageTitleUpdateFromFile(const std::filesystem::path& source,
const std::filesystem::path& game_root, std::string& error) {
std::vector<uint8_t> data;
if (!ReadWholeFile(source, data, error)) {
return false;
}
if (StfsPackageReader::LooksLikeStfs(data)) {
StfsPackageReader package;
if (!package.Open(std::move(data), error)) {
return false;
}
std::vector<StfsPackageReader::Entry> entries;
if (!package.ListEntries(entries, error)) {
return false;
}
size_t staged_count = 0;
for (const auto& payload : kPayloads) {
const auto wanted = ToLowerCopy(std::string(payload.container_path));
const auto it = std::find_if(entries.begin(), entries.end(), [&](const auto& entry) {
return !entry.is_dir && ToLowerCopy(entry.path) == wanted;
});
if (it == entries.end()) {
error = "The package does not contain " + std::string(payload.container_path) +
"; it is not the Skate 3 title update.";
return false;
}
std::vector<uint8_t> file_data;
if (!package.ReadFile(*it, file_data, error)) {
return false;
}
if (file_data.size() != payload.size ||
Sha256OfData(file_data.data(), file_data.size()) != payload.sha256) {
error = "The package contains a different version of " +
std::string(payload.container_path) +
"; this build requires Title Update 3 (3.0.3.0).";
return false;
}
if (!StagePayload(payload, file_data, game_root, error)) {
return false;
}
++staged_count;
}
return staged_count == kPayloads.size();
}
// Not an STFS container; accept a raw xexp payload that matches one of the
// expected files exactly.
if (const auto* payload = MatchPayload(data)) {
if (!StagePayload(*payload, data, game_root, error)) {
return false;
}
if (!IsTitleUpdateInstalled(game_root)) {
error = std::string(payload->staged_path) +
" was installed, but the other title update file is still missing. Provide the "
"full title update package.";
return false;
}
return true;
}
error =
"The selected file is neither the Skate 3 title update package nor one of its "
"xexp payloads.";
return false;
}
void ShowTitleUpdateInstallWizard(rex::ui::ImGuiDrawer* drawer, rex::PathConfig runtime_paths,
std::function<void(rex::PathConfig)> complete) {
const auto game_root = runtime_paths.game_data_root;
rex::ui::AcquireWizardDialog::Options options;
options.title = "Skate 3 Title Update";
options.intro =
"This build of Skate 3 requires Title Update 3, a free update originally published on "
"Xbox Live. It is not part of the game disc.";
options.target_directory = game_root.string();
options.initial_status =
"Download it automatically, or select a title update package you already have.";
options.fetch_button_label = "Download (1.7 MB)";
options.pick_button_label = "Select file...";
options.fetch_connecting_status =
"Connecting to the download server... (this can take a moment)";
options.fetch_working_status = "Downloading Title Update 3...";
options.install_working_status = "Installing the title update...";
options.done_status = "Title Update 3 installed.";
options.done_button_label = "Start Game";
auto fetch = [game_root](std::atomic<uint64_t>& copied_bytes, std::atomic<uint64_t>& total_bytes,
std::string& error) {
if (!DownloadAndStageTitleUpdate(game_root, copied_bytes, total_bytes, error)) {
return false;
}
if (!IsTitleUpdateInstalled(game_root)) {
error = "The title update could not be verified after installation.";
return false;
}
return true;
};
auto install = [game_root](const std::filesystem::path& source,
std::atomic<uint64_t>& copied_bytes,
std::atomic<uint64_t>& total_bytes, std::string& error) {
(void)copied_bytes;
(void)total_bytes;
if (!StageTitleUpdateFromFile(source, game_root, error)) {
return false;
}
if (!IsTitleUpdateInstalled(game_root)) {
error = "The title update could not be verified after installation.";
return false;
}
return true;
};
new rex::ui::AcquireWizardDialog(
drawer, std::move(options), std::move(fetch), []() { return PickTitleUpdateFile(); },
std::move(install),
[runtime_paths = std::move(runtime_paths), complete = std::move(complete)]() mutable {
if (complete) {
complete(std::move(runtime_paths));
}
});
}
bool RunTitleUpdateInstallWizardBlocking(rex::ui::WindowedAppContext& app_context,
rex::ui::Window* window,
rex::ui::ImGuiDrawer* drawer,
rex::PathConfig runtime_paths,
rex::PathConfig& installed_paths) {
if (const char* automated_tu = std::getenv("SKATE3_INSTALL_TU");
automated_tu != nullptr && *automated_tu != '\0') {
std::string error;
bool ok = false;
REXLOG_INFO("Installing Skate 3 title update from SKATE3_INSTALL_TU={}", automated_tu);
if (std::string_view(automated_tu) == "download") {
std::atomic<uint64_t> copied_bytes{0};
std::atomic<uint64_t> total_bytes{0};
ok = DownloadAndStageTitleUpdate(runtime_paths.game_data_root, copied_bytes, total_bytes,
error);
} else {
ok = StageTitleUpdateFromFile(automated_tu, runtime_paths.game_data_root, error);
}
if (!ok || !IsTitleUpdateInstalled(runtime_paths.game_data_root)) {
REXLOG_ERROR("Automated title update installation failed: {}", error);
return false;
}
installed_paths = std::move(runtime_paths);
REXLOG_INFO("Automated title update installation completed successfully");
return true;
}
struct InstallResult {
bool done = false;
bool ok = false;
rex::PathConfig paths;
};
auto result = std::make_shared<InstallResult>();
ShowTitleUpdateInstallWizard(drawer, runtime_paths,
[result](rex::PathConfig runtime_paths) mutable {
result->paths = std::move(runtime_paths);
result->ok = true;
result->done = true;
});
#if defined(_WIN32)
HWND hwnd = nullptr;
if (auto* win32_window = dynamic_cast<rex::ui::Win32Window*>(window)) {
hwnd = win32_window->hwnd();
}
#endif
REXLOG_INFO("Entering Skate 3 title update installer pump");
while (!result->done && !app_context.HasQuitFromUIThread()) {
app_context.ExecutePendingFunctionsFromUIThread();
#if defined(_WIN32)
MSG message;
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
if (message.message == WM_QUIT) {
app_context.QuitFromUIThread();
break;
}
TranslateMessage(&message);
DispatchMessageW(&message);
}
if (app_context.HasQuitFromUIThread()) {
break;
}
if (window) {
window->RequestPaint();
}
if (hwnd) {
RedrawWindow(hwnd, nullptr, nullptr, RDW_INVALIDATE | RDW_UPDATENOW | RDW_NOERASE);
}
#else
if (window) {
window->RequestPaint();
}
#if !defined(__APPLE__)
while (gtk_events_pending()) {
gtk_main_iteration_do(FALSE);
}
#endif
#endif
std::this_thread::sleep_for(std::chrono::milliseconds(16));
}
if (!result->ok) {
REXLOG_INFO("Leaving title update installer pump without installation");
return false;
}
installed_paths = std::move(result->paths);
REXLOG_INFO("Leaving title update installer pump after successful installation");
return true;
}
} // namespace skate3