1use std::cmp::Ordering;
2use std::collections::hash_map::DefaultHasher;
21use std::collections::HashSet;
22use std::hash::{Hash, Hasher};
23use std::ops::RangeInclusive;
24
25pub enum RecordId {
26 FormId(std::num::NonZeroU32),
27 NamespacedId(NamespacedId),
28}
29
30#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
34pub struct NamespacedId {
35 namespace: Namespace,
36 hashed_id: u64,
37}
38
39impl NamespacedId {
40 pub fn new(record_type: [u8; 4], id_data: &[u8]) -> Self {
41 let mut hasher = DefaultHasher::new();
42 id_data.hash(&mut hasher);
43
44 Self {
45 namespace: record_type.into(),
46 hashed_id: hasher.finish(),
47 }
48 }
49}
50
51#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
56pub enum Namespace {
57 Race,
58 Class,
59 Birthsign,
60 Script,
61 Cell,
62 Faction,
63 Sound,
64 Global,
65 Region,
66 Skill,
67 MagicEffect,
68 Land,
69 PathGrid,
70 Dialog,
71 Other,
72}
73
74impl From<[u8; 4]> for Namespace {
75 fn from(record_type: [u8; 4]) -> Namespace {
76 use self::Namespace::*;
77 match &record_type {
78 b"RACE" => Race,
79 b"CLAS" => Class,
80 b"BSGN" => Birthsign,
81 b"SCPT" => Script,
82 b"CELL" => Cell,
83 b"FACT" => Faction,
84 b"SOUN" => Sound,
85 b"GLOB" => Global,
86 b"REGN" => Region,
87 b"SKIL" => Skill,
88 b"MGEF" => MagicEffect,
89 b"LAND" => Land,
90 b"PGRD" => PathGrid,
91 b"DIAL" => Dialog,
92 _ => Other,
93 }
94 }
95}
96
97impl From<Namespace> for u32 {
98 fn from(value: Namespace) -> u32 {
99 value as u32
100 }
101}
102
103#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
104pub enum ObjectIndexMask {
105 Full = 0x00FF_FFFF,
106 Medium = 0x0000_FFFF,
107 Small = 0x0000_0FFF,
108}
109
110impl From<ObjectIndexMask> for u32 {
111 fn from(value: ObjectIndexMask) -> u32 {
112 value as u32
113 }
114}
115
116#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
117pub struct SourcePlugin {
118 pub hashed_name: u64,
119 pub mod_index_mask: u32,
121 pub object_index_mask: u32,
122}
123
124impl SourcePlugin {
125 pub fn master(name: &str, mod_index_mask: u32, object_index_mask: ObjectIndexMask) -> Self {
126 let object_index_mask = u32::from(object_index_mask);
127
128 SourcePlugin {
129 hashed_name: calculate_filename_hash(name),
130 mod_index_mask,
131 object_index_mask,
132 }
133 }
134
135 pub fn parent(name: &str, object_index_mask: ObjectIndexMask) -> Self {
136 let object_index_mask = u32::from(object_index_mask);
137
138 SourcePlugin {
140 hashed_name: calculate_filename_hash(name),
141 mod_index_mask: object_index_mask,
142 object_index_mask,
143 }
144 }
145}
146
147#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
148pub enum RecordIdType {
149 FormId,
150 NamespacedId,
151}
152
153#[derive(Clone, Debug)]
154pub struct ResolvedRecordId {
155 record_id_type: RecordIdType,
156 overridden_record: bool,
157 hashed_data: u64,
158 other_data: u32,
159}
160
161impl ResolvedRecordId {
162 pub fn from_form_id(
163 parent_plugin: SourcePlugin,
164 masters: &[SourcePlugin],
165 raw_form_id: u32,
166 ) -> Self {
167 let source_master = masters
168 .iter()
169 .find(|m| (raw_form_id & !m.object_index_mask) == m.mod_index_mask);
170
171 match source_master {
172 Some(hashed_master) => {
173 let object_index = raw_form_id & hashed_master.object_index_mask;
174 ResolvedRecordId {
175 record_id_type: RecordIdType::FormId,
176 overridden_record: true,
177 hashed_data: hashed_master.hashed_name,
178 other_data: object_index,
179 }
180 }
181 None => {
182 let object_index = raw_form_id & parent_plugin.object_index_mask;
183 ResolvedRecordId {
184 record_id_type: RecordIdType::FormId,
185 overridden_record: false,
186 hashed_data: parent_plugin.hashed_name,
187 other_data: object_index,
188 }
189 }
190 }
191 }
192
193 pub fn from_namespaced_id(
194 namespaced_id: &NamespacedId,
195 masters_record_ids: &HashSet<NamespacedId>,
196 ) -> Self {
197 let overridden_record = masters_record_ids.contains(namespaced_id);
198
199 ResolvedRecordId {
200 record_id_type: RecordIdType::NamespacedId,
201 overridden_record,
202 hashed_data: namespaced_id.hashed_id,
203 other_data: namespaced_id.namespace.into(),
204 }
205 }
206
207 pub fn is_overridden_record(&self) -> bool {
208 self.overridden_record
209 }
210
211 pub fn is_object_index_in(&self, range: &RangeInclusive<u32>) -> bool {
212 match self.record_id_type {
213 RecordIdType::FormId => range.contains(&self.other_data),
214 RecordIdType::NamespacedId => false,
215 }
216 }
217}
218
219impl Ord for ResolvedRecordId {
220 fn cmp(&self, other: &Self) -> Ordering {
221 match self.record_id_type.cmp(&other.record_id_type) {
222 Ordering::Equal => match self.other_data.cmp(&other.other_data) {
223 Ordering::Equal => self.hashed_data.cmp(&other.hashed_data),
224 o => o,
225 },
226 o => o,
227 }
228 }
229}
230
231impl PartialOrd for ResolvedRecordId {
232 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
233 Some(self.cmp(other))
234 }
235}
236
237impl PartialEq for ResolvedRecordId {
238 fn eq(&self, other: &Self) -> bool {
239 self.record_id_type == other.record_id_type
240 && self.other_data == other.other_data
241 && self.hashed_data == other.hashed_data
242 }
243}
244
245impl Eq for ResolvedRecordId {}
246
247impl Hash for ResolvedRecordId {
248 fn hash<H: Hasher>(&self, state: &mut H) {
249 self.record_id_type.hash(state);
250 self.other_data.hash(state);
251 self.hashed_data.hash(state);
252 }
253}
254
255pub fn calculate_filename_hash(string: &str) -> u64 {
256 let mut hasher = DefaultHasher::new();
257 string.to_lowercase().hash(&mut hasher);
258 hasher.finish()
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 const OTHER_RECORD_TYPES: &[&[u8; 4]] = &[
266 b"ACTI", b"ALCH", b"APPA", b"ARMO", b"BODY", b"BOOK", b"CLOT", b"CONT", b"CREA", b"DOOR",
267 b"ENCH", b"GMST", b"INFO", b"INGR", b"LEVC", b"LEVI", b"LIGH", b"LOCK", b"LTEX", b"MISC",
268 b"NPC_", b"PROB", b"REPA", b"SNDG", b"SPEL", b"STAT", b"TES3", b"WEAP",
269 ];
270
271 #[test]
272 fn namespace_from_array_should_namespace_race_class_bsgn_scpt_cell_fact_soun_glob_and_regn() {
273 assert_eq!(Namespace::Race, (*b"RACE").into());
274 assert_eq!(Namespace::Class, (*b"CLAS").into());
275 assert_eq!(Namespace::Birthsign, (*b"BSGN").into());
276 assert_eq!(Namespace::Script, (*b"SCPT").into());
277 assert_eq!(Namespace::Cell, (*b"CELL").into());
278 assert_eq!(Namespace::Faction, (*b"FACT").into());
279 assert_eq!(Namespace::Sound, (*b"SOUN").into());
280 assert_eq!(Namespace::Global, (*b"GLOB").into());
281 assert_eq!(Namespace::Region, (*b"REGN").into());
282 assert_eq!(Namespace::Skill, (*b"SKIL").into());
283 assert_eq!(Namespace::MagicEffect, (*b"MGEF").into());
284 assert_eq!(Namespace::Land, (*b"LAND").into());
285 assert_eq!(Namespace::PathGrid, (*b"PGRD").into());
286 assert_eq!(Namespace::Dialog, (*b"DIAL").into());
287 }
288
289 #[test]
290 fn namespace_from_array_should_put_unrecognised_record_types_into_other_namespace() {
291 assert_eq!(Namespace::Other, (*b" ").into());
292 assert_eq!(Namespace::Other, (*b"DUMY").into());
293
294 for record_type in OTHER_RECORD_TYPES {
295 assert_eq!(Namespace::Other, (**record_type).into());
296 }
297 }
298
299 #[test]
300 fn namespaced_id_new_should_hash_id_data_and_map_record_type_to_namespace() {
301 let data = vec![1, 2, 3, 4];
302 let record_id = NamespacedId::new(*b"BOOK", &data);
303
304 let mut hasher = DefaultHasher::new();
305 data.hash(&mut hasher);
306 let hashed_data = hasher.finish();
307
308 assert_eq!(Namespace::Other, record_id.namespace);
309 assert_eq!(hashed_data, record_id.hashed_id);
310 }
311
312 mod source_plugin {
313 use super::*;
314
315 #[test]
316 fn parent_should_use_object_index_mask_as_mod_index_mask() {
317 let plugin = SourcePlugin::parent("a", ObjectIndexMask::Full);
318
319 assert_eq!(u32::from(ObjectIndexMask::Full), plugin.object_index_mask);
320 assert_eq!(plugin.mod_index_mask, plugin.object_index_mask);
321 }
322 }
323
324 mod resolved_record_id {
325 use super::*;
326
327 const PARENT_PLUGIN_NAME: u64 = 1;
328 const PARENT_PLUGIN: SourcePlugin = SourcePlugin {
329 hashed_name: PARENT_PLUGIN_NAME,
330 mod_index_mask: ObjectIndexMask::Full as u32,
331 object_index_mask: ObjectIndexMask::Full as u32,
332 };
333 const OTHER_PARENT_PLUGIN: SourcePlugin = SourcePlugin {
334 hashed_name: 6,
335 mod_index_mask: ObjectIndexMask::Full as u32,
336 object_index_mask: ObjectIndexMask::Full as u32,
337 };
338 const MASTERS: &[SourcePlugin] = &[
339 SourcePlugin {
340 hashed_name: 2,
341 mod_index_mask: 0,
342 object_index_mask: ObjectIndexMask::Full as u32,
343 },
344 SourcePlugin {
345 hashed_name: 3,
346 mod_index_mask: 0x1200_0000,
347 object_index_mask: ObjectIndexMask::Full as u32,
348 },
349 SourcePlugin {
350 hashed_name: 4,
351 mod_index_mask: 0xFD12_0000,
352 object_index_mask: ObjectIndexMask::Medium as u32,
353 },
354 SourcePlugin {
355 hashed_name: 5,
356 mod_index_mask: 0xFE12_3000,
357 object_index_mask: ObjectIndexMask::Small as u32,
358 },
359 ];
360 const NO_MASTERS: &[SourcePlugin] = &[];
361
362 fn hash(form_id: &ResolvedRecordId) -> u64 {
363 let mut hasher = DefaultHasher::new();
364 form_id.hash(&mut hasher);
365 hasher.finish()
366 }
367
368 #[test]
369 fn new_should_match_override_record_to_master_based_on_mod_index() {
370 let form_id = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x00456789);
371
372 assert!(form_id.is_overridden_record());
373 assert_eq!(0x456789, form_id.other_data);
374 assert_eq!(MASTERS[0].hashed_name, form_id.hashed_data);
375
376 let form_id = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x12456789);
377
378 assert!(form_id.is_overridden_record());
379 assert_eq!(0x456789, form_id.other_data);
380 assert_eq!(MASTERS[1].hashed_name, form_id.hashed_data);
381
382 let form_id = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0xFD126789);
383
384 assert!(form_id.is_overridden_record());
385 assert_eq!(0x6789, form_id.other_data);
386 assert_eq!(MASTERS[2].hashed_name, form_id.hashed_data);
387
388 let form_id = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0xFE123789);
389
390 assert!(form_id.is_overridden_record());
391 assert_eq!(0x789, form_id.other_data);
392 assert_eq!(MASTERS[3].hashed_name, form_id.hashed_data);
393 }
394
395 #[test]
396 fn new_should_create_non_override_formid_if_no_master_mod_indexes_match() {
397 let form_id = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01456789);
398
399 assert!(!form_id.is_overridden_record());
400 assert_eq!(0x456789, form_id.other_data);
401 assert_eq!(PARENT_PLUGIN_NAME, form_id.hashed_data);
402
403 let form_id = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x20456789);
404
405 assert!(!form_id.is_overridden_record());
406 assert_eq!(0x456789, form_id.other_data);
407 assert_eq!(PARENT_PLUGIN_NAME, form_id.hashed_data);
408
409 let form_id = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0xFD216789);
410
411 assert!(!form_id.is_overridden_record());
412 assert_eq!(0x216789, form_id.other_data);
413 assert_eq!(PARENT_PLUGIN_NAME, form_id.hashed_data);
414
415 let form_id = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0xFE321789);
416
417 assert!(!form_id.is_overridden_record());
418 assert_eq!(0x321789, form_id.other_data);
419 assert_eq!(PARENT_PLUGIN_NAME, form_id.hashed_data);
420 }
421
422 #[test]
423 fn new_should_use_parent_source_plugin_other_data_mask_if_no_master_mod_indexes_match() {
424 let parent_plugin = SourcePlugin {
425 hashed_name: PARENT_PLUGIN_NAME,
426 mod_index_mask: u32::from(ObjectIndexMask::Full),
427 object_index_mask: u32::from(ObjectIndexMask::Full),
428 };
429 let form_id = ResolvedRecordId::from_form_id(parent_plugin, MASTERS, 0x01456789);
430
431 assert!(!form_id.is_overridden_record());
432 assert_eq!(0x456789, form_id.other_data);
433 assert_eq!(PARENT_PLUGIN_NAME, form_id.hashed_data);
434
435 let parent_plugin = SourcePlugin {
436 hashed_name: PARENT_PLUGIN_NAME,
437 mod_index_mask: u32::from(ObjectIndexMask::Medium),
438 object_index_mask: u32::from(ObjectIndexMask::Medium),
439 };
440 let form_id = ResolvedRecordId::from_form_id(parent_plugin, MASTERS, 0xFD216789);
441
442 assert!(!form_id.is_overridden_record());
443 assert_eq!(0x6789, form_id.other_data);
444 assert_eq!(PARENT_PLUGIN_NAME, form_id.hashed_data);
445
446 let parent_plugin = SourcePlugin {
447 hashed_name: PARENT_PLUGIN_NAME,
448 mod_index_mask: u32::from(ObjectIndexMask::Small),
449 object_index_mask: u32::from(ObjectIndexMask::Small),
450 };
451 let form_id = ResolvedRecordId::from_form_id(parent_plugin, MASTERS, 0xFE321789);
452
453 assert!(!form_id.is_overridden_record());
454 assert_eq!(0x789, form_id.other_data);
455 assert_eq!(PARENT_PLUGIN_NAME, form_id.hashed_data);
456 }
457
458 #[test]
459 fn form_ids_should_not_be_equal_if_plugin_names_are_unequal() {
460 let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01);
461 let form_id2 = ResolvedRecordId::from_form_id(OTHER_PARENT_PLUGIN, MASTERS, 0x05000001);
462
463 assert_ne!(form_id1, form_id2);
464 }
465
466 #[test]
467 fn form_ids_should_not_be_equal_if_object_indices_are_unequal() {
468 let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01);
469 let form_id2 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x02);
470
471 assert_ne!(form_id1, form_id2);
472 }
473
474 #[test]
475 fn form_ids_with_equal_plugin_names_and_object_ids_should_be_equal() {
476 let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, NO_MASTERS, 0x01);
477 let form_id2 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x05000001);
478
479 assert_eq!(form_id1, form_id2);
480 }
481
482 #[test]
483 fn form_ids_can_be_equal_if_one_is_an_override_record_and_the_other_is_not() {
484 let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01);
485 let form_id2 = ResolvedRecordId::from_form_id(MASTERS[0], NO_MASTERS, 0x05000001);
486
487 assert_ne!(form_id1.overridden_record, form_id2.overridden_record);
488 assert_eq!(form_id1, form_id2);
489 }
490
491 #[test]
492 fn form_ids_should_be_ordered_according_to_object_index_then_hashed_datas() {
493 let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01);
494 let form_id2 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x02);
495
496 assert_eq!(Ordering::Less, form_id1.cmp(&form_id2));
497 assert_eq!(Ordering::Greater, form_id2.cmp(&form_id1));
498
499 let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x05000001);
500 let form_id2 = ResolvedRecordId::from_form_id(OTHER_PARENT_PLUGIN, MASTERS, 0x05000001);
501
502 assert_eq!(Ordering::Less, form_id1.cmp(&form_id2));
503 assert_eq!(Ordering::Greater, form_id2.cmp(&form_id1));
504 }
505
506 #[test]
507 fn form_ids_should_not_be_ordered_according_to_override_record_flag_value() {
508 let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01);
509 let form_id2 = ResolvedRecordId::from_form_id(MASTERS[0], NO_MASTERS, 0x05000001);
510
511 assert_ne!(form_id1.overridden_record, form_id2.overridden_record);
512 assert_eq!(Ordering::Equal, form_id2.cmp(&form_id1));
513 }
514
515 #[test]
516 fn form_id_hashes_should_not_be_equal_if_plugin_names_are_unequal() {
517 let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01);
518 let form_id2 = ResolvedRecordId::from_form_id(OTHER_PARENT_PLUGIN, MASTERS, 0x05000001);
519
520 assert_ne!(hash(&form_id1), hash(&form_id2));
521 }
522
523 #[test]
524 fn form_id_hashes_should_not_be_equal_if_object_indices_are_unequal() {
525 let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01);
526 let form_id2 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x02);
527
528 assert_ne!(hash(&form_id1), hash(&form_id2));
529 }
530
531 #[test]
532 fn form_id_hashes_with_equal_plugin_names_and_object_ids_should_be_equal() {
533 let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, NO_MASTERS, 0x01);
534 let form_id2 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x05000001);
535
536 assert_eq!(hash(&form_id1), hash(&form_id2));
537 }
538
539 #[test]
540 fn form_id_hashes_can_be_equal_with_unequal_override_record_flag_values() {
541 let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01);
542 let form_id2 = ResolvedRecordId::from_form_id(MASTERS[0], NO_MASTERS, 0x05000001);
543
544 assert_ne!(form_id1.overridden_record, form_id2.overridden_record);
545 assert_eq!(hash(&form_id1), hash(&form_id2));
546 }
547 }
548
549 #[test]
550 fn calculate_filename_hash_should_treat_plugin_names_case_insensitively_like_windows() {
551 assert_eq!(calculate_filename_hash("i"), calculate_filename_hash("I"));
560 assert_ne!(
561 calculate_filename_hash("\u{0130}"),
562 calculate_filename_hash("i")
563 );
564 assert_ne!(
565 calculate_filename_hash("\u{0131}"),
566 calculate_filename_hash("i")
567 );
568 assert_ne!(
569 calculate_filename_hash("\u{0131}"),
570 calculate_filename_hash("\u{0130}")
571 );
572
573 assert_eq!(
575 calculate_filename_hash("\u{03a1}"),
576 calculate_filename_hash("\u{03c1}")
577 );
578 assert_ne!(
579 calculate_filename_hash("\u{03a1}"),
580 calculate_filename_hash("\u{03f1}")
581 );
582
583 assert_eq!("\u{03c1}", "\u{03a1}".to_lowercase());
589 assert_eq!("\u{03a1}", "\u{03a1}".to_uppercase());
590
591 assert_eq!("\u{03c1}", "\u{03c1}".to_lowercase());
593 assert_eq!("\u{03a1}", "\u{03c1}".to_uppercase());
594
595 assert_eq!("\u{03f1}", "\u{03f1}".to_lowercase());
597 assert_eq!("\u{03a1}", "\u{03f1}".to_uppercase());
598
599 assert_eq!("i\u{0307}", "\u{0130}".to_lowercase());
601 assert_eq!("\u{0130}", "\u{0130}".to_uppercase());
602
603 assert_eq!("\u{0131}", "\u{0131}".to_lowercase());
605 assert_eq!("I", "\u{0131}".to_uppercase());
606 }
607}