From ddf3ca8962fa69f3ec8713d21c65ca8b049e773d Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 23 Jul 2026 16:23:55 -0700 Subject: [PATCH] feat: add ordered mod values and MIPS pair patches --- docs/MOD_PACKAGES.md | 45 +++++ runtime/include/mod_packages.h | 20 ++ runtime/src/mod_packages.cpp | 287 ++++++++++++++++++++++++++-- runtime/tests/test_mod_packages.cpp | 120 +++++++++++- 4 files changed, 456 insertions(+), 16 deletions(-) diff --git a/docs/MOD_PACKAGES.md b/docs/MOD_PACKAGES.md index 466e5b7f..d40dc045 100644 --- a/docs/MOD_PACKAGES.md +++ b/docs/MOD_PACKAGES.md @@ -142,6 +142,51 @@ Integer values use canonical decimal text. Leading plus signs, redundant leading zeroes, values outside the bounds, and values not aligned to `step` are rejected. +## Ordered values and split MIPS immediates + +Package format 3 adds feature-local ordering constraints for related integer +fields: + +```toml +format_version = 3 + +[[constraint]] +feature = "rank-thresholds" +kind = "ordered_integer" +direction = "nondecreasing" +options = ["rank-c", "rank-b", "rank-a"] +``` + +All listed options must be integer options on that feature. Defaults must +satisfy the constraint. While a feature is enabled, an edit that would invert +the order is rejected with the neighboring option labels. Disabled features +may retain an incomplete or invalid draft, but cannot be enabled until it is +valid. `nonincreasing` is also supported. + +Format 3 also provides a narrow, typed transform for constants constructed by +a linked MIPS `LUI`/`ORI` pair: + +```toml +replace_from = { + option = "speed", + encoding = "mips_lui_ori_u32", + omit_when_default = true +} +``` + +The patch must target one aligned, fully guarded eight-byte `main_exe` +instruction pair. The loader verifies the opcodes and register linkage, then +places the raw high and low 16-bit halves into the two immediates. It does not +apply signed-`ADDIU` carry adjustment. `offset` and `addend` are not accepted +for this encoding. + +`omit_when_default` suppresses the entire patch when the selected value equals +the option default. This models source tools whose declared default means +"make no writes," including cases where multiple guarded sites contain +different stock values. For any nondefault selection, every declared site +retains its collision claim even if one generated replacement happens to equal +its stock guard. + ## Native operations `main_exe` writes use PSX guest virtual addresses. Expected bytes are checked diff --git a/runtime/include/mod_packages.h b/runtime/include/mod_packages.h index 1a63cdee..6bfcebff 100644 --- a/runtime/include/mod_packages.h +++ b/runtime/include/mod_packages.h @@ -43,6 +43,23 @@ struct ModFeature { bool legacy = false; }; +enum class ModConstraintKind { + OrderedInteger, +}; + +enum class ModConstraintDirection { + Nondecreasing, + Nonincreasing, +}; + +struct ModConstraint { + std::string feature_id; + ModConstraintKind kind = ModConstraintKind::OrderedInteger; + ModConstraintDirection direction = + ModConstraintDirection::Nondecreasing; + std::vector options; +}; + struct ModRequirement { std::string id; std::string version; @@ -64,6 +81,7 @@ enum class ModValueEncoding { U8, U16LE, U32LE, + MipsLuiOriU32, }; struct ModPatch { @@ -76,6 +94,7 @@ struct ModPatch { ModValueEncoding replace_encoding = ModValueEncoding::U8; uint64_t replace_offset = 0; int64_t replace_addend = 0; + bool replace_omit_when_default = false; std::string when_option; std::string when_value; std::map when; @@ -121,6 +140,7 @@ struct ModPackage { std::vector conflicts; std::vector features; std::vector options; + std::vector constraints; std::vector patches; std::vector overlays; std::vector derived_discs; diff --git a/runtime/src/mod_packages.cpp b/runtime/src/mod_packages.cpp index c6e88f5e..9c3fc958 100644 --- a/runtime/src/mod_packages.cpp +++ b/runtime/src/mod_packages.cpp @@ -21,7 +21,7 @@ namespace PSXRecompV4 { namespace { constexpr uint32_t kMinFormatVersion = 1; -constexpr uint32_t kMaxFormatVersion = 2; +constexpr uint32_t kMaxFormatVersion = 3; constexpr uint64_t kMaxArchiveBytes = 256ull * 1024ull * 1024ull; constexpr uint32_t kMaxArchiveFiles = 4096; @@ -79,6 +79,7 @@ bool parse_value_encoding(const std::string& text, ModValueEncoding& out) { {"u8", ModValueEncoding::U8}, {"u16le", ModValueEncoding::U16LE}, {"u32le", ModValueEncoding::U32LE}, + {"mips_lui_ori_u32", ModValueEncoding::MipsLuiOriU32}, }; for (const auto& [name, encoding] : values) { if (text == name) { @@ -97,6 +98,8 @@ uint32_t value_encoding_size(ModValueEncoding encoding) { return 2; case ModValueEncoding::U32LE: return 4; + case ModValueEncoding::MipsLuiOriU32: + return 8; } return 0; } @@ -673,6 +676,11 @@ const ModOption* find_option(const ModPackage& package, return option == package.options.end() ? nullptr : &*option; } +bool constraint_satisfied( + const ModPackage& package, const ModConstraint& constraint, + const std::function& value_for, + std::string* reason); + const ModFeatureSelection* find_feature_selection(const ModPackage& package, const ModSelection& selection, const std::string& feature_id) { @@ -720,6 +728,27 @@ std::string effective_option_value(const ModPackage& package, return option ? option->default_value : std::string(); } +bool feature_constraints_satisfied( + const ModPackage& package, const ModSelection& selection, + const std::string& feature_id, const std::string* override_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)) + return false; + } + return true; +} + bool conditions_match(const ModPackage& package, const ModSelection& selection, const std::string& feature_id, const std::map& conditions) { @@ -887,6 +916,46 @@ bool valid_option_value(const ModOption& option, const std::string& value) { integer_step_aligned(parsed, option.min_value, option.step); } +bool constraint_satisfied( + const ModPackage& package, const ModConstraint& constraint, + const std::function& value_for, + std::string* reason) { + if (constraint.kind != ModConstraintKind::OrderedInteger) return false; + for (size_t index = 1; index < constraint.options.size(); ++index) { + const std::string& previous_id = constraint.options[index - 1]; + const std::string& current_id = constraint.options[index]; + int64_t previous = 0; + int64_t current = 0; + if (!parse_canonical_int64(value_for(previous_id), previous) || + !parse_canonical_int64(value_for(current_id), current)) { + if (reason) + *reason = "constraint references a non-integer value"; + return false; + } + const bool violated = + constraint.direction == ModConstraintDirection::Nondecreasing + ? previous > current + : previous < current; + if (violated) { + if (reason) { + const ModOption* previous_option = find_option( + package, constraint.feature_id, previous_id); + const ModOption* current_option = find_option( + package, constraint.feature_id, current_id); + *reason = + (previous_option ? previous_option->label : previous_id) + + (constraint.direction == + ModConstraintDirection::Nondecreasing + ? " must be less than or equal to " + : " must be greater than or equal to ") + + (current_option ? current_option->label : current_id); + } + return false; + } + } + return true; +} + bool checked_add_int64(int64_t left, int64_t right, int64_t& out) { if ((right > 0 && left > std::numeric_limits::max() - right) || (right < 0 && left < std::numeric_limits::min() - right)) @@ -903,6 +972,8 @@ uint64_t value_encoding_max(ModValueEncoding encoding) { return UINT16_MAX; case ModValueEncoding::U32LE: return UINT32_MAX; + case ModValueEncoding::MipsLuiOriU32: + return UINT32_MAX; } return 0; } @@ -926,6 +997,7 @@ bool encode_unsigned_value(ModValueEncoding encoding, int64_t value, const uint32_t size = value_encoding_size(encoding); out.resize(size); const uint64_t encoded = static_cast(value); + if (encoding == ModValueEncoding::MipsLuiOriU32) return false; for (uint32_t i = 0; i < size; ++i) out[i] = static_cast(encoded >> (i * 8)); return true; @@ -1100,6 +1172,67 @@ bool ModPackageManager::read_manifest(const fs::path& path, ModPackage& out, out.options.push_back(std::move(option)); } } + if (cfg.contains("constraint")) { + if (out.format_version < 3) + throw std::runtime_error( + "constraints require format_version 3"); + for (const toml::value& v : + toml::find(cfg, "constraint").as_array()) { + ModConstraint constraint; + constraint.feature_id = + toml::find(v, "feature"); + const std::string kind = + toml::find(v, "kind"); + const std::string direction = + toml::find(v, "direction"); + constraint.options = + toml::find>(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; + } 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 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)); + } + } if (cfg.contains("patch")) { size_t declaration_index = 0; for (const toml::value& v : toml::find(cfg, "patch").as_array()) { @@ -1155,7 +1288,8 @@ bool ModPackageManager::read_manifest(const fs::path& path, ModPackage& out, for (const auto& [key, unused] : table) { (void)unused; if (key != "option" && key != "encoding" && - key != "offset" && key != "addend") + key != "offset" && key != "addend" && + key != "omit_when_default") throw std::runtime_error( "replace_from has unknown field: " + key); } @@ -1172,10 +1306,22 @@ bool ModPackageManager::read_manifest(const fs::path& path, ModPackage& out, static_cast(replace_offset); patch.replace_addend = toml::find_or(replacement, "addend", 0); + patch.replace_omit_when_default = + toml::find_or( + replacement, "omit_when_default", false); if (!parse_value_encoding( encoding, patch.replace_encoding)) throw std::runtime_error( - "replace_from encoding must be u8, u16le, or u32le"); + "replace_from encoding is unsupported"); + const bool mips_pair_encoding = + patch.replace_encoding == + ModValueEncoding::MipsLuiOriU32; + if ((mips_pair_encoding || + patch.replace_omit_when_default) && + out.format_version < 3) + throw std::runtime_error( + "typed MIPS encodings and omit_when_default " + "require format_version 3"); const ModOption* option = find_option( out, patch.feature_id, patch.replace_from_option); if (!option || option->type != ModOptionType::Integer) @@ -1193,6 +1339,37 @@ bool ModPackageManager::read_manifest(const fs::path& path, ModPackage& out, patch.replace_addend)) throw std::runtime_error( "replace_from option range/addend does not fit encoding"); + if (mips_pair_encoding) { + if (patch.target != ModPatchTarget::MainExe || + patch.expected.size() != 8 || + patch.replace_offset != 0 || + patch.replace_addend != 0 || + patch.location % 4 != 0) + throw std::runtime_error( + "typed MIPS LUI/ORI encoding requires one " + "aligned complete main_exe instruction pair " + "at offset zero without an addend"); + const uint32_t lui = + static_cast(patch.expected[0]) | + (static_cast(patch.expected[1]) << 8) | + (static_cast(patch.expected[2]) << 16) | + (static_cast(patch.expected[3]) << 24); + const uint32_t ori = + static_cast(patch.expected[4]) | + (static_cast(patch.expected[5]) << 8) | + (static_cast(patch.expected[6]) << 16) | + (static_cast(patch.expected[7]) << 24); + const uint32_t lui_rs = (lui >> 21) & 0x1Fu; + const uint32_t lui_rt = (lui >> 16) & 0x1Fu; + const uint32_t ori_rs = (ori >> 21) & 0x1Fu; + const uint32_t ori_rt = (ori >> 16) & 0x1Fu; + if ((lui >> 26) != 0x0Fu || lui_rs != 0 || + lui_rt == 0 || (ori >> 26) != 0x0Du || + ori_rs != lui_rt || ori_rt != lui_rt) + throw std::runtime_error( + "typed MIPS encoding guard is not a linked " + "LUI/ORI register pair"); + } } const uint64_t sector_size = patch.target == ModPatchTarget::DiscRaw ? 2352 : @@ -1691,8 +1868,17 @@ bool ModPackageManager::set_feature_enabled(const std::string& package_id, set_error(error, "unknown package feature"); return false; } - ModFeatureSelection& selection = - selections_[package_id].features[feature_id]; + 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; + } + } + ModFeatureSelection& selection = package_selection.features[feature_id]; selection.enabled = enabled; selection.has_enabled = true; return true; @@ -1716,7 +1902,17 @@ bool ModPackageManager::set_feature_option(const std::string& package_id, set_error(error, "invalid feature option value"); return false; } - selections_[package_id].features[feature_id].values[option_id] = value; + 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; + } + } + package_selection.features[feature_id].values[option_id] = value; return true; } @@ -1796,6 +1992,30 @@ ModResolution ModPackageManager::resolve(const std::string& game_id, result.errors.push_back(id + " conflicts with " + conflict); const auto selection = selections_.find(id); + const ModSelection blank_selection; + const ModSelection& effective_selection = + 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); + } if (selection != selections_.end()) { for (const ModOption& option : package->options) { const ModFeature* feature = @@ -1901,15 +2121,19 @@ ModResolution ModPackageManager::resolve(const std::string& game_id, effective_option_value( *package, selected, patch->feature_id, patch->replace_from_option); + const ModOption* source_option = find_option( + *package, patch->feature_id, + patch->replace_from_option); + if (patch->replace_omit_when_default && + source_option && + selected_value == source_option->default_value) + continue; int64_t parsed = 0; int64_t adjusted = 0; std::vector encoded; if (!parse_canonical_int64(selected_value, parsed) || !checked_add_int64( - parsed, patch->replace_addend, adjusted) || - !encode_unsigned_value( - patch->replace_encoding, adjusted, - encoded)) { + parsed, patch->replace_addend, adjusted)) { result.errors.push_back( package->id + "/" + patch->feature_id + ": could not encode replace_from option " + @@ -1917,11 +2141,44 @@ ModResolution ModPackageManager::resolve(const std::string& game_id, continue; } write.replacement = write.expected; - std::copy( - encoded.begin(), encoded.end(), - write.replacement.begin() + - static_cast(patch->replace_offset)); - if (write.replacement == write.expected) + if (patch->replace_encoding == + ModValueEncoding::MipsLuiOriU32) { + if (adjusted < 0 || + static_cast(adjusted) > UINT32_MAX) { + result.errors.push_back( + package->id + "/" + patch->feature_id + + ": could not encode replace_from option " + + patch->replace_from_option); + continue; + } + const uint32_t value = + static_cast(adjusted); + write.replacement[0] = + static_cast(value >> 16); + write.replacement[1] = + static_cast(value >> 24); + write.replacement[4] = + static_cast(value); + write.replacement[5] = + static_cast(value >> 8); + } else { + if (!encode_unsigned_value( + patch->replace_encoding, adjusted, + encoded)) { + result.errors.push_back( + package->id + "/" + patch->feature_id + + ": could not encode replace_from option " + + patch->replace_from_option); + continue; + } + std::copy( + encoded.begin(), encoded.end(), + write.replacement.begin() + + static_cast( + patch->replace_offset)); + } + if (write.replacement == write.expected && + !patch->replace_omit_when_default) continue; } write.package_id = package->id; diff --git a/runtime/tests/test_mod_packages.cpp b/runtime/tests/test_mod_packages.cpp index 1d3982b9..7f7771ec 100644 --- a/runtime/tests/test_mod_packages.cpp +++ b/runtime/tests/test_mod_packages.cpp @@ -405,7 +405,7 @@ int main() { "feature state must resolve deterministically after reload"); write_text(root / "packages/parametric.mod/1.0.0/manifest.toml", - "format_version = 2\n" + "format_version = 3\n" "id = \"parametric.mod\"\n" "version = \"1.0.0\"\n" "name = \"Parametric\"\n" @@ -446,6 +446,15 @@ int main() { "step = 1\n" "default = 305419896\n" "[[option]]\n" + "feature = \"numeric\"\n" + "id = \"split\"\n" + "label = \"Split\"\n" + "type = \"integer\"\n" + "min = 200000\n" + "max = 600000\n" + "step = 1\n" + "default = 425984\n" + "[[option]]\n" "feature = \"numeric-collision\"\n" "id = \"byte\"\n" "label = \"Byte\"\n" @@ -453,6 +462,11 @@ int main() { "min = 0\n" "max = 255\n" "default = 8\n" + "[[constraint]]\n" + "feature = \"numeric\"\n" + "kind = \"ordered_integer\"\n" + "direction = \"nondecreasing\"\n" + "options = [\"byte\", \"word\", \"dword\"]\n" "[[patch]]\n" "feature = \"numeric\"\n" "target = \"main_exe\"\n" @@ -484,6 +498,18 @@ int main() { "expected = \"0000aabb\"\n" "replace_from = { option = \"word\", encoding = \"u16le\", offset = 0 }\n" "[[patch]]\n" + "feature = \"numeric\"\n" + "target = \"main_exe\"\n" + "address = 2147500048\n" + "expected = \"0600013c00802134\"\n" + "replace_from = { option = \"split\", encoding = \"mips_lui_ori_u32\", omit_when_default = true }\n" + "[[patch]]\n" + "feature = \"numeric\"\n" + "target = \"main_exe\"\n" + "address = 2147500056\n" + "expected = \"0400013c00202134\"\n" + "replace_from = { option = \"split\", encoding = \"mips_lui_ori_u32\", omit_when_default = true }\n" + "[[patch]]\n" "feature = \"numeric-collision\"\n" "target = \"main_exe\"\n" "address = 2147500032\n" @@ -531,14 +557,63 @@ int main() { guarded_word_write->replacement == std::vector({0x34, 0x12, 0xaa, 0xbb}), "replace_from must preserve guarded bytes outside its value field"); + check(!numeric_write(0x80004010ull) && + !numeric_write(0x80004018ull), + "omit_when_default must suppress every split-immediate site"); const std::string parametric_fingerprint = parametric.fingerprint; check(feature_reload.set_feature_option( "parametric.mod", "numeric", "byte", "9", &error), error.c_str()); + check(!feature_reload.set_feature_option( + "parametric.mod", "numeric", "word", "5", &error), + "enabled ordered integer features must reject inverted values"); ModResolution changed_parametric = feature_reload.resolve("SLUS-TEST"); check(changed_parametric.ok && changed_parametric.fingerprint != parametric_fingerprint, "changing a generated integer must change the plan fingerprint"); + check(feature_reload.set_feature_option( + "parametric.mod", "numeric", "split", "200000", &error), + error.c_str()); + ModResolution split_parametric = feature_reload.resolve("SLUS-TEST"); + const auto split_write = [&](uint64_t location) + -> const ModResolution::Write* { + const auto found = std::find_if( + split_parametric.writes.begin(), split_parametric.writes.end(), + [&](const ModResolution::Write& write) { + return write.package_id == "parametric.mod" && + write.location == location; + }); + return found == split_parametric.writes.end() ? nullptr : &*found; + }; + check(split_write(0x80004010ull) && + split_write(0x80004010ull)->replacement == + std::vector({ + 0x03, 0x00, 0x01, 0x3c, + 0x40, 0x0d, 0x21, 0x34}) && + split_write(0x80004018ull) && + split_write(0x80004018ull)->replacement == + std::vector({ + 0x03, 0x00, 0x01, 0x3c, + 0x40, 0x0d, 0x21, 0x34}), + "typed MIPS split encodings must update every guarded pair"); + check(feature_reload.set_feature_option( + "parametric.mod", "numeric", "split", "270336", &error), + error.c_str()); + ModResolution partial_stock_split = + feature_reload.resolve("SLUS-TEST"); + check(std::count_if( + partial_stock_split.writes.begin(), + partial_stock_split.writes.end(), + [](const ModResolution::Write& write) { + return write.package_id == "parametric.mod" && + (write.location == 0x80004010ull || + write.location == 0x80004018ull); + }) == 2, + "a nondefault split value must retain ownership of a pair whose " + "replacement happens to equal stock"); + check(feature_reload.set_feature_option( + "parametric.mod", "numeric", "split", "425984", &error), + error.c_str()); check(feature_reload.set_feature_enabled( "parametric.mod", "numeric-collision", true, &error), error.c_str()); @@ -547,6 +622,19 @@ int main() { check(feature_reload.set_feature_enabled( "parametric.mod", "numeric-collision", false, &error), error.c_str()); + check(feature_reload.set_feature_enabled( + "parametric.mod", "numeric", false, &error), error.c_str()); + check(feature_reload.set_feature_option( + "parametric.mod", "numeric", "word", "0", &error), + "disabled features may retain an invalid draft"); + check(!feature_reload.set_feature_enabled( + "parametric.mod", "numeric", true, &error), + "an invalid ordered integer draft must block feature enablement"); + check(feature_reload.set_feature_option( + "parametric.mod", "numeric", "word", "4660", &error), + error.c_str()); + check(feature_reload.set_feature_enabled( + "parametric.mod", "numeric", true, &error), error.c_str()); check(feature_reload.save_state(&error), error.c_str()); ModPackageManager parametric_reload(root); check(parametric_reload.scan(&error), error.c_str()); @@ -605,6 +693,36 @@ int main() { "address=2147487744\nexpected=\"00\"\n" "replace_from={option=\"value\",encoding=\"u8\",shift=1}\n"), "replace_from must reject unknown transform fields"); + check(reject_parametric_manifest( + "dynamic-mips-v2", + "format_version=2\n" + dynamic_prelude + + "[[patch]]\nfeature=\"bad\"\ntarget=\"main_exe\"\n" + "address=2147487744\nexpected=\"0600013c00802134\"\n" + "replace_from={option=\"value\"," + "encoding=\"mips_lui_ori_u32\"}\n"), + "typed MIPS pairs must require package format 3"); + check(reject_parametric_manifest( + "dynamic-mips-unlinked", + "format_version=3\n" + dynamic_prelude + + "[[patch]]\nfeature=\"bad\"\ntarget=\"main_exe\"\n" + "address=2147487744\nexpected=\"0600013c00802234\"\n" + "replace_from={option=\"value\"," + "encoding=\"mips_lui_ori_u32\"}\n"), + "typed MIPS pairs must reject unlinked registers"); + check(reject_parametric_manifest( + "constraint-inverted-default", + "format_version=3\n" + "id=\"bad.dynamic\"\nversion=\"1.0.0\"\nname=\"Bad\"\n" + "[[target]]\ngame_id=\"SLUS-TEST\"\n" + "[[feature]]\nid=\"bad\"\nname=\"Bad\"\n" + "[[option]]\nfeature=\"bad\"\nid=\"low\"\nlabel=\"Low\"\n" + "type=\"integer\"\nmin=0\nmax=10\ndefault=8\n" + "[[option]]\nfeature=\"bad\"\nid=\"high\"\nlabel=\"High\"\n" + "type=\"integer\"\nmin=0\nmax=10\ndefault=2\n" + "[[constraint]]\nfeature=\"bad\"\n" + "kind=\"ordered_integer\"\ndirection=\"nondecreasing\"\n" + "options=[\"low\",\"high\"]\n"), + "ordered integer defaults must satisfy their constraint"); check(reject_parametric_manifest( "dynamic-step-default", "format_version=2\n"