mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
feat: support multi-option mod conditions
This commit is contained in:
+19
-4
@@ -68,10 +68,22 @@ output_size = 600000000
|
||||
output_sha256 = "..."
|
||||
when_option = "delay"
|
||||
when_value = "fast"
|
||||
|
||||
# For option matrices, use a condition table. All listed option values must
|
||||
# match. `when_option` / `when_value` remain supported for single-option cases.
|
||||
[[derived_disc]]
|
||||
kind = "vcdiff"
|
||||
patch = "assets/fast-rockman.xdelta3"
|
||||
patch_sha256 = "..."
|
||||
output_size = 600000000
|
||||
output_sha256 = "..."
|
||||
when = { delay = "fast", title_screen = "rockman_japan" }
|
||||
```
|
||||
|
||||
Option types are `boolean`, `choice`, and bounded `integer`. `when_option` /
|
||||
`when_value` are optional; an unconditional patch omits both.
|
||||
`when_value` are optional; `when = { option = "value", ... }` can be used when a
|
||||
patch or derived-disc recipe depends on multiple option values. An unconditional
|
||||
patch omits both condition forms.
|
||||
|
||||
## Patch targets
|
||||
|
||||
@@ -97,7 +109,9 @@ option combinations.
|
||||
|
||||
A `derived_disc` is a data-only VCDIFF recipe whose source is the verified stock
|
||||
disc from `[[target]].disc_sha256`. It is intended for mods that relocate files,
|
||||
grow the ISO, replace large assets, or otherwise change disc geometry.
|
||||
grow the ISO, replace large assets, or otherwise change disc geometry. A package
|
||||
may contain a matrix of conditional recipes for its own options, but exactly one
|
||||
recipe may resolve in the complete mod plan.
|
||||
|
||||
The launcher continues to display and persist the user's stock BIN/CUE. Before
|
||||
boot, the runtime:
|
||||
@@ -117,8 +131,9 @@ base package can support many small composable add-ons.
|
||||
|
||||
Release builders stage the trusted decoder with
|
||||
`-DPSXRECOMP_XDELTA3_EXECUTABLE=/path/to/xdelta3`. More than one active
|
||||
derived-disc provider is rejected; packages should use dependencies and
|
||||
conflicts to make ownership explicit.
|
||||
derived-disc recipe is rejected after option resolution; packages should use a
|
||||
single owner package for structural transforms and dependencies/conflicts for
|
||||
external ownership.
|
||||
|
||||
## Resolution rules
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ struct ModPatch {
|
||||
std::vector<uint8_t> replacement;
|
||||
std::string when_option;
|
||||
std::string when_value;
|
||||
std::map<std::string, std::string> when;
|
||||
int64_t order = 0;
|
||||
};
|
||||
|
||||
@@ -68,6 +69,7 @@ struct ModDerivedDisc {
|
||||
std::string output_sha256;
|
||||
std::string when_option;
|
||||
std::string when_value;
|
||||
std::map<std::string, std::string> when;
|
||||
};
|
||||
|
||||
struct ModPackage {
|
||||
|
||||
@@ -605,6 +605,45 @@ std::string effective_option_value(const ModPackage& package,
|
||||
return option == package.options.end() ? std::string() : option->default_value;
|
||||
}
|
||||
|
||||
bool conditions_match(const ModPackage& package, const ModSelection& selection,
|
||||
const std::map<std::string, std::string>& conditions) {
|
||||
for (const auto& [id, value] : conditions) {
|
||||
if (effective_option_value(package, selection, id) != value)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void read_conditions(const toml::value& value, const std::vector<ModOption>& options,
|
||||
std::map<std::string, std::string>& when,
|
||||
const char* label) {
|
||||
const std::string when_option =
|
||||
value.contains("when_option") ? toml::find<std::string>(value, "when_option") : "";
|
||||
const std::string when_value =
|
||||
value.contains("when_value") ? toml::find<std::string>(value, "when_value") : "";
|
||||
if (when_option.empty() != when_value.empty())
|
||||
throw std::runtime_error(
|
||||
std::string(label) + " condition requires both when_option and when_value");
|
||||
if (!when_option.empty()) when[when_option] = when_value;
|
||||
if (value.contains("when")) {
|
||||
const auto table = toml::find<std::map<std::string, std::string>>(value, "when");
|
||||
for (const auto& [id, condition_value] : table) {
|
||||
if (when.find(id) != when.end() && when[id] != condition_value)
|
||||
throw std::runtime_error(
|
||||
std::string(label) + " has conflicting conditions for " + id);
|
||||
when[id] = condition_value;
|
||||
}
|
||||
}
|
||||
for (const auto& [id, condition_value] : when) {
|
||||
(void)condition_value;
|
||||
const auto option = std::find_if(
|
||||
options.begin(), options.end(),
|
||||
[&](const ModOption& item) { return item.id == id; });
|
||||
if (option == options.end())
|
||||
throw std::runtime_error(std::string(label) + " references unknown option");
|
||||
}
|
||||
}
|
||||
|
||||
bool writes_overlap(const ModResolution::Write& a, const ModResolution::Write& b) {
|
||||
if (a.target != b.target) return false;
|
||||
const uint64_t a_end = a.location + a.replacement.size();
|
||||
@@ -781,21 +820,12 @@ bool ModPackageManager::read_manifest(const fs::path& path, ModPackage& out,
|
||||
patch.location % sector_size + patch.replacement.size() > sector_size)
|
||||
throw std::runtime_error(
|
||||
"disc patch may not cross a sector boundary");
|
||||
patch.when_option =
|
||||
v.contains("when_option") ? toml::find<std::string>(v, "when_option") : "";
|
||||
patch.when_value =
|
||||
v.contains("when_value") ? toml::find<std::string>(v, "when_value") : "";
|
||||
patch.order = toml::find_or<int64_t>(
|
||||
v, "order", (int64_t)declaration_index);
|
||||
if (patch.when_option.empty() != patch.when_value.empty())
|
||||
throw std::runtime_error(
|
||||
"patch condition requires both when_option and when_value");
|
||||
if (!patch.when_option.empty()) {
|
||||
const auto option = std::find_if(
|
||||
out.options.begin(), out.options.end(),
|
||||
[&](const ModOption& item) { return item.id == patch.when_option; });
|
||||
if (option == out.options.end())
|
||||
throw std::runtime_error("patch references unknown option");
|
||||
read_conditions(v, out.options, patch.when, "patch");
|
||||
if (!patch.when.empty()) {
|
||||
patch.when_option = patch.when.begin()->first;
|
||||
patch.when_value = patch.when.begin()->second;
|
||||
}
|
||||
out.patches.push_back(std::move(patch));
|
||||
++declaration_index;
|
||||
@@ -809,10 +839,6 @@ bool ModPackageManager::read_manifest(const fs::path& path, ModPackage& out,
|
||||
derived.patch_sha256 = toml::find<std::string>(v, "patch_sha256");
|
||||
const int64_t output_size = toml::find<int64_t>(v, "output_size");
|
||||
derived.output_sha256 = toml::find<std::string>(v, "output_sha256");
|
||||
derived.when_option =
|
||||
v.contains("when_option") ? toml::find<std::string>(v, "when_option") : "";
|
||||
derived.when_value =
|
||||
v.contains("when_value") ? toml::find<std::string>(v, "when_value") : "";
|
||||
if (derived.kind != "vcdiff")
|
||||
throw std::runtime_error("derived_disc kind must be vcdiff");
|
||||
if (!safe_archive_name(relative_patch))
|
||||
@@ -824,16 +850,10 @@ bool ModPackageManager::read_manifest(const fs::path& path, ModPackage& out,
|
||||
if (output_size <= 0)
|
||||
throw std::runtime_error("derived_disc output_size must be positive");
|
||||
derived.output_size = (uint64_t)output_size;
|
||||
if (derived.when_option.empty() != derived.when_value.empty())
|
||||
throw std::runtime_error(
|
||||
"derived_disc condition requires both when_option and when_value");
|
||||
if (!derived.when_option.empty()) {
|
||||
const auto option = std::find_if(
|
||||
out.options.begin(), out.options.end(),
|
||||
[&](const ModOption& item) { return item.id == derived.when_option; });
|
||||
if (option == out.options.end())
|
||||
throw std::runtime_error(
|
||||
"derived_disc references unknown option");
|
||||
read_conditions(v, out.options, derived.when, "derived_disc");
|
||||
if (!derived.when.empty()) {
|
||||
derived.when_option = derived.when.begin()->first;
|
||||
derived.when_value = derived.when.begin()->second;
|
||||
}
|
||||
derived.patch = out.root / fs::path(relative_patch);
|
||||
if (!fs::is_regular_file(derived.patch))
|
||||
@@ -1246,9 +1266,7 @@ ModResolution ModPackageManager::resolve(const std::string& game_id,
|
||||
selected_it == selections_.end() ? blank : selected_it->second;
|
||||
if (package->resolver == "declarative") {
|
||||
for (const ModDerivedDisc& derived : package->derived_discs) {
|
||||
if (!derived.when_option.empty() &&
|
||||
effective_option_value(*package, selected, derived.when_option) !=
|
||||
derived.when_value)
|
||||
if (!conditions_match(*package, selected, derived.when))
|
||||
continue;
|
||||
ModResolution::DerivedDisc resolved;
|
||||
resolved.kind = derived.kind;
|
||||
@@ -1262,9 +1280,7 @@ ModResolution ModPackageManager::resolve(const std::string& game_id,
|
||||
std::vector<const ModPatch*> patches;
|
||||
patches.reserve(package->patches.size());
|
||||
for (const ModPatch& patch : package->patches) {
|
||||
if (!patch.when_option.empty() &&
|
||||
effective_option_value(*package, selected, patch.when_option) !=
|
||||
patch.when_value)
|
||||
if (!conditions_match(*package, selected, patch.when))
|
||||
continue;
|
||||
patches.push_back(&patch);
|
||||
}
|
||||
|
||||
@@ -195,6 +195,48 @@ int main() {
|
||||
reload.selections().at("conflict.b").enabled,
|
||||
"enabling a conflicting package must disable the previous package");
|
||||
|
||||
write_text(root / "packages/matrix.mod/1.0.0/manifest.toml",
|
||||
manifest("matrix.mod", "1.0.0",
|
||||
"\n[[option]]\n"
|
||||
"id = \"title\"\n"
|
||||
"label = \"Title\"\n"
|
||||
"type = \"choice\"\n"
|
||||
"default = \"mega\"\n"
|
||||
"[[option.choice]]\n"
|
||||
"value = \"mega\"\n"
|
||||
"label = \"Mega\"\n"
|
||||
"[[option.choice]]\n"
|
||||
"value = \"rockman\"\n"
|
||||
"label = \"Rockman\"\n"
|
||||
"\n[[option]]\n"
|
||||
"id = \"script\"\n"
|
||||
"label = \"Script\"\n"
|
||||
"type = \"choice\"\n"
|
||||
"default = \"original\"\n"
|
||||
"[[option.choice]]\n"
|
||||
"value = \"original\"\n"
|
||||
"label = \"Original\"\n"
|
||||
"[[option.choice]]\n"
|
||||
"value = \"retranslation\"\n"
|
||||
"label = \"Retranslation\"\n"
|
||||
"\n[[derived_disc]]\n"
|
||||
"kind = \"vcdiff\"\n"
|
||||
"patch = \"assets/matrix.xdelta3\"\n"
|
||||
"patch_sha256 = \"2222222222222222222222222222222222222222222222222222222222222222\"\n"
|
||||
"output_size = 222222\n"
|
||||
"output_sha256 = \"3333333333333333333333333333333333333333333333333333333333333333\"\n"
|
||||
"when = { title = \"rockman\", script = \"retranslation\" }\n"));
|
||||
write_text(root / "packages/matrix.mod/1.0.0/assets/matrix.xdelta3", "test");
|
||||
check(reload.scan(&error), error.c_str());
|
||||
check(reload.set_enabled("conflict.b", false, &error), error.c_str());
|
||||
check(reload.set_enabled("matrix.mod", true, &error), error.c_str());
|
||||
check(reload.set_option("matrix.mod", "title", "rockman", &error), error.c_str());
|
||||
check(reload.set_option("matrix.mod", "script", "retranslation", &error), error.c_str());
|
||||
ModResolution matrix = reload.resolve("SLUS-TEST");
|
||||
check(matrix.ok && matrix.derived_discs.size() == 1 &&
|
||||
matrix.derived_discs[0].output_size == 222222,
|
||||
"multi-option derived-disc condition must match selected values");
|
||||
|
||||
ModPackage invalid;
|
||||
write_text(root / "bad.toml",
|
||||
"format_version=1\nid=\"../bad\"\nversion=\"1.0.0\"\nname=\"Bad\"\n"
|
||||
|
||||
Reference in New Issue
Block a user