Skip to content

Commit

Permalink
Update valid light plugin FormID ranges
Browse files Browse the repository at this point in the history
Skyrim SE v1.6.1130.0 extends light plugins to support up to 4096
records, like Starfield, if the plugin's HEDR version is at least
1.71. There are some new official and Creation plugins that use the
expanded range.

Fallout 4 v1.10.162 (released in 2019) similarly extended the
range of valid FormIDs from that game, again based on HEDR version.
esplugin previously used that extended range for all Fallout 4
plugins but now it checks the HEDR version.
  • Loading branch information
Ortham committed Dec 6, 2023
1 parent 7308716 commit e570779
Show file tree
Hide file tree
Showing 2 changed files with 217 additions and 210 deletions.
214 changes: 35 additions & 179 deletions src/form_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,22 @@
use std::cmp::Ordering;
use std::hash::{Hash, Hasher};

use crate::game_id::GameId;
use crate::u32_to_usize;

#[derive(Clone, Debug)]
pub struct HashedFormId {
// Precalculate and store whether this FormID is for an overridden record
// for efficiency, as alignment padding means it wastes no space.
overridden_record: bool,
game_id: GameId,
object_index: u32,
hashed_plugin_name: u64,
}

impl HashedFormId {
pub fn new(
game_id: GameId,
hashed_parent_plugin_name: u64,
hashed_masters: &[u64],
raw_form_id: u32,
) -> Self {
pub fn new(hashed_parent_plugin_name: u64, hashed_masters: &[u64], raw_form_id: u32) -> Self {
let mod_index = u32_to_usize(raw_form_id >> 24);
Self {
overridden_record: mod_index < hashed_masters.len(),
game_id,
object_index: raw_form_id & 0xFF_FFFF,
hashed_plugin_name: hashed_masters
.get(mod_index)
Expand All @@ -51,13 +43,8 @@ impl HashedFormId {
}
}

pub fn is_valid_in_light_plugin(&self) -> bool {
match self.game_id {
GameId::SkyrimSE => self.object_index >= 0x800 && self.object_index <= 0xFFF,
GameId::Fallout4 => self.object_index >= 0x001 && self.object_index <= 0xFFF,
GameId::Starfield => self.object_index <= 0xFFF,
_ => false,
}
pub fn object_index(&self) -> u32 {
self.object_index
}

pub fn is_overridden_record(&self) -> bool {
Expand Down Expand Up @@ -115,275 +102,144 @@ mod tests {

#[test]
fn is_overridden_record_should_be_true_if_mod_index_is_less_than_masters_length() {
let form_id = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x01);

assert!(form_id.is_overridden_record());

let form_id = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x01000001);
let form_id = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x01000001);

assert!(form_id.is_overridden_record());
}

#[test]
fn is_overridden_record_should_be_false_if_mod_index_is_not_less_than_masters_length() {
let form_id = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x02000001);
let form_id = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x02000001);

assert!(!form_id.is_overridden_record());

let form_id = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x03000001);
let form_id = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x03000001);

assert!(!form_id.is_overridden_record());
}

#[allow(non_snake_case)]
#[test]
fn is_valid_in_light_plugin_should_be_false_if_game_is_morrowind() {
// Check every possible object ID, it's only takes a second or so.
for index in 0..=0xFF_FFFF {
let form_id = HashedFormId::new(GameId::Morrowind, PARENT_PLUGIN_NAME, MASTERS, index);

assert!(!form_id.is_valid_in_light_plugin());
}
}

#[allow(non_snake_case)]
#[test]
fn is_valid_in_light_plugin_should_be_false_if_game_is_oblivion() {
// Check every possible object ID, it's only takes a second or so.
for index in 0..=0xFF_FFFF {
let form_id = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, index);

assert!(!form_id.is_valid_in_light_plugin());
}
}

#[allow(non_snake_case)]
#[test]
fn is_valid_in_light_plugin_should_be_false_if_game_is_skyrim() {
// Check every possible object ID, it's only takes a second or so.
for index in 0..=0xFF_FFFF {
let form_id = HashedFormId::new(GameId::Skyrim, PARENT_PLUGIN_NAME, MASTERS, index);

assert!(!form_id.is_valid_in_light_plugin());
}
}

#[allow(non_snake_case)]
#[test]
fn is_valid_in_light_plugin_should_be_false_if_game_is_fallout_3() {
// Check every possible object ID, it's only takes a second or so.
for index in 0..=0xFF_FFFF {
let form_id = HashedFormId::new(GameId::Fallout3, PARENT_PLUGIN_NAME, MASTERS, index);

assert!(!form_id.is_valid_in_light_plugin());
}
}

