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