mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-27 02:32:02 -04:00
runtime: pass active mod context to resolvers
This commit is contained in:
@@ -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
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user