#[allow(non_snake_case)]
#[test]
fn is_valid_in_light_plugin_should_be_false_if_game_is_fallout_nv() {
// Check every possible object ID, it's only takes a second or so.
for index in 0..=0xFF_FFFF {
let form_id = HashedFormId::new(GameId::FalloutNV, PARENT_PLUGIN_NAME, MASTERS, index);

assert!(!form_id.is_valid_in_light_plugin());
}
}

#[allow(non_snake_case)]
#[test]
fn is_valid_in_light_plugin_should_be_true_if_game_is_skyrim_se_and_object_index_is_between_0x800_and_0xFFF_inclusive(
) {
for index in 0x800..=0xFFF {
let form_id = HashedFormId::new(GameId::SkyrimSE, PARENT_PLUGIN_NAME, MASTERS, index);

assert!(form_id.is_valid_in_light_plugin());
}
}

#[allow(non_snake_case)]
#[test]
fn is_valid_in_light_plugin_should_be_true_if_game_is_fallout_4_and_object_index_is_between_0x001_and_0xFFF_inclusive(
) {
for index in 0x001..=0xFFF {
let form_id = HashedFormId::new(GameId::Fallout4, PARENT_PLUGIN_NAME, MASTERS, index);

assert!(form_id.is_valid_in_light_plugin());
}
}

#[allow(non_snake_case)]
#[test]
fn is_valid_in_light_plugin_should_be_false_if_game_is_starfield_and_object_index_is_less_than_0xFFF(
) {
for index in 0..=0xFFF {
let form_id = HashedFormId::new(GameId::Starfield, PARENT_PLUGIN_NAME, MASTERS, index);

assert!(form_id.is_valid_in_light_plugin());
}
}

#[allow(non_snake_case)]
#[test]
fn is_valid_in_light_plugin_should_be_false_if_game_is_skyrim_se_and_object_index_is_outwith_0x800_and_0xFFF_inclusive(
) {
for index in 0..0x800 {
let form_id = HashedFormId::new(GameId::SkyrimSE, PARENT_PLUGIN_NAME, MASTERS, index);

assert!(!form_id.is_valid_in_light_plugin());
}

for index in 0x1000..=0xFF_FFFF {
let form_id = HashedFormId::new(GameId::SkyrimSE, PARENT_PLUGIN_NAME, MASTERS, index);

assert!(!form_id.is_valid_in_light_plugin());
}
}

#[allow(non_snake_case)]
#[test]
fn is_valid_in_light_plugin_should_be_false_if_game_is_fallout_4_and_object_index_is_outwith_0x001_and_0xFFF_inclusive(
) {
let form_id = HashedFormId::new(GameId::Fallout4, PARENT_PLUGIN_NAME, MASTERS, 0);

assert!(!form_id.is_valid_in_light_plugin());

for index in 0x1000..=0xFF_FFFF {
let form_id = HashedFormId::new(GameId::Fallout4, PARENT_PLUGIN_NAME, MASTERS, index);

assert!(!form_id.is_valid_in_light_plugin());
}
}

#[allow(non_snake_case)]
#[test]
fn is_valid_in_light_plugin_should_be_false_if_game_is_starfield_and_object_index_is_greater_than_0xFFF(
) {
for index in 0x1000..=0xFF_FFFF {
let form_id = HashedFormId::new(GameId::Starfield, PARENT_PLUGIN_NAME, MASTERS, index);

assert!(!form_id.is_valid_in_light_plugin());
}
}

#[test]
fn object_index_should_equal_last_three_bytes_of_raw_form_id_value() {
let form_id = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x01);

assert_eq!(0x01, form_id.object_index);

let form_id = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x01000001);
let form_id = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x01000001);

assert_eq!(0x01, form_id.object_index);
}

#[test]
fn should_store_master_at_mod_index_as_plugin_name() {
let form_id = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x01);

assert_eq!(MASTERS[0], form_id.hashed_plugin_name);

let form_id = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x01000001);
let form_id = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x01000001);

assert_eq!(MASTERS[1], form_id.hashed_plugin_name);
}

#[test]
fn should_store_parent_plugin_name_for_mod_index_greater_than_last_index_of_masters() {
let form_id = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, NO_MASTERS, 0x01);
let form_id = HashedFormId::new(PARENT_PLUGIN_NAME, NO_MASTERS, 0x01);

