add DLC package installation support

This commit is contained in:
mchughalex 2026-06-08 08:41:04 +01:00
parent 78a1b71ce4
commit 7641af3252
3 changed files with 188 additions and 0 deletions

View file

@ -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.

View file

@ -16,6 +16,7 @@
#include <string>
#include <string_view>
#include <thread>
#include <unordered_set>
#include <vector>
#if defined(_WIN32)
@ -35,6 +36,7 @@
#include <rex/cvar.h>
#include <rex/filesystem.h>
#include <rex/filesystem/devices/stfs_container_device.h>
#include <rex/filesystem/devices/host_path_device.h>
#include <rex/filesystem/vfs.h>
#include <rex/input/input_system.h>
@ -45,6 +47,8 @@
#include <rex/ppc/context.h>
#include <rex/system/function_dispatcher.h>
#include <rex/system/kernel_state.h>
#include <rex/system/xam/content_device.h>
#include <rex/system/xam/content_manager.h>
#include <rex/system/xam/user_profile.h>
#include <rex/system.h>
#include <rex/ui/keybinds.h>
@ -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<std::string>& 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<const char*> 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<std::filesystem::path> DiscoverDlcSourceDirectories(
const std::filesystem::path& executable_root, const std::filesystem::path& game_data_root,
const std::filesystem::path& user_data_root) {
std::vector<std::filesystem::path> 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<std::string> 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<rex::system::XContentType>(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<uint32_t>(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<uint32_t>(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());
}
}

View file

@ -35,6 +35,7 @@ class Skate3BaseApp : public rex::ReXApp {
private:
void InstallRecipeOverlay();
void InstallBigDeviceAliases();
void InstallDlcPackages();
void ToggleSimpleSettings();
void ApplySettingsCursorMode();
void ApplyGameplayCursorMode();