diff --git a/docs/MOD_PACKAGES.md b/docs/MOD_PACKAGES.md index da3bd0dd..f65268b8 100644 --- a/docs/MOD_PACKAGES.md +++ b/docs/MOD_PACKAGES.md @@ -135,10 +135,18 @@ derived-disc recipe is rejected after option resolution; packages should use a single owner package for structural transforms and dependencies/conflicts for external ownership. +Do not split one structural option system into many mutually exclusive +full-disc packages. For example, a game-wide Tweaks system should be one package +with launcher options and a conditional recipe matrix. Choices that are +mutually exclusive by design belong inside that package as option values, while +unrelated mods should remain separate packages and compose normally. + ## Resolution rules - Installed versions are side-by-side. The launcher can select an older version to roll back. +- Enabling or disabling one package changes only that package. It does not + silently toggle other packages. - Enabled packages are topologically ordered by dependencies, then by stable package/patch order. - Missing dependencies, version mismatches, declared conflicts, dependency diff --git a/runtime/src/mod_packages.cpp b/runtime/src/mod_packages.cpp index 2c86e826..50bf59e7 100644 --- a/runtime/src/mod_packages.cpp +++ b/runtime/src/mod_packages.cpp @@ -1088,26 +1088,6 @@ bool ModPackageManager::set_enabled(const std::string& id, bool enabled, std::st return false; } selections_[id].enabled = enabled; - if (!enabled) return true; - - const ModPackage* package = find_selected(packages_, id, selections_[id]); - if (!package) return true; - - auto conflicts_with_enabled = [&](const std::string& other_id, - const ModPackage& other) { - const auto declared = std::find(package->conflicts.begin(), - package->conflicts.end(), other_id); - if (declared != package->conflicts.end()) return true; - const auto reverse = std::find(other.conflicts.begin(), other.conflicts.end(), id); - return reverse != other.conflicts.end(); - }; - - for (auto& [other_id, selection] : selections_) { - if (other_id == id || !selection.enabled) continue; - const ModPackage* other = find_selected(packages_, other_id, selection); - if (other && conflicts_with_enabled(other_id, *other)) - selection.enabled = false; - } return true; } diff --git a/runtime/tests/test_mod_packages.cpp b/runtime/tests/test_mod_packages.cpp index 4c80a16c..19680683 100644 --- a/runtime/tests/test_mod_packages.cpp +++ b/runtime/tests/test_mod_packages.cpp @@ -191,9 +191,11 @@ int main() { check(reload.scan(&error), error.c_str()); check(reload.set_enabled("conflict.a", true, &error), error.c_str()); check(reload.set_enabled("conflict.b", true, &error), error.c_str()); - check(!reload.selections().at("conflict.a").enabled && + check(reload.selections().at("conflict.a").enabled && reload.selections().at("conflict.b").enabled, - "enabling a conflicting package must disable the previous package"); + "enabling a package must not silently disable another package"); + check(!reload.resolve("SLUS-TEST").ok, + "declared conflicts must fail resolution"); write_text(root / "packages/matrix.mod/1.0.0/manifest.toml", manifest("matrix.mod", "1.0.0", @@ -228,6 +230,7 @@ int main() { "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.a", false, &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());