assert_eq!(PARENT_PLUGIN_NAME, form_id.hashed_plugin_name);

let form_id = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x05000001);
let form_id = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x05000001);

assert_eq!(PARENT_PLUGIN_NAME, form_id.hashed_plugin_name);
}

#[test]
fn form_ids_should_not_be_equal_if_plugin_names_are_unequal() {
let form_id1 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(GameId::Oblivion, OTHER_PLUGIN_NAME, MASTERS, 0x05000001);
let form_id1 = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(OTHER_PLUGIN_NAME, MASTERS, 0x05000001);

assert_ne!(form_id1, form_id2);
}

#[test]
fn form_ids_should_not_be_equal_if_object_indices_are_unequal() {
let form_id1 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x02);
let form_id1 = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x02);

assert_ne!(form_id1, form_id2);
}

#[test]
fn form_ids_with_equal_plugin_names_and_object_ids_should_be_equal() {
let form_id1 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, NO_MASTERS, 0x01);
let form_id2 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x05000001);
let form_id1 = HashedFormId::new(PARENT_PLUGIN_NAME, NO_MASTERS, 0x01);
let form_id2 = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x05000001);

assert_eq!(form_id1, form_id2);
}

#[test]
fn form_ids_can_be_equal_if_one_is_an_override_record_and_the_other_is_not() {
let form_id1 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(GameId::Oblivion, MASTERS[0], NO_MASTERS, 0x05000001);
let form_id1 = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(MASTERS[0], NO_MASTERS, 0x05000001);

assert_ne!(form_id1.overridden_record, form_id2.overridden_record);
assert_eq!(form_id1, form_id2);
}

#[test]
fn form_ids_should_be_ordered_according_to_object_index_then_hashed_plugin_names() {
let form_id1 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x02);
let form_id1 = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x02);

assert_eq!(Ordering::Less, form_id1.cmp(&form_id2));
assert_eq!(Ordering::Greater, form_id2.cmp(&form_id1));

let form_id1 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x05000001);
let form_id2 = HashedFormId::new(GameId::Oblivion, OTHER_PLUGIN_NAME, MASTERS, 0x05000001);
let form_id1 = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x05000001);
let form_id2 = HashedFormId::new(OTHER_PLUGIN_NAME, MASTERS, 0x05000001);

assert_eq!(Ordering::Less, form_id1.cmp(&form_id2));
assert_eq!(Ordering::Greater, form_id2.cmp(&form_id1));
}

#[test]
fn form_ids_should_not_be_ordered_according_to_override_record_flag_value() {
let form_id1 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(GameId::Oblivion, MASTERS[0], NO_MASTERS, 0x05000001);
let form_id1 = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(MASTERS[0], NO_MASTERS, 0x05000001);

assert_ne!(form_id1.overridden_record, form_id2.overridden_record);
assert_eq!(Ordering::Equal, form_id2.cmp(&form_id1));
}

#[test]
fn form_id_hashes_should_not_be_equal_if_plugin_names_are_unequal() {
let form_id1 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(GameId::Oblivion, OTHER_PLUGIN_NAME, MASTERS, 0x05000001);
let form_id1 = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(OTHER_PLUGIN_NAME, MASTERS, 0x05000001);

assert_ne!(hash(&form_id1), hash(&form_id2));
}

#[test]
fn form_id_hashes_should_not_be_equal_if_object_indices_are_unequal() {
let form_id1 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x02);
let form_id1 = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x02);

assert_ne!(hash(&form_id1), hash(&form_id2));
}

#[test]
fn form_id_hashes_with_equal_plugin_names_and_object_ids_should_be_equal() {
let form_id1 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, NO_MASTERS, 0x01);
let form_id2 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x05000001);
let form_id1 = HashedFormId::new(PARENT_PLUGIN_NAME, NO_MASTERS, 0x01);
let form_id2 = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x05000001);

assert_eq!(hash(&form_id1), hash(&form_id2));
}

#[test]
fn form_id_hashes_can_be_equal_with_unequal_override_record_flag_values() {
let form_id1 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(GameId::Oblivion, MASTERS[0], NO_MASTERS, 0x05000001);
let form_id1 = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(MASTERS[0], NO_MASTERS, 0x05000001);

assert_ne!(form_id1.overridden_record, form_id2.overridden_record);
assert_eq!(hash(&form_id1), hash(&form_id2));
Expand Down

0 comments on commit e570779

Please sign in to comment.