mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Merge pull request #685 from boburning/fix/portable-generated-texture-defaults
Keep runtime-generated texture-pack defaults portable across installs
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
namespace PSXRecompV4 {
|
||||
@@ -530,6 +531,14 @@ public:
|
||||
std::string feature_option_value(const std::string& package_id,
|
||||
const std::string& feature_id,
|
||||
const std::string& option_id) const;
|
||||
/* Runtime-derived defaults are not player selections and are never saved.
|
||||
* relative_path is rooted at this manager's mods root, captured as an
|
||||
* absolute path at registration so subsequent cwd changes cannot rebase it.
|
||||
* Existing explicit selections (including legacy absolute paths) win. */
|
||||
bool set_generated_feature_resource_default(const std::string& package_id,
|
||||
const std::string& feature_id, const std::string& resource_id,
|
||||
const std::filesystem::path& relative_path, std::string* error = nullptr);
|
||||
|
||||
std::filesystem::path feature_resource_path(
|
||||
const std::string& package_id,
|
||||
const std::string& feature_id,
|
||||
@@ -588,6 +597,10 @@ private:
|
||||
std::vector<ModResolution::Diagnostic>* diagnostics,
|
||||
std::vector<std::string>* errors) const;
|
||||
|
||||
std::filesystem::path generated_resource_default(const std::string& package_id,
|
||||
const std::string& feature_id, const std::string& resource_id) const;
|
||||
std::map<std::tuple<std::string, std::string, std::string>, std::filesystem::path>
|
||||
generated_resource_defaults_;
|
||||
std::filesystem::path root_;
|
||||
bool developer_channel_ = kDeveloperChannelDefault;
|
||||
std::vector<std::string> scan_errors_;
|
||||
|
||||
@@ -885,7 +885,8 @@ static fs::path package_default_path(const ModPackage& package, const ModResourc
|
||||
fs::path effective_resource_path(const ModPackage& package,
|
||||
const ModSelection& selection,
|
||||
const std::string& feature_id,
|
||||
const std::string& id) {
|
||||
const std::string& id,
|
||||
const fs::path& generated_default = {}) {
|
||||
const ModFeature* feature = find_feature(package, feature_id);
|
||||
if (!feature || feature->legacy) return {};
|
||||
const ModFeatureSelection* selected =
|
||||
@@ -898,7 +899,7 @@ fs::path effective_resource_path(const ModPackage& package,
|
||||
const ModResource* declared = find_resource(package, feature_id, id);
|
||||
if (declared && !declared->default_path.empty() && !package.root.empty())
|
||||
return package_default_path(package, *declared);
|
||||
return {};
|
||||
return generated_default;
|
||||
}
|
||||
|
||||
bool prospective_feature_enabled(
|
||||
@@ -1724,6 +1725,7 @@ void ModPackageManager::set_root(fs::path mods_root) {
|
||||
packages_.clear();
|
||||
selections_.clear();
|
||||
sources_.clear();
|
||||
generated_resource_defaults_.clear();
|
||||
}
|
||||
|
||||
bool ModPackageManager::read_manifest(const fs::path& path, ModPackage& out,
|
||||
@@ -3344,6 +3346,35 @@ std::string ModPackageManager::feature_option_value(
|
||||
feature_id, option_id);
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -3363,7 +3394,8 @@ fs::path ModPackageManager::feature_resource_path(
|
||||
const auto selected = selections_.find(id);
|
||||
if (selected == selections_.end()) continue;
|
||||
for (const auto& r : other->resources) if(r.shared_source == resource->shared_source) {
|
||||
auto path = effective_resource_path(*other, selected->second, r.feature_id, r.id);
|
||||
auto path = effective_resource_path(*other, selected->second, r.feature_id, r.id,
|
||||
generated_resource_default(other->id, r.feature_id, r.id));
|
||||
if (!path.empty()) return path;
|
||||
}
|
||||
}
|
||||
@@ -3373,7 +3405,8 @@ fs::path ModPackageManager::feature_resource_path(
|
||||
const ModSelection blank;
|
||||
return effective_resource_path(
|
||||
*package, found == selections_.end() ? blank : found->second,
|
||||
feature_id, resource_id);
|
||||
feature_id, resource_id,
|
||||
generated_resource_default(package_id, feature_id, resource_id));
|
||||
}
|
||||
|
||||
std::map<std::string, ModSelection> ModPackageManager::effective_selections(
|
||||
@@ -4036,7 +4069,8 @@ ModResolution ModPackageManager::resolve(const std::string& game_id,
|
||||
!is_feature_enabled(*package, selected, *feature))
|
||||
continue;
|
||||
const fs::path path = effective_resource_path(
|
||||
*package, selected, resource.feature_id, resource.id);
|
||||
*package, selected, resource.feature_id, resource.id,
|
||||
generated_resource_default(package->id, resource.feature_id, resource.id));
|
||||
if (path.empty()) {
|
||||
if (resource.required) {
|
||||
result.errors.push_back(
|
||||
|
||||
@@ -1583,15 +1583,19 @@ bool mod_runtime_initialize(const std::filesystem::path& root,
|
||||
});
|
||||
if (resource == package->resources.end()) continue;
|
||||
std::error_code ec;
|
||||
const auto pack = std::filesystem::absolute(
|
||||
root / "texture-packs" / texture_game_id, ec);
|
||||
std::string directory_error;
|
||||
if (!s.manager.set_generated_feature_resource_default(package->id,
|
||||
plugin.feature_id, "pack", std::filesystem::path("texture-packs") / texture_game_id,
|
||||
&directory_error)) {
|
||||
std::fprintf(stderr, "psxrecomp: texture pack folder: %s\n", directory_error.c_str());
|
||||
continue;
|
||||
}
|
||||
const auto pack = s.manager.feature_resource_path(package->id, plugin.feature_id, "pack");
|
||||
if (!ec) std::filesystem::create_directories(pack / "replacements", ec);
|
||||
if (!ec) std::filesystem::create_directories(pack / "dumps", ec);
|
||||
std::string directory_error;
|
||||
if (ec || !s.manager.set_feature_resource_path(package->id,
|
||||
plugin.feature_id, "pack", pack, &directory_error)) {
|
||||
if (ec) {
|
||||
std::fprintf(stderr, "psxrecomp: texture pack folder: %s\n",
|
||||
ec ? ec.message().c_str() : directory_error.c_str());
|
||||
ec.message().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2346,6 +2346,8 @@ int main() {
|
||||
}
|
||||
ModPackageManager manager(folder);
|
||||
check(manager.scan(&error) && manager.scan_errors().empty(), "source/preparation catalog parses");
|
||||
check(!manager.set_generated_feature_resource_default("cars", "content", "source", "generated/source", &error),
|
||||
"generated defaults cannot appoint shared user sources");
|
||||
check(manager.set_feature_resource_path("cars", "content", "source", source, &error), error.c_str());
|
||||
check(manager.feature_resource_path("music", "content", "source") == source, "source picker prefills sibling package");
|
||||
check(manager.set_feature_resource_path("music", "content", "source", replacement, &error), error.c_str());
|
||||
|
||||
@@ -2058,6 +2058,84 @@ int main(int argc, char** argv) {
|
||||
check(!psx_mod_netplay_is_active() && psx_mod_netplay_savestates_supported() &&
|
||||
PSXRecompV4::mod_runtime_fingerprint().empty(), "profile end clears hooks and snapshot policy");
|
||||
check(read_selection() == offline_selection, "profile end preserves offline selection file");
|
||||
|
||||
{
|
||||
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";
|
||||
|
||||
Reference in New Issue
Block a user