Skip to content

Commit

Permalink
Add support for Fallout 4 ESLs' extended valid FormID range
Browse files Browse the repository at this point in the history
  • Loading branch information
Ortham committed Dec 1, 2019
1 parent 412550b commit d13da66
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 53 deletions.
187 changes: 140 additions & 47 deletions src/form_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,30 @@
use std::cmp::Ordering;
use std::hash::{Hash, Hasher};

#[derive(Clone, Debug, Default)]
use crate::game_id::GameId;

#[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(hashed_parent_plugin_name: u64, hashed_masters: &[u64], raw_form_id: u32) -> Self {
pub fn new(
game_id: GameId,
hashed_parent_plugin_name: u64,
hashed_masters: &[u64],
raw_form_id: u32,
) -> Self {
let mod_index = raw_form_id >> 24;

Self {
overridden_record: (mod_index as usize) < hashed_masters.len(),
game_id,
object_index: raw_form_id & 0xFF_FFFF,
hashed_plugin_name: hashed_masters
.get(mod_index as usize)
Expand All @@ -43,7 +52,11 @@ impl HashedFormId {
}

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

pub fn is_overridden_record(&self) -> bool {
Expand Down Expand Up @@ -101,173 +114,253 @@ mod tests {

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

assert!(form_id.is_overridden_record());

let form_id = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x01000001);
let form_id = HashedFormId::new(GameId::Oblivion, 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(PARENT_PLUGIN_NAME, MASTERS, 0x02000001);
let form_id = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x02000001);

assert!(!form_id.is_overridden_record());

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

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

#[allow(non_snake_case)]
#[test]
fn valid_in_light_master_should_be_true_if_object_index_is_between_0x800_and_0xFFF_inclusive() {
let form_id = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x800);
fn valid_in_light_master_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.valid_in_light_master());
assert!(!form_id.valid_in_light_master());
}
}

let form_id = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0xD00);
#[allow(non_snake_case)]
#[test]
fn valid_in_light_master_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.valid_in_light_master());
assert!(!form_id.valid_in_light_master());
}
}

let form_id = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0xFFF);
#[allow(non_snake_case)]
#[test]
fn valid_in_light_master_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.valid_in_light_master());
assert!(!form_id.valid_in_light_master());
}
}

#[allow(non_snake_case)]
#[test]
fn valid_in_light_master_should_be_false_if_object_index_is_outwith_0x800_and_0xFFF_inclusive()
{
let form_id = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x7FF);
fn valid_in_light_master_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.valid_in_light_master());
assert!(!form_id.valid_in_light_master());
}
}

#[allow(non_snake_case)]
#[test]
fn valid_in_light_master_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.valid_in_light_master());
}
}

#[allow(non_snake_case)]
#[test]
fn valid_in_light_master_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);

let form_id = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x1000);
assert!(form_id.valid_in_light_master());
}
}

#[allow(non_snake_case)]
#[test]
fn valid_in_light_master_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.valid_in_light_master());
}
}

#[allow(non_snake_case)]
#[test]
fn valid_in_light_master_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.valid_in_light_master());
}

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

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

#[allow(non_snake_case)]
#[test]
fn valid_in_light_master_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.valid_in_light_master());

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

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

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

assert_eq!(0x01, form_id.object_index);

let form_id = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x01000001);
let form_id = HashedFormId::new(GameId::Oblivion, 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(PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x01);

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

let form_id = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x01000001);
let form_id = HashedFormId::new(GameId::Oblivion, 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(PARENT_PLUGIN_NAME, NO_MASTERS, 0x01);
let form_id = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, NO_MASTERS, 0x01);

assert_eq!(PARENT_PLUGIN_NAME, form_id.hashed_plugin_name);

