diff --git a/config/skate3.example.toml b/config/skate3.example.toml new file mode 100644 index 0000000..70b973a --- /dev/null +++ b/config/skate3.example.toml @@ -0,0 +1,64 @@ +# Optional developer override file. +# +# To override cvars locally, copy this file beside skate3.exe as skate3.toml +# and edit values there. Release builds do not require this file. + +log_verbose = false +log_level = "info" + +protect_zero = false + +audio_maxqframes = 16 +audio_worker_thread_priority = 0 + +input_backend = "xinput" +mnk_mode = false +mnk_capture_mouse = false + +fullscreen = true +vsync = false +video_mode_refresh_rate = 60.0 +resolution_scale = 2 +draw_resolution_scale_x = 2 +draw_resolution_scale_y = 2 +d3d12_allow_variable_refresh_rate_and_tearing = true +d3d12_strict_vsync_present = false +d3d12_frame_latency_waitable_object = false +d3d12_max_frame_latency = 1 +d3d12_present_frame_limiter = false +d3d12_present_frame_limiter_fps = 120.0 +d3d12_present_frame_limiter_sleep_margin_us = 1000 +d3d12_present_frame_limiter_spin_us = 150 + +vblank_host_clock_pacing = false +vblank_precise_sleep_margin_us = 1000 +vblank_precise_spin_us = 150 +host_present_from_non_ui_thread = true +presenter_strict_guest_output_backpressure = false +presenter_strict_guest_output_backpressure_timeout_ms = 100 + +present_letterbox = true +present_safe_area_x = 100 +present_safe_area_y = 100 +present_effect = "bilinear" + +readback_memexport = false +anisotropic_override = -1 +texture_cache_memory_limit_render_to_texture = 128 +texture_cache_memory_limit_soft = 2048 +texture_cache_memory_limit_hard = 4096 +texture_cache_memory_limit_soft_lifetime = 300 +gpu_allow_invalid_fetch_constants = true +clear_memory_page_state = false +execute_unclipped_draw_vs_on_cpu = true +occlusion_query_enable = true + +advanced_settings_overlay_enabled = true + +selected_user_profile = "player" +user_profile_name = "Player" +user_profile_xuid = "B13E07DFF9AB6772" +user_profile_signed_in = true +user_live_signed_in = false + +game_data_root = "" diff --git a/src/skate3_iso_installer.cpp b/src/skate3_iso_installer.cpp new file mode 100644 index 0000000..01f056e --- /dev/null +++ b/src/skate3_iso_installer.cpp @@ -0,0 +1,462 @@ +#include "skate3_iso_installer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#include + +#include +#else +#include +#endif + +namespace skate3 { +namespace { + +constexpr uint64_t kSectorSize = 2048; +constexpr std::array kPossibleGameOffsets = { + 0x00000000ull, 0x0000FB20ull, 0x00020600ull, 0x02080000ull, 0x0FD90000ull}; +constexpr std::string_view kXdvdfsMagic = "MICROSOFT*XBOX*MEDIA"; +constexpr std::string_view kDefaultXex = "default.xex"; + +uint16_t ReadLe16(const uint8_t* p) { + return static_cast(p[0]) | (static_cast(p[1]) << 8); +} + +uint32_t ReadLe32(const uint8_t* p) { + return static_cast(p[0]) | (static_cast(p[1]) << 8) | + (static_cast(p[2]) << 16) | (static_cast(p[3]) << 24); +} + +std::string ToLower(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return value; +} + +bool IsUnsafeIsoPath(std::string_view path) { + if (path.empty() || path.starts_with('/') || path.starts_with('\\')) { + return true; + } + size_t start = 0; + while (start <= path.size()) { + size_t end = path.find('/', start); + if (end == std::string_view::npos) { + end = path.size(); + } + auto component = path.substr(start, end - start); + if (component.empty() || component == "." || component == "..") { + return true; + } + if (end == path.size()) { + break; + } + start = end + 1; + } + return false; +} + +#if defined(_WIN32) +std::filesystem::path PickIsoFile() { + wchar_t filename[MAX_PATH] = {}; + OPENFILENAMEW ofn{}; + ofn.lStructSize = sizeof(ofn); + ofn.hwndOwner = GetActiveWindow(); + ofn.lpstrFile = filename; + ofn.nMaxFile = static_cast(std::size(filename)); + ofn.lpstrFilter = L"Xbox 360 ISO (*.iso)\0*.iso\0All files (*.*)\0*.*\0"; + ofn.lpstrTitle = L"Select Skate 3 Xbox 360 ISO"; + ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR | + OFN_DONTADDTORECENT; + if (!GetOpenFileNameW(&ofn)) { + return {}; + } + return filename; +} +#else +std::filesystem::path PickIsoFile() { + GtkWidget* dialog = gtk_file_chooser_dialog_new( + "Select Skate 3 Xbox 360 ISO", nullptr, GTK_FILE_CHOOSER_ACTION_OPEN, "_Cancel", + GTK_RESPONSE_CANCEL, "_Open", GTK_RESPONSE_ACCEPT, nullptr); + if (!dialog) { + return {}; + } + + GtkFileFilter* iso_filter = gtk_file_filter_new(); + gtk_file_filter_set_name(iso_filter, "Xbox 360 ISO (*.iso)"); + gtk_file_filter_add_pattern(iso_filter, "*.iso"); + gtk_file_filter_add_pattern(iso_filter, "*.ISO"); + gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), iso_filter); + + 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 + +struct IsoEntry { + std::string path; + uint64_t offset = 0; + uint64_t size = 0; +}; + +class XboxIsoReader { + public: + bool Open(const std::filesystem::path& path, std::string& error) { + iso_path_ = path; + file_.open(path, std::ios::binary); + if (!file_) { + error = "Unable to open the selected ISO."; + return false; + } + + file_.seekg(0, std::ios::end); + file_size_ = static_cast(file_.tellg()); + file_.seekg(0, std::ios::beg); + + uint64_t game_offset = 0; + bool found_magic = false; + std::array magic{}; + for (uint64_t candidate : kPossibleGameOffsets) { + const uint64_t magic_offset = candidate + 32 * kSectorSize; + if (magic_offset + magic.size() > file_size_) { + continue; + } + file_.seekg(static_cast(magic_offset), std::ios::beg); + file_.read(magic.data(), static_cast(magic.size())); + if (std::string_view(magic.data(), magic.size()) == kXdvdfsMagic) { + game_offset = candidate; + found_magic = true; + break; + } + } + + if (!found_magic) { + error = "The selected file is not a recognized Xbox 360 game ISO."; + return false; + } + + std::array root_info{}; + const uint64_t root_info_offset = game_offset + 32 * kSectorSize + 20; + if (!ReadAt(root_info_offset, root_info.data(), root_info.size())) { + error = "The ISO root directory could not be read."; + return false; + } + + const uint32_t root_sector = ReadLe32(root_info.data()); + const uint32_t root_size = ReadLe32(root_info.data() + 4); + if (root_size < 13 || root_size > 32 * 1024 * 1024) { + error = "The ISO root directory is invalid."; + return false; + } + + entries_.clear(); + if (!ParseDirectory("", game_offset, game_offset + uint64_t(root_sector) * kSectorSize, error)) { + return false; + } + + if (!HasFile(kDefaultXex)) { + error = "The ISO does not contain default.xex."; + return false; + } + + return true; + } + + bool HasFile(std::string_view path) const { + const auto wanted = ToLower(std::string(path)); + return std::any_of(entries_.begin(), entries_.end(), [&](const IsoEntry& entry) { + return ToLower(entry.path) == wanted; + }); + } + + uint64_t TotalSize() const { + uint64_t total = 0; + for (const auto& entry : entries_) { + total += entry.size; + } + return total; + } + + bool ExtractAll(const std::filesystem::path& target_root, std::atomic& copied_bytes, + std::string& error) { + std::error_code ec; + std::filesystem::create_directories(target_root, ec); + if (ec) { + error = "Unable to create the game directory."; + return false; + } + + std::vector buffer(4 * 1024 * 1024); + for (const auto& entry : entries_) { + if (IsUnsafeIsoPath(entry.path)) { + error = "The ISO contains an unsafe file path."; + return false; + } + + const auto target = target_root / std::filesystem::path(entry.path); + std::filesystem::create_directories(target.parent_path(), ec); + if (ec) { + error = "Unable to create an install subdirectory."; + return false; + } + + std::ofstream out(target, std::ios::binary | std::ios::trunc); + if (!out) { + error = "Unable to create " + entry.path + "."; + return false; + } + + uint64_t remaining = entry.size; + uint64_t read_offset = entry.offset; + while (remaining > 0) { + const size_t chunk = static_cast(std::min(remaining, buffer.size())); + if (!ReadAt(read_offset, buffer.data(), chunk)) { + error = "Failed to read " + entry.path + " from the ISO."; + return false; + } + out.write(reinterpret_cast(buffer.data()), static_cast(chunk)); + if (!out) { + error = "Failed to write " + entry.path + "."; + return false; + } + remaining -= chunk; + read_offset += chunk; + copied_bytes.fetch_add(chunk, std::memory_order_relaxed); + } + } + + return true; + } + + private: + bool ReadAt(uint64_t offset, void* data, size_t size) { + if (offset + size > file_size_) { + return false; + } + file_.seekg(static_cast(offset), std::ios::beg); + file_.read(reinterpret_cast(data), static_cast(size)); + return file_.good(); + } + + bool ParseDirectory(const std::string& prefix, uint64_t game_offset, uint64_t directory_offset, + std::string& error) { + struct PendingNode { + uint64_t directory_offset = 0; + uint32_t node_offset = 0; + std::string prefix; + }; + + std::vector pending; + pending.push_back({directory_offset, 0, prefix}); + std::array header{}; + size_t visited = 0; + + while (!pending.empty()) { + auto node = std::move(pending.back()); + pending.pop_back(); + if (++visited > 500000) { + error = "The ISO directory tree is unexpectedly large."; + return false; + } + + const uint64_t entry_offset = node.directory_offset + node.node_offset; + if (!ReadAt(entry_offset, header.data(), header.size())) { + error = "Failed to read an ISO directory entry."; + return false; + } + + const uint16_t left = ReadLe16(header.data()); + const uint16_t right = ReadLe16(header.data() + 2); + const uint32_t sector = ReadLe32(header.data() + 4); + const uint32_t length = ReadLe32(header.data() + 8); + const uint8_t attributes = header[12]; + const uint8_t name_length = header[13]; + if (name_length == 0 || name_length > 240) { + error = "The ISO contains an invalid directory entry name."; + return false; + } + + std::string name(name_length, '\0'); + if (!ReadAt(entry_offset + header.size(), name.data(), name.size())) { + error = "Failed to read an ISO directory entry name."; + return false; + } + + if (left) { + pending.push_back({node.directory_offset, static_cast(left) * 4u, node.prefix}); + } + if (right) { + pending.push_back({node.directory_offset, static_cast(right) * 4u, node.prefix}); + } + + const bool is_directory = (attributes & 0x10) != 0; + const std::string full_path = node.prefix + name; + if (is_directory) { + if (length != 0) { + pending.push_back({game_offset + uint64_t(sector) * kSectorSize, 0, full_path + "/"}); + } + } else { + entries_.push_back({full_path, game_offset + uint64_t(sector) * kSectorSize, length}); + } + } + + return true; + } + + std::filesystem::path iso_path_; + std::ifstream file_; + uint64_t file_size_ = 0; + std::vector entries_; +}; + +} // namespace + +bool IsGameInstalled(const std::filesystem::path& game_root) { + return std::filesystem::is_regular_file(game_root / std::string(kDefaultXex)); +} + +bool RunRexglueIsoInstallWizardBlocking(rex::ui::WindowedAppContext& app_context, + rex::ui::Window* window, + rex::ui::ImGuiDrawer* drawer, + rex::PathConfig runtime_paths, + rex::PathConfig& installed_paths) { + struct InstallResult { + bool done = false; + bool ok = false; + rex::PathConfig paths; + }; + + auto result = std::make_shared(); + auto pick_source = []() { return PickIsoFile(); }; + auto install = [game_root = runtime_paths.game_data_root]( + const std::filesystem::path& source, std::atomic& copied_bytes, + std::atomic& total_bytes, std::string& error) { + XboxIsoReader iso; + if (!iso.Open(source, error)) { + return false; + } + total_bytes = iso.TotalSize(); + if (!iso.ExtractAll(game_root, copied_bytes, error)) { + return false; + } + if (!IsGameInstalled(game_root)) { + error = "Installation completed, but default.xex was not found in the install directory."; + return false; + } + return true; + }; + + if (const char* automated_iso = std::getenv("SKATE3_INSTALL_ISO"); + automated_iso != nullptr && *automated_iso != '\0') { + std::atomic copied_bytes{0}; + std::atomic total_bytes{0}; + std::string error; + REXLOG_INFO("Installing Skate 3 game files from SKATE3_INSTALL_ISO={}", automated_iso); + if (!install(std::filesystem::path(automated_iso), copied_bytes, total_bytes, error)) { + REXLOG_ERROR("Automated ISO installation failed: {}", error); + return false; + } + installed_paths = std::move(runtime_paths); + REXLOG_INFO("Automated ISO installation completed successfully"); + return true; + } + + new rex::ui::InstallWizardDialog( + drawer, "Skate 3 Setup", + "Skate 3 game files were not found. Select your Xbox 360 ISO to install them.", + runtime_paths.game_data_root.string(), std::move(pick_source), std::move(install), + [result, runtime_paths = std::move(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(window)) { + hwnd = win32_window->hwnd(); + } +#endif + + REXLOG_INFO("Entering rexglue ISO 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(); + } + while (gtk_events_pending()) { + gtk_main_iteration_do(FALSE); + } +#endif + std::this_thread::sleep_for(std::chrono::milliseconds(16)); + } + + if (!result->ok) { + REXLOG_INFO("Leaving rexglue ISO installer pump without installation"); + return false; + } + + installed_paths = std::move(result->paths); + REXLOG_INFO("Leaving rexglue ISO installer pump after successful installation"); + return true; +} + +} // namespace skate3 diff --git a/src/skate3_iso_installer.h b/src/skate3_iso_installer.h new file mode 100644 index 0000000..3019d81 --- /dev/null +++ b/src/skate3_iso_installer.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include + +namespace skate3 { + +bool IsGameInstalled(const std::filesystem::path& game_root); +bool RunRexglueIsoInstallWizardBlocking(rex::ui::WindowedAppContext& app_context, + rex::ui::Window* window, + rex::ui::ImGuiDrawer* drawer, + rex::PathConfig runtime_paths, + rex::PathConfig& installed_paths); + +} // namespace skate3