From 7641af32522d2ed287ffbf8cde25596925776343 Mon Sep 17 00:00:00 2001 From: mchughalex <25671999+mchughalex@users.noreply.github.com> Date: Mon, 8 Jun 2026 08:41:04 +0100 Subject: [PATCH] add DLC package installation support --- README.md | 11 +++ src/skate3_app_common.cpp | 176 ++++++++++++++++++++++++++++++++++++++ src/skate3_app_common.h | 1 + 3 files changed, 188 insertions(+) diff --git a/README.md b/README.md index 01d4fd6..f91be20 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,17 @@ Gameplay (click to go to YouTube): 6. Wait for the installer to extract the game files. 7. Click "Start Game". +### DLC + +Skate3Recomp does not include Skate 3 DLC files. To use DLC, provide package +files from your own legally obtained Xbox 360 DLC. + +Create a `dlc` folder beside the executable, inside the installed game folder, +or in the Skate3Recomp user data folder. Place the DLC package files in that +folder and start the game. Skate3Recomp will validate matching Skate 3 +marketplace packages and install them into the local content store +automatically. + ## Controls - Standard Xbox controls using an Xbox controller are the preferred and main input method. DualShock and others are untested, but are likely to work with Steam Input through XInput. diff --git a/src/skate3_app_common.cpp b/src/skate3_app_common.cpp index 32b2bed..f93b772 100644 --- a/src/skate3_app_common.cpp +++ b/src/skate3_app_common.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #if defined(_WIN32) @@ -35,6 +36,7 @@ #include #include +#include #include #include #include @@ -45,6 +47,8 @@ #include #include #include +#include +#include #include #include #include @@ -63,6 +67,11 @@ extern const rex::PPCImageInfo eawebkit_PPCImageConfig; // lift via config because they overlap with auto-discovered parent functions. extern "C" REX_FUNC(__restgprlr_19); +REXCVAR_DEFINE_STRING(skate3_dlc_root, "", "Skate 3", + "Directory containing Skate 3 DLC package files"); +REXCVAR_DEFINE_BOOL(skate3_auto_install_dlc, true, "Skate 3", + "Install DLC package files found in configured DLC folders"); + namespace { #if defined(__linux__) || defined(__APPLE__) @@ -127,6 +136,7 @@ void SetRestartArgument(std::vector& args, std::string name, std::s constexpr std::string_view kUserDirectoryName = "skate3"; constexpr std::string_view kSettingsFilename = "settings.toml"; +constexpr std::string_view kDlcDirectoryName = "dlc"; std::filesystem::path DefaultDocumentsUserRoot() { return rex::filesystem::GetUserFolder() / std::string(kUserDirectoryName); @@ -256,6 +266,66 @@ const char* FirstExistingFontPath(std::initializer_list paths) { return nullptr; } +std::string Hex8(uint32_t value) { + std::ostringstream stream; + stream << std::uppercase << std::hex << std::setw(8) << std::setfill('0') << value; + return stream.str(); +} + +std::filesystem::path InstalledMarketplaceContentPath(const std::filesystem::path& content_root, + uint32_t title_id, + const std::filesystem::path& package_path) { + return content_root / "0000000000000000" / Hex8(title_id) / "00000002" / + package_path.filename(); +} + +std::filesystem::path InstalledMarketplaceHeaderPath(const std::filesystem::path& content_root, + uint32_t title_id, + const std::filesystem::path& package_path) { + return content_root / "0000000000000000" / Hex8(title_id) / "Headers" / "00000002" / + (package_path.filename().string() + ".header"); +} + +bool IsInstalledMarketplaceContent(const std::filesystem::path& content_root, uint32_t title_id, + const std::filesystem::path& package_path) { + return std::filesystem::is_directory( + InstalledMarketplaceContentPath(content_root, title_id, package_path)) && + std::filesystem::is_regular_file( + InstalledMarketplaceHeaderPath(content_root, title_id, package_path)); +} + +std::vector DiscoverDlcSourceDirectories( + const std::filesystem::path& executable_root, const std::filesystem::path& game_data_root, + const std::filesystem::path& user_data_root) { + std::vector dirs; + auto add_dir = [&](std::filesystem::path dir) { + if (dir.empty()) { + return; + } + std::error_code ec; + dir = std::filesystem::absolute(dir, ec); + if (ec) { + return; + } + for (const auto& existing : dirs) { + if (std::filesystem::equivalent(existing, dir, ec)) { + return; + } + ec.clear(); + } + dirs.push_back(std::move(dir)); + }; + + const std::string configured_root = REXCVAR_GET(skate3_dlc_root); + if (!configured_root.empty()) { + add_dir(configured_root); + } + add_dir(executable_root / std::string(kDlcDirectoryName)); + add_dir(game_data_root / std::string(kDlcDirectoryName)); + add_dir(user_data_root / std::string(kDlcDirectoryName)); + return dirs; +} + } // namespace // sub_82EBAE4C: alternate entry into sub_82EBAE34 (37 vtable refs in .data). @@ -411,6 +481,7 @@ void Skate3BaseApp::OnPostSetup() { if (std::getenv("SKATE3_DISABLE_BIG_ALIASES") == nullptr) { InstallBigDeviceAliases(); } + InstallDlcPackages(); InstallRecipeOverlay(); // Register multi-entry-function alternate entries. @@ -828,3 +899,108 @@ void Skate3BaseApp::InstallBigDeviceAliases() { "dlcbig:", "\\Device\\Harddisk0\\Partition1"); big_device_aliases_installed_ = true; } + +void Skate3BaseApp::InstallDlcPackages() { + if (!REXCVAR_GET(skate3_auto_install_dlc) || !runtime() || !runtime()->kernel_state() || + !runtime()->kernel_state()->content_manager()) { + return; + } + + const uint32_t title_id = runtime()->kernel_state()->title_id(); + if (title_id == 0) { + REXLOG_WARN("Skipping Skate 3 DLC install; title ID is not available"); + return; + } + + const auto user_dlc_root = runtime()->user_data_root() / std::string(kDlcDirectoryName); + std::error_code create_ec; + std::filesystem::create_directories(user_dlc_root, create_ec); + if (create_ec) { + REXLOG_WARN("Could not create Skate 3 DLC drop folder {}: {}", user_dlc_root.string(), + create_ec.message()); + } + + const auto source_dirs = + DiscoverDlcSourceDirectories(rex::filesystem::GetExecutableFolder(), + runtime()->game_data_root(), runtime()->user_data_root()); + std::unordered_set seen_packages; + size_t installed_count = 0; + size_t skipped_count = 0; + + for (const auto& source_dir : source_dirs) { + if (!std::filesystem::is_directory(source_dir)) { + continue; + } + + std::error_code iter_ec; + for (const auto& entry : std::filesystem::directory_iterator(source_dir, iter_ec)) { + if (iter_ec) { + REXLOG_WARN("Could not scan Skate 3 DLC folder {}: {}", source_dir.string(), + iter_ec.message()); + break; + } + if (!entry.is_regular_file()) { + continue; + } + + const auto package_path = entry.path(); + std::error_code canonical_ec; + auto package_key = std::filesystem::weakly_canonical(package_path, canonical_ec).string(); + if (canonical_ec) { + package_key = std::filesystem::absolute(package_path).string(); + } + if (!seen_packages.insert(package_key).second) { + continue; + } + + const auto header = rex::filesystem::StfsContainerDevice::ReadPackageHeader(package_path); + if (!header) { + REXLOG_WARN("Skipping DLC candidate with invalid STFS header: {}", package_path.string()); + ++skipped_count; + continue; + } + + const auto content_type = + static_cast(header->metadata.content_type); + if (content_type != rex::system::XContentType::kMarketplaceContent) { + REXLOG_WARN("Skipping non-DLC content package {} with type {:08X}", + package_path.filename().string(), static_cast(content_type)); + ++skipped_count; + continue; + } + + const uint32_t package_title_id = header->metadata.execution_info.title_id; + if (package_title_id != 0 && package_title_id != title_id) { + REXLOG_WARN("Skipping DLC package {} for title {:08X}; running title is {:08X}", + package_path.filename().string(), package_title_id, title_id); + ++skipped_count; + continue; + } + + if (IsInstalledMarketplaceContent(runtime()->user_data_root(), title_id, package_path)) { + REXLOG_INFO("DLC package already installed: {}", package_path.filename().string()); + ++skipped_count; + continue; + } + + REXLOG_INFO("Installing Skate 3 DLC package: {}", package_path.string()); + const auto result = runtime()->kernel_state()->content_manager()->InstallContent(package_path); + if (result == 0) { + ++installed_count; + REXLOG_INFO("Installed Skate 3 DLC package: {}", package_path.filename().string()); + } else { + ++skipped_count; + REXLOG_WARN("Failed to install Skate 3 DLC package {}: {:08X}", + package_path.filename().string(), static_cast(result)); + } + } + } + + if (installed_count || skipped_count) { + REXLOG_INFO("Skate 3 DLC scan complete: {} installed, {} skipped", installed_count, + skipped_count); + } else { + REXLOG_INFO("No Skate 3 DLC packages found. Drop legally obtained DLC package files in {}", + user_dlc_root.string()); + } +} diff --git a/src/skate3_app_common.h b/src/skate3_app_common.h index e0f95ee..da905e4 100644 --- a/src/skate3_app_common.h +++ b/src/skate3_app_common.h @@ -35,6 +35,7 @@ class Skate3BaseApp : public rex::ReXApp { private: void InstallRecipeOverlay(); void InstallBigDeviceAliases(); + void InstallDlcPackages(); void ToggleSimpleSettings(); void ApplySettingsCursorMode(); void ApplyGameplayCursorMode();