mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
feat: add ordered mod values and MIPS pair patches
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<std::string> 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<std::string, std::string> when;
|
||||
@@ -121,6 +140,7 @@ struct ModPackage {
|
||||
std::vector<std::string> conflicts;
|
||||
std::vector<ModFeature> features;
|
||||
std::vector<ModOption> options;
|
||||
std::vector<ModConstraint> constraints;
|
||||
std::vector<ModPatch> patches;
|
||||
std::vector<ModOverlay> overlays;
|
||||
std::vector<ModDerivedDisc> derived_discs;
|
||||
|
||||
+272
-15
@@ -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<std::string(const std::string&)>& 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<std::string, std::string>& 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<std::string(const std::string&)>& 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<int64_t>::max() - right) ||
|
||||
(right < 0 && left < std::numeric_limits<int64_t>::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<uint64_t>(value);
|
||||
if (encoding == ModValueEncoding::MipsLuiOriU32) return false;
|
||||
for (uint32_t i = 0; i < size; ++i)
|
||||
out[i] = static_cast<uint8_t>(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<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;
|
||||
} 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);
|
||||
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<uint64_t>(replace_offset);
|
||||
patch.replace_addend =
|
||||
toml::find_or<int64_t>(replacement, "addend", 0);
|
||||
patch.replace_omit_when_default =
|
||||
toml::find_or<bool>(
|
||||
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<uint32_t>(patch.expected[0]) |
|
||||
(static_cast<uint32_t>(patch.expected[1]) << 8) |
|
||||
(static_cast<uint32_t>(patch.expected[2]) << 16) |
|
||||
(static_cast<uint32_t>(patch.expected[3]) << 24);
|
||||
const uint32_t ori =
|
||||
static_cast<uint32_t>(patch.expected[4]) |
|
||||
(static_cast<uint32_t>(patch.expected[5]) << 8) |
|
||||
(static_cast<uint32_t>(patch.expected[6]) << 16) |
|
||||
(static_cast<uint32_t>(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<uint8_t> 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<size_t>(patch->replace_offset));
|
||||
if (write.replacement == write.expected)
|
||||
if (patch->replace_encoding ==
|
||||
ModValueEncoding::MipsLuiOriU32) {
|
||||
if (adjusted < 0 ||
|
||||
static_cast<uint64_t>(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<uint32_t>(adjusted);
|
||||
write.replacement[0] =
|
||||
static_cast<uint8_t>(value >> 16);
|
||||
write.replacement[1] =
|
||||
static_cast<uint8_t>(value >> 24);
|
||||
write.replacement[4] =
|
||||
static_cast<uint8_t>(value);
|
||||
write.replacement[5] =
|
||||
static_cast<uint8_t>(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<size_t>(
|
||||
patch->replace_offset));
|
||||
}
|
||||
if (write.replacement == write.expected &&
|
||||
!patch->replace_omit_when_default)
|
||||
continue;
|
||||
}
|
||||
write.package_id = package->id;
|
||||
|
||||
@@ -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<uint8_t>({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<uint8_t>({
|
||||
0x03, 0x00, 0x01, 0x3c,
|
||||
0x40, 0x0d, 0x21, 0x34}) &&
|
||||
split_write(0x80004018ull) &&
|
||||
split_write(0x80004018ull)->replacement ==
|
||||
std::vector<uint8_t>({
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user