fix: keep generated defaults portable with conditional resources

This commit is contained in:
boburning
2026-10-23 21:32:43 -04:00
committed by Matthew Stanley
parent e5d92035a9
commit b3abd6df01
3 changed files with 147 additions and 0 deletions
+29
View File
@@ -3475,6 +3475,35 @@ bool ModPackageManager::feature_resource_active(
feature_id, resource->when);
}
bool ModPackageManager::set_generated_feature_resource_default(
const std::string& package_id, const std::string& feature_id,
const std::string& resource_id, const fs::path& relative_path,
std::string* error) {
const auto* package = selected_package(package_id);
const auto* resource = package ? find_resource(*package, feature_id, resource_id) : nullptr;
if (!resource || !resource->shared_source.empty() || relative_path.empty() ||
relative_path.is_absolute() || relative_path.has_root_name() || relative_path.has_root_directory()) {
set_error(error, "invalid generated resource default");
return false;
}
for (const auto& component : relative_path) if (component == "." || component == ".." || component.empty()) {
set_error(error, "generated resource default must stay relative to mods root");
return false;
}
std::error_code ec;
const auto path = fs::absolute(root_ / relative_path, ec);
if (ec) { set_error(error, ec.message()); return false; }
generated_resource_defaults_[{package_id, feature_id, resource_id}] = path;
return true;
}
fs::path ModPackageManager::generated_resource_default(
const std::string& package_id, const std::string& feature_id,
const std::string& resource_id) const {
const auto found = generated_resource_defaults_.find({package_id, feature_id, resource_id});
return found == generated_resource_defaults_.end() ? fs::path() : found->second;
}
fs::path ModPackageManager::feature_resource_path(
const std::string& package_id,
const std::string& feature_id,
+39
View File
@@ -2569,6 +2569,45 @@ int main() {
check(!ModPackageManager::read_manifest(a,malformed,&error),"duplicate default roster rejected");
}
{
const auto old_root = root / "conditional-default-old";
const auto new_root = root / "conditional-default-new";
write_text(old_root / "bundled/conditional-default/1.0.0/manifest.toml",
"format_version = 10\nid = \"conditional-default\"\nversion = \"1.0.0\"\nname = \"Conditional default\"\n"
"[[target]]\ngame_id = \"SLUS-TEST\"\n"
"[[feature]]\nid = \"media\"\nname = \"Media\"\ndefault_enabled = true\n"
"[[option]]\nfeature = \"media\"\nid = \"language\"\nlabel = \"Language\"\n"
"type = \"choice\"\ndefault = \"en\"\n"
"[[option.choice]]\nvalue = \"en\"\nlabel = \"English\"\n"
"[[option.choice]]\nvalue = \"es\"\nlabel = \"Spanish\"\n"
"[[resource]]\nfeature = \"media\"\nid = \"english\"\nlabel = \"English\"\nrequired = true\nwhen = { language = \"en\" }\n"
"[[resource]]\nfeature = \"media\"\nid = \"spanish\"\nlabel = \"Spanish\"\nrequired = true\nwhen = { language = \"es\" }\n");
write_text(old_root / "english.dat", "English");
const auto register_defaults = [&](ModPackageManager& manager) {
check(manager.set_generated_feature_resource_default("conditional-default", "media", "english", "english.dat", &error), error.c_str());
check(manager.set_generated_feature_resource_default("conditional-default", "media", "spanish", "spanish.dat", &error), error.c_str());
};
ModPackageManager original(old_root);
check(original.scan(&error), error.c_str());
register_defaults(original);
check(original.resolve("SLUS-TEST").ok, "inactive missing generated default is not required");
fs::rename(old_root, new_root, ec);
check(!ec && !fs::exists(old_root), "old conditional default installation is unavailable");
ModPackageManager relocated(new_root);
check(relocated.scan(&error), error.c_str());
register_defaults(relocated);
const auto english = relocated.resolve("SLUS-TEST");
check(english.ok && english.resources.size() == 1 && english.resources[0].id == "english" &&
english.resources[0].path == fs::absolute(new_root / "english.dat"),
"only the selected generated default resolves at the moved root");
write_text(new_root / "spanish.dat", "Spanish");
check(relocated.set_feature_option("conditional-default", "media", "language", "es", &error), error.c_str());
const auto spanish = relocated.resolve("SLUS-TEST");
check(spanish.ok && spanish.resources.size() == 1 && spanish.resources[0].id == "spanish" &&
spanish.resources[0].path == fs::absolute(new_root / "spanish.dat"),
"changing the option selects the other relocated generated default");
}
fs::remove_all(root, ec);
if (failures) {
std::cerr << failures << " mod package test(s) failed\n";
+79
View File
@@ -2630,6 +2630,85 @@ id = "override.test"
"changing dropdown replaces the picker instead of exposing both");
}
#endif
{
const auto a = root / "portable-default-a";
const auto b = root / "portable-default-b";
const auto manifest = a / "bundled/portable.hd/1.0.0/manifest.toml";
write_text(manifest,
"format_version = 5\nid = \"portable.hd\"\nversion = \"1.0.0\"\nname = \"Portable HD fixture\"\nresolver = \"declarative\"\nsave_compatibility = \"shared\"\n"
"[[target]]\ngame_id = \"SLUS-PORTABLE\"\n"
"[[feature]]\nid = \"hd\"\nname = \"HD\"\ndefault_enabled = false\n"
"[[resource]]\nfeature = \"hd\"\nid = \"pack\"\nlabel = \"Pack\"\nformat = \"directory\"\nrequired = true\n"
"[[option]]\nfeature = \"hd\"\nid = \"replacements\"\nlabel = \"Load\"\ntype = \"boolean\"\ndefault = \"true\"\n"
"[[option]]\nfeature = \"hd\"\nid = \"dump\"\nlabel = \"Dump\"\ntype = \"boolean\"\ndefault = \"false\"\n"
"[[plugin]]\nfeature = \"hd\"\nid = \"psx.hd-textures\"\n");
check(PSXRecompV4::mod_runtime_initialize(a, "SLUS-PORTABLE", 0x80002000, {}, &error), error.c_str());
check(fs::is_directory(a / "texture-packs/SLUS-PORTABLE/replacements"), "disabled default was actually initialized");
check(PSXRecompV4::mod_runtime_commit({}, &error), error.c_str());
fs::copy(a, b, fs::copy_options::recursive, ec);
check(!ec, "copy default-disabled fixture to relocated root");
fs::rename(a, root / "unavailable-default-a", ec);
check(!ec && !fs::exists(a), "old install root is unavailable");
check(PSXRecompV4::mod_runtime_initialize(b, "SLUS-PORTABLE", 0x80002000, {}, &error), error.c_str());
#if defined(RECOMP_LAUNCHER)
const auto* portable_provider = PSXRecompV4::mod_runtime_launcher_provider();
RecompLauncherCModResource portable_resource{};
check(portable_provider && portable_provider->feature_resource_get(portable_provider->ctx,
"portable.hd", "hd", 0, &portable_resource) &&
fs::path(portable_resource.path) == fs::absolute(b / "texture-packs/SLUS-PORTABLE"),
"live launcher getter exposes exact relocated B default while disabled");
#endif
check(PSXRecompV4::mod_runtime_commit({}, &error), error.c_str());
PSXRecompV4::ModPackageManager readback(b);
check(readback.scan(&error) && readback.load_state(&error), error.c_str());
check(readback.selected_package("portable.hd") != nullptr, "portable fixture package remains selected");
const auto retained = readback.feature_resource_path("portable.hd", "hd", "pack");
check(retained.empty() || retained.lexically_relative(a).empty() ||
*retained.lexically_relative(a).begin() == "..",
"disabled generated default must not retain unavailable install root");
check(!fs::exists(a), "relocation does not recreate unavailable old root");
check(readback.set_feature_enabled("portable.hd", "hd", true, &error) && readback.save_state(&error), error.c_str());
check(PSXRecompV4::mod_runtime_initialize(b, "SLUS-PORTABLE", 0x80002000, {}, &error), error.c_str());
const auto before_cwd = fs::current_path();
fs::current_path(root);
check(PSXRecompV4::mod_runtime_commit({}, &error), error.c_str());
mod_runtime_activate_plugins();
check(fs::path(hd_root) == fs::absolute(b / "texture-packs/SLUS-PORTABLE"),
"enabled relocated plugin receives B default despite changed cwd");
fs::current_path(before_cwd);
PSXRecompV4::ModPackageManager generated(b);
check(generated.scan(&error) && generated.load_state(&error), error.c_str());
check(generated.set_generated_feature_resource_default("portable.hd", "hd", "pack",
"texture-packs/SLUS-PORTABLE", &error), error.c_str());
const auto expected = fs::absolute(b / "texture-packs/SLUS-PORTABLE");
check(generated.feature_resource_path("portable.hd", "hd", "pack") == expected,
"manager getter returns exact relocated default");
const auto resolved = generated.resolve("SLUS-PORTABLE");
check(resolved.ok && resolved.resources.size() == 1 && resolved.resources[0].path == expected,
"committed resolver and getter agree on B");
for (const auto& invalid : {fs::path("../escape"), fs::path("./escape"), expected})
check(!generated.set_generated_feature_resource_default("portable.hd", "hd", "pack", invalid, &error),
"generated defaults reject rooted or traversal paths");
const auto external = root / "explicit-external-pack";
fs::create_directories(external);
check(generated.set_feature_resource_path("portable.hd", "hd", "pack", external, &error) &&
generated.save_state(&error), error.c_str());
fs::rename(external, root / "unavailable-external-pack", ec);
check(!ec, "explicit external fixture made unavailable");
PSXRecompV4::ModPackageManager legacy(b);
check(legacy.scan(&error) && legacy.load_state(&error) &&
legacy.set_generated_feature_resource_default("portable.hd", "hd", "pack",
"texture-packs/SLUS-PORTABLE", &error), error.c_str());
check(legacy.feature_resource_path("portable.hd", "hd", "pack") == external,
"missing legacy absolute user selection remains unchanged");
const auto missing_plan = legacy.resolve("SLUS-PORTABLE");
check(missing_plan.resources.size() == 1 && missing_plan.resources[0].path == external,
"resolver preserves missing selection for existing downstream validation");
}
fs::remove_all(root, ec);
if (failures) return 1;
std::cout << "mod runtime tests passed\n";