runtime: pass active mod context to resolvers

This commit is contained in:
Matthew Stanley
2026-07-24 10:30:35 -07:00
parent fe3dd3f373
commit 9cf77cdc85
3 changed files with 254 additions and 91 deletions
+10
View File
@@ -45,6 +45,7 @@ struct ModFeature {
enum class ModConstraintKind {
OrderedInteger,
RequiresFeature,
};
enum class ModConstraintDirection {
@@ -58,6 +59,9 @@ struct ModConstraint {
ModConstraintDirection direction =
ModConstraintDirection::Nondecreasing;
std::vector<std::string> options;
std::string required_feature_id;
std::string required_option_id;
std::string required_value;
};
struct ModRequirement {
@@ -235,9 +239,15 @@ struct ModResolution {
std::vector<std::string> errors;
};
struct ModBuiltinResolverContext {
const std::map<std::string, const ModPackage*>* active_packages = nullptr;
const std::map<std::string, ModSelection>* selections = nullptr;
};
using ModBuiltinResolver = std::function<bool(
const ModPackage& package,
const ModSelection& selection,
const ModBuiltinResolverContext& context,
std::vector<ModResolution::Write>& writes,
std::vector<std::string>& errors)>;
+203 -91
View File
@@ -799,23 +799,107 @@ std::string effective_option_value(const ModPackage& package,
return option ? option->default_value : std::string();
}
bool feature_constraints_satisfied(
bool prospective_feature_enabled(
const ModPackage& package, const ModSelection& selection,
const std::string& feature_id, const std::string* override_id,
const std::string& feature_id, const std::string* override_feature_id,
const bool* override_enabled) {
if (override_feature_id && override_enabled &&
feature_id == *override_feature_id)
return *override_enabled;
const ModFeature* feature = find_feature(package, feature_id);
return feature && is_feature_enabled(package, selection, *feature);
}
std::string prospective_option_value(
const ModPackage& package, const ModSelection& selection,
const std::string& feature_id, const std::string& option_id,
const std::string* override_feature_id,
const std::string* override_option_id,
const std::string* override_value) {
if (override_feature_id && override_option_id && override_value &&
feature_id == *override_feature_id && option_id == *override_option_id)
return *override_value;
return effective_option_value(package, selection, feature_id, option_id);
}
bool runtime_constraint_satisfied(
const ModPackage& package, const ModSelection& selection,
const ModConstraint& constraint,
const std::string* override_feature_id, const bool* override_enabled,
const std::string* override_option_feature_id,
const std::string* override_option_id,
const std::string* override_value, std::string* reason) {
for (const ModConstraint& constraint : package.constraints) {
if (constraint.feature_id != feature_id) continue;
if (!constraint_satisfied(
package, constraint,
[&](const std::string& id) {
return override_id && override_value &&
id == *override_id
? *override_value
: effective_option_value(
package, selection, feature_id, id);
},
reason))
if (!prospective_feature_enabled(
package, selection, constraint.feature_id, override_feature_id,
override_enabled))
return true;
if (constraint.kind == ModConstraintKind::OrderedInteger) {
return constraint_satisfied(
package, constraint,
[&](const std::string& id) {
return prospective_option_value(
package, selection, constraint.feature_id, id,
override_option_feature_id, override_option_id,
override_value);
},
reason);
}
if (constraint.kind == ModConstraintKind::RequiresFeature) {
const ModFeature* required =
find_feature(package, constraint.required_feature_id);
const std::string required_name =
required && !required->name.empty()
? required->name
: constraint.required_feature_id;
if (!prospective_feature_enabled(
package, selection, constraint.required_feature_id,
override_feature_id, override_enabled)) {
if (reason) *reason = "requires " + required_name + " to be enabled";
return false;
}
if (!constraint.required_option_id.empty()) {
const std::string actual = prospective_option_value(
package, selection, constraint.required_feature_id,
constraint.required_option_id, override_option_feature_id,
override_option_id, override_value);
if (actual != constraint.required_value) {
const ModOption* option = find_option(
package, constraint.required_feature_id,
constraint.required_option_id);
if (reason) {
*reason =
"requires " + required_name + " " +
(option && !option->label.empty()
? option->label
: constraint.required_option_id) +
" = " + constraint.required_value;
}
return false;
}
}
return true;
}
if (reason) *reason = "unsupported constraint kind";
return false;
}
bool package_constraints_satisfied(
const ModPackage& package, const ModSelection& selection,
const std::string* override_feature_id, const bool* override_enabled,
const std::string* override_option_feature_id,
const std::string* override_option_id,
const std::string* override_value,
std::string* failing_feature_id, std::string* reason) {
for (const ModConstraint& constraint : package.constraints) {
std::string local_reason;
if (!runtime_constraint_satisfied(
package, selection, constraint, override_feature_id,
override_enabled, override_option_feature_id,
override_option_id, override_value, &local_reason)) {
if (failing_feature_id) *failing_feature_id = constraint.feature_id;
if (reason) *reason = local_reason;
return false;
}
}
return true;
}
@@ -1365,53 +1449,85 @@ bool ModPackageManager::read_manifest(const fs::path& path, ModPackage& out,
toml::find<std::string>(v, "feature");
const std::string kind =
toml::find<std::string>(v, "kind");
const std::string direction =
toml::find<std::string>(v, "direction");
constraint.options =
toml::find<std::vector<std::string>>(v, "options");
if (!find_feature(out, constraint.feature_id))
throw std::runtime_error(
"constraint references unknown feature");
if (kind != "ordered_integer")
throw std::runtime_error(
"constraint kind must be ordered_integer");
if (direction == "nondecreasing") {
constraint.direction =
ModConstraintDirection::Nondecreasing;
} else if (direction == "nonincreasing") {
constraint.direction =
ModConstraintDirection::Nonincreasing;
if (kind == "ordered_integer") {
const std::string direction =
toml::find<std::string>(v, "direction");
constraint.options =
toml::find<std::vector<std::string>>(v, "options");
if (direction == "nondecreasing") {
constraint.direction =
ModConstraintDirection::Nondecreasing;
} else if (direction == "nonincreasing") {
constraint.direction =
ModConstraintDirection::Nonincreasing;
} else {
throw std::runtime_error(
"ordered_integer direction must be "
"nondecreasing or nonincreasing");
}
if (constraint.options.size() < 2)
throw std::runtime_error(
"ordered_integer requires at least two options");
std::set<std::string> option_ids;
for (const std::string& id : constraint.options) {
const ModOption* option =
find_option(out, constraint.feature_id, id);
if (!option || option->type != ModOptionType::Integer)
throw std::runtime_error(
"ordered_integer must reference same-feature "
"integer options");
if (!option_ids.insert(id).second)
throw std::runtime_error(
"ordered_integer contains a duplicate option");
}
std::string reason;
if (!constraint_satisfied(
out, constraint,
[&](const std::string& id) {
return find_option(
out, constraint.feature_id, id)
->default_value;
},
&reason))
throw std::runtime_error(
"constraint defaults are invalid: " + reason);
} else if (kind == "requires_feature") {
if (out.format_version < 4)
throw std::runtime_error(
"requires_feature constraints require format_version 4");
constraint.kind = ModConstraintKind::RequiresFeature;
constraint.required_feature_id =
toml::find<std::string>(v, "requires_feature");
if (!find_feature(out, constraint.required_feature_id))
throw std::runtime_error(
"requires_feature references unknown feature");
constraint.required_option_id =
toml::find_or<std::string>(v, "requires_option", "");
constraint.required_value =
toml::find_or<std::string>(v, "requires_value", "");
if (constraint.required_option_id.empty() !=
constraint.required_value.empty())
throw std::runtime_error(
"requires_feature option constraint requires both "
"requires_option and requires_value");
if (!constraint.required_option_id.empty()) {
const ModOption* option = find_option(
out, constraint.required_feature_id,
constraint.required_option_id);
if (!option)
throw std::runtime_error(
"requires_feature references unknown option");
if (!valid_option_value(
*option, constraint.required_value))
throw std::runtime_error(
"requires_feature references invalid option value");
}
} else {
throw std::runtime_error(
"ordered_integer direction must be "
"nondecreasing or nonincreasing");
throw std::runtime_error("unknown constraint kind");
}
if (constraint.options.size() < 2)
throw std::runtime_error(
"ordered_integer requires at least two options");
std::set<std::string> option_ids;
for (const std::string& id : constraint.options) {
const ModOption* option =
find_option(out, constraint.feature_id, id);
if (!option || option->type != ModOptionType::Integer)
throw std::runtime_error(
"ordered_integer must reference same-feature "
"integer options");
if (!option_ids.insert(id).second)
throw std::runtime_error(
"ordered_integer contains a duplicate option");
}
std::string reason;
if (!constraint_satisfied(
out, constraint,
[&](const std::string& id) {
return find_option(
out, constraint.feature_id, id)
->default_value;
},
&reason))
throw std::runtime_error(
"constraint defaults are invalid: " + reason);
out.constraints.push_back(std::move(constraint));
}
}
@@ -2227,14 +2343,15 @@ bool ModPackageManager::set_feature_enabled(const std::string& package_id,
return false;
}
ModSelection& package_selection = selections_[package_id];
if (enabled) {
std::string reason;
if (!feature_constraints_satisfied(
*package, package_selection, feature_id,
nullptr, nullptr, &reason)) {
set_error(error, package_id + "/" + feature_id + ": " + reason);
return false;
}
std::string failing_feature;
std::string reason;
if (!package_constraints_satisfied(
*package, package_selection, &feature_id, &enabled,
nullptr, nullptr, nullptr, &failing_feature, &reason)) {
set_error(error, package_id + "/" +
(failing_feature.empty() ? feature_id : failing_feature) +
": " + reason);
return false;
}
ModFeatureSelection& selection = package_selection.features[feature_id];
selection.enabled = enabled;
@@ -2261,14 +2378,15 @@ bool ModPackageManager::set_feature_option(const std::string& package_id,
return false;
}
ModSelection& package_selection = selections_[package_id];
if (is_feature_enabled(*package, package_selection, *feature)) {
std::string reason;
if (!feature_constraints_satisfied(
*package, package_selection, feature_id,
&option_id, &value, &reason)) {
set_error(error, package_id + "/" + feature_id + ": " + reason);
return false;
}
std::string failing_feature;
std::string reason;
if (!package_constraints_satisfied(
*package, package_selection, nullptr, nullptr,
&feature_id, &option_id, &value, &failing_feature, &reason)) {
set_error(error, package_id + "/" +
(failing_feature.empty() ? feature_id : failing_feature) +
": " + reason);
return false;
}
package_selection.features[feature_id].values[option_id] = value;
return true;
@@ -2355,24 +2473,13 @@ ModResolution ModPackageManager::resolve(const std::string& game_id,
selection == selections_.end()
? blank_selection
: selection->second;
for (const ModConstraint& constraint : package->constraints) {
const ModFeature* feature =
find_feature(*package, constraint.feature_id);
if (!feature ||
!is_feature_enabled(
*package, effective_selection, *feature))
continue;
std::string reason;
if (!constraint_satisfied(
*package, constraint,
[&](const std::string& option_id) {
return effective_option_value(
*package, effective_selection,
constraint.feature_id, option_id);
},
&reason))
result.errors.push_back(
id + "/" + constraint.feature_id + ": " + reason);
std::string failing_feature;
std::string reason;
if (!package_constraints_satisfied(
*package, effective_selection, nullptr, nullptr,
nullptr, nullptr, nullptr, &failing_feature, &reason)) {
result.errors.push_back(
id + "/" + failing_feature + ": " + reason);
}
if (selection != selections_.end()) {
for (const ModOption& option : package->options) {
@@ -2432,6 +2539,9 @@ ModResolution ModPackageManager::resolve(const std::string& game_id,
return result;
}
ModBuiltinResolverContext resolver_context;
resolver_context.active_packages = &active;
resolver_context.selections = &selections_;
for (const ModPackage* package : result.ordered) {
const auto selected_it = selections_.find(package->id);
const ModSelection blank;
@@ -2697,7 +2807,9 @@ ModResolution ModPackageManager::resolve(const std::string& game_id,
const std::string resolver_id = package->resolver.substr(8);
const auto resolver = builtin_resolvers().find(resolver_id);
if (resolver != builtin_resolvers().end() &&
!resolver->second(*package, selected, result.writes, result.errors) &&
!resolver->second(
*package, selected, resolver_context,
result.writes, result.errors) &&
result.errors.empty())
result.errors.push_back(package->id + ": built-in resolver failed");
}
+41
View File
@@ -259,6 +259,47 @@ int main() {
matrix.derived_discs[0].output_size == 222222,
"multi-option derived-disc condition must match selected values");
mod_clear_builtin_resolvers_for_tests();
bool resolver_context_seen = false;
check(mod_register_builtin_resolver(
"context-test",
[&](const ModPackage& package, const ModSelection& selection,
const ModBuiltinResolverContext& context,
std::vector<ModResolution::Write>& writes,
std::vector<std::string>& errors) {
(void)writes;
(void)errors;
resolver_context_seen =
package.id == "context.consumer" &&
selection.enabled &&
context.active_packages &&
context.selections &&
context.active_packages->count("context.provider") == 1 &&
context.active_packages->count("context.consumer") == 1 &&
context.selections->at("context.provider").enabled &&
context.selections->at("context.consumer").enabled;
return resolver_context_seen;
}),
"test resolver must register");
write_text(root / "packages/context.provider/1.0.0/manifest.toml",
manifest("context.provider", "1.0.0"));
write_text(root / "packages/context.consumer/1.0.0/manifest.toml",
"format_version = 1\n"
"id = \"context.consumer\"\n"
"version = \"1.0.0\"\n"
"name = \"context.consumer\"\n"
"resolver = \"builtin:context-test\"\n"
"[[target]]\n"
"game_id = \"SLUS-TEST\"\n");
check(reload.scan(&error), error.c_str());
check(reload.set_enabled("matrix.mod", false, &error), error.c_str());
check(reload.set_enabled("context.provider", true, &error), error.c_str());
check(reload.set_enabled("context.consumer", true, &error), error.c_str());
ModResolution context_resolution = reload.resolve("SLUS-TEST");
check(context_resolution.ok && resolver_context_seen,
"built-in resolver must receive active package selection context");
mod_clear_builtin_resolvers_for_tests();
write_text(root / "packages/features.mod/1.0.0/manifest.toml",
manifest("features.mod", "1.0.0",
"\n[[feature]]\n"