let form_id = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x05000001);
let form_id = HashedFormId::new(GameId::Oblivion, 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(PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(OTHER_PLUGIN_NAME, MASTERS, 0x05000001);
let form_id1 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(GameId::Oblivion, 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(PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x02);
let form_id1 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(GameId::Oblivion, 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(PARENT_PLUGIN_NAME, NO_MASTERS, 0x01);
let form_id2 = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x05000001);
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);

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(PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(MASTERS[0], NO_MASTERS, 0x05000001);
let form_id1 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(GameId::Oblivion, 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(PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x02);
let form_id1 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(GameId::Oblivion, 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(PARENT_PLUGIN_NAME, MASTERS, 0x05000001);
let form_id2 = HashedFormId::new(OTHER_PLUGIN_NAME, MASTERS, 0x05000001);
let form_id1 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x05000001);
let form_id2 = HashedFormId::new(GameId::Oblivion, 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(PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(MASTERS[0], NO_MASTERS, 0x05000001);
let form_id1 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(GameId::Oblivion, 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(PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(OTHER_PLUGIN_NAME, MASTERS, 0x05000001);
let form_id1 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(GameId::Oblivion, 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(PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x02);
let form_id1 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(GameId::Oblivion, 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(PARENT_PLUGIN_NAME, NO_MASTERS, 0x01);
let form_id2 = HashedFormId::new(PARENT_PLUGIN_NAME, MASTERS, 0x05000001);
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);

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(PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(MASTERS[0], NO_MASTERS, 0x05000001);
let form_id1 = HashedFormId::new(GameId::Oblivion, PARENT_PLUGIN_NAME, MASTERS, 0x01);
let form_id2 = HashedFormId::new(GameId::Oblivion, 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
18 changes: 12 additions & 6 deletions src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -374,13 +374,18 @@ fn sorted_slices_intersect<T: PartialOrd>(left: &[T], right: &[T]) -> bool {
false
}

fn hashed_form_ids(form_ids: &[u32], filename: &str, masters: &[String]) -> Vec<HashedFormId> {
fn hashed_form_ids(
form_ids: &[u32],
game_id: GameId,
filename: &str,
masters: &[String],
) -> Vec<HashedFormId> {
let hashed_filename = hash(filename);
let hashed_masters: Vec<_> = masters.iter().map(|m| hash(&m)).collect();

let mut form_ids: Vec<_> = form_ids
.iter()
.map(|form_id| HashedFormId::new(hashed_filename, &hashed_masters, *form_id))
.map(|form_id| HashedFormId::new(game_id, hashed_filename, &hashed_masters, *form_id))
.collect();

form_ids.sort();
Expand Down Expand Up @@ -485,7 +490,7 @@ fn parse_record_ids<'a>(
.map_err(|_| nom::Err::Error((input, nom::error::ErrorKind::Alpha)))?;

let (remaining_input, form_ids) = parse_form_ids(input, game_id)?;
let form_ids = hashed_form_ids(&form_ids, filename, &masters).into();
let form_ids = hashed_form_ids(&form_ids, game_id, filename, &masters).into();

Ok((remaining_input, form_ids))
}
Expand All @@ -503,7 +508,7 @@ fn read_record_ids<R: BufRead + Seek>(
let masters = masters(&header_record)?;

let form_ids = read_form_ids(reader, game_id)?;
let form_ids = hashed_form_ids(&form_ids, filename, &masters).into();
let form_ids = hashed_form_ids(&form_ids, game_id, filename, &masters).into();

Ok(form_ids)
}
Expand Down Expand Up @@ -1607,10 +1612,11 @@ mod tests {
let raw_form_ids = vec![0x0000_0001, 0x0100_0002];

let masters = vec!["tést.esm".to_string()];
let form_ids = hashed_form_ids(&raw_form_ids, "Blàñk.esp", &masters);
let form_ids = hashed_form_ids(&raw_form_ids, GameId::Oblivion, "Blàñk.esp", &masters);

let other_masters = vec!["TÉST.ESM".to_string()];
let other_form_ids = hashed_form_ids(&raw_form_ids, "BLÀÑK.ESP", &other_masters);
let other_form_ids =
hashed_form_ids(&raw_form_ids, GameId::Oblivion, "BLÀÑK.ESP", &other_masters);

assert_eq!(form_ids, other_form_ids);
}
Expand Down

0 comments on commit d13da66

Please sign in to comment.