1use std::io::{Cursor, Read, Write};
30
31use crate::gff_helpers::{get_bool, get_string, get_u32, upsert_field};
32use rakata_formats::{
33 gff_schema::{AbsentDefault, FieldLife, FieldSchema, GffSchema, GffType},
34 read_gff, read_gff_from_bytes, write_gff, Gff, GffBinaryError, GffStruct, GffValue,
35};
36use thiserror::Error;
37
38pub const DEFAULT_REPUTATION: u8 = 100;
42
43const MAX_REPUTATION: i64 = 100;
45
46#[derive(Debug, Clone, PartialEq, Default)]
48pub struct Fac {
49 pub factions: Vec<FacFaction>,
51 pub reputations: Vec<FacReputation>,
53}
54
55impl Fac {
56 pub fn new() -> Self {
58 Self::default()
59 }
60
61 pub fn reputation(&self, source: u32, target: u32) -> u8 {
69 self.reputations
70 .iter()
71 .find(|entry| entry.source == source && entry.target == target)
72 .map_or(DEFAULT_REPUTATION, FacReputation::effective_standing)
73 }
74
75 pub fn reaction(&self, source: u32, target: u32) -> FacReaction {
77 FacReaction::from_standing(self.reputation(source, target))
78 }
79
80 pub fn faction(&self, id: u32) -> Option<&FacFaction> {
82 usize::try_from(id).ok().and_then(|i| self.factions.get(i))
83 }
84
85 pub fn from_gff(gff: &Gff) -> Result<Self, FacError> {
92 if gff.file_type != *b"FAC " && gff.file_type != *b"GFF " {
93 return Err(FacError::UnsupportedFileType(gff.file_type));
94 }
95
96 let root = &gff.root;
97 Ok(Self {
98 factions: read_list(root, "FactionList", FacFaction::from_struct)?,
99 reputations: read_list(root, "RepList", FacReputation::from_struct)?,
100 })
101 }
102
103 pub fn to_gff(&self) -> Gff {
114 let mut root = GffStruct::new(-1);
115
116 upsert_field(
117 &mut root,
118 "FactionList",
119 GffValue::List(
120 self.factions
121 .iter()
122 .enumerate()
123 .map(|(index, faction)| faction.to_struct(index))
124 .collect(),
125 ),
126 );
127 upsert_field(
128 &mut root,
129 "RepList",
130 GffValue::List(
131 self.reputations
132 .iter()
133 .filter(|entry| entry.effective_standing() != DEFAULT_REPUTATION)
134 .enumerate()
135 .map(|(index, entry)| entry.to_struct(index))
136 .collect(),
137 ),
138 );
139
140 Gff::new(*b"FAC ", root)
141 }
142}
143
144fn read_list<T>(
146 root: &GffStruct,
147 label: &'static str,
148 build: impl Fn(&GffStruct) -> T,
149) -> Result<Vec<T>, FacError> {
150 match root.field(label) {
151 Some(GffValue::List(entries)) => Ok(entries.iter().map(build).collect()),
152 Some(_) => Err(FacError::TypeMismatch {
153 field: label,
154 expected: "List",
155 }),
156 None => Ok(Vec::new()),
157 }
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct FacFaction {
166 pub name: String,
168 pub parent_id: u32,
177 pub global: bool,
181}
182
183impl Default for FacFaction {
184 fn default() -> Self {
185 Self {
186 name: String::new(),
187 parent_id: u32::MAX,
188 global: true,
190 }
191 }
192}
193
194impl FacFaction {
195 fn from_struct(structure: &GffStruct) -> Self {
196 Self {
197 name: get_string(structure, "FactionName").unwrap_or_default(),
198 parent_id: get_u32(structure, "FactionParentID").unwrap_or(u32::MAX),
199 global: get_bool(structure, "FactionGlobal").unwrap_or(true),
200 }
201 }
202
203 fn to_struct(&self, index: usize) -> GffStruct {
204 let mut structure = GffStruct::new(i32::try_from(index).expect("faction index fits i32"));
205 upsert_field(
206 &mut structure,
207 "FactionName",
208 GffValue::String(self.name.clone()),
209 );
210 upsert_field(
211 &mut structure,
212 "FactionParentID",
213 GffValue::UInt32(self.parent_id),
214 );
215 upsert_field(
216 &mut structure,
217 "FactionGlobal",
218 GffValue::UInt16(u16::from(self.global)),
219 );
220 structure
221 }
222}
223
224#[derive(Debug, Clone, PartialEq, Eq)]
226pub struct FacReputation {
227 pub source: u32,
229 pub target: u32,
231 pub standing: i32,
237}
238
239impl FacReputation {
240 pub fn effective_standing(&self) -> u8 {
244 u8::try_from(i64::from(self.standing).clamp(0, MAX_REPUTATION))
245 .unwrap_or(DEFAULT_REPUTATION)
246 }
247
248 fn from_struct(structure: &GffStruct) -> Self {
249 Self {
250 source: get_u32(structure, "FactionID1").unwrap_or(0),
251 target: get_u32(structure, "FactionID2").unwrap_or(0),
252 standing: read_standing(structure),
253 }
254 }
255
256 fn to_struct(&self, index: usize) -> GffStruct {
257 let mut structure =
258 GffStruct::new(i32::try_from(index).expect("reputation index fits i32"));
259 upsert_field(&mut structure, "FactionID1", GffValue::UInt32(self.source));
260 upsert_field(&mut structure, "FactionID2", GffValue::UInt32(self.target));
261 upsert_field(
262 &mut structure,
263 "FactionRep",
264 GffValue::UInt32(u32::from_ne_bytes(self.standing.to_ne_bytes())),
267 );
268 structure
269 }
270}
271
272fn read_standing(structure: &GffStruct) -> i32 {
281 match structure.field("FactionRep") {
282 Some(GffValue::UInt32(value)) => i32::from_ne_bytes(value.to_ne_bytes()),
283 Some(GffValue::Int32(value)) => *value,
284 Some(GffValue::UInt16(value)) => i32::from(*value),
285 Some(GffValue::Int16(value)) => i32::from(*value),
286 Some(GffValue::UInt8(value)) => i32::from(*value),
287 Some(GffValue::Int8(value)) => i32::from(*value),
288 _ => i32::from(DEFAULT_REPUTATION),
289 }
290}
291
292#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
298pub enum FacReaction {
299 Hostile,
301 Neutral,
303 Friendly,
305}
306
307impl FacReaction {
308 pub fn from_standing(standing: u8) -> Self {
310 match standing {
311 0..=10 => Self::Hostile,
312 11..=89 => Self::Neutral,
313 _ => Self::Friendly,
314 }
315 }
316}
317
318#[derive(Debug, Error)]
320pub enum FacError {
321 #[error("unsupported FAC file type: {0:?}")]
323 UnsupportedFileType([u8; 4]),
324 #[error("FAC field `{field}` has incompatible type (expected {expected})")]
326 TypeMismatch {
327 field: &'static str,
329 expected: &'static str,
331 },
332 #[error(transparent)]
334 Gff(#[from] GffBinaryError),
335}
336
337#[cfg_attr(
339 feature = "tracing",
340 tracing::instrument(level = "debug", skip(reader))
341)]
342pub fn read_fac<R: Read>(reader: &mut R) -> Result<Fac, FacError> {
343 let gff = read_gff(reader)?;
344 Fac::from_gff(&gff)
345}
346
347#[cfg_attr(
349 feature = "tracing",
350 tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
351)]
352pub fn read_fac_from_bytes(bytes: &[u8]) -> Result<Fac, FacError> {
353 let gff = read_gff_from_bytes(bytes)?;
354 Fac::from_gff(&gff)
355}
356
357#[cfg_attr(
359 feature = "tracing",
360 tracing::instrument(level = "debug", skip(writer, fac))
361)]
362pub fn write_fac<W: Write>(writer: &mut W, fac: &Fac) -> Result<(), FacError> {
363 let gff = fac.to_gff();
364 write_gff(writer, &gff)?;
365 Ok(())
366}
367
368#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(fac)))]
370pub fn write_fac_to_vec(fac: &Fac) -> Result<Vec<u8>, FacError> {
371 let mut cursor = Cursor::new(Vec::new());
372 write_fac(&mut cursor, fac)?;
373 Ok(cursor.into_inner())
374}
375
376impl GffSchema for Fac {
377 fn schema() -> &'static [FieldSchema] {
378 static FACTION_CHILDREN: &[FieldSchema] = &[
379 FieldSchema {
380 label: "FactionName",
381 expected_type: GffType::String,
382 life: FieldLife::Live,
383 required: false,
384 absent: AbsentDefault::Unverified,
385 children: None,
386 constraint: None,
387 },
388 FieldSchema {
389 label: "FactionParentID",
390 expected_type: GffType::UInt32,
391 life: FieldLife::Live,
392 required: false,
393 absent: AbsentDefault::Unverified,
394 children: None,
395 constraint: None,
396 },
397 FieldSchema {
398 label: "FactionGlobal",
399 expected_type: GffType::UInt16,
400 life: FieldLife::Live,
401 required: false,
402 absent: AbsentDefault::Unverified,
403 children: None,
404 constraint: None,
405 },
406 ];
407 static REP_CHILDREN: &[FieldSchema] = &[
408 FieldSchema {
409 label: "FactionID1",
410 expected_type: GffType::UInt32,
411 life: FieldLife::Live,
412 required: false,
413 absent: AbsentDefault::Unverified,
414 children: None,
415 constraint: None,
416 },
417 FieldSchema {
418 label: "FactionID2",
419 expected_type: GffType::UInt32,
420 life: FieldLife::Live,
421 required: false,
422 absent: AbsentDefault::Unverified,
423 children: None,
424 constraint: None,
425 },
426 FieldSchema {
427 label: "FactionRep",
428 expected_type: GffType::UInt32,
429 life: FieldLife::Live,
430 required: false,
431 absent: AbsentDefault::Unverified,
432 children: None,
433 constraint: None,
434 },
435 ];
436 static SCHEMA: &[FieldSchema] = &[
437 FieldSchema {
438 label: "FactionList",
439 expected_type: GffType::List,
440 life: FieldLife::Live,
441 required: false,
442 absent: AbsentDefault::Unverified,
443 children: Some(FACTION_CHILDREN),
444 constraint: None,
445 },
446 FieldSchema {
447 label: "RepList",
448 expected_type: GffType::List,
449 life: FieldLife::Live,
450 required: false,
451 absent: AbsentDefault::Unverified,
452 children: Some(REP_CHILDREN),
453 constraint: None,
454 },
455 ];
456 SCHEMA
457 }
458}
459
460#[cfg(test)]
461mod tests {
462 use super::*;
463
464 fn sample() -> Fac {
465 Fac {
466 factions: vec![
467 FacFaction {
468 name: "PLAYER".to_string(),
469 ..FacFaction::default()
470 },
471 FacFaction {
472 name: "Hostile".to_string(),
473 parent_id: 0,
474 global: false,
475 },
476 ],
477 reputations: vec![FacReputation {
478 source: 0,
479 target: 1,
480 standing: 5,
481 }],
482 }
483 }
484
485 #[test]
486 fn round_trips_through_gff() {
487 let fac = sample();
488
489 let bytes = write_fac_to_vec(&fac).expect("writes");
490 let parsed = read_fac_from_bytes(&bytes).expect("reads back");
491
492 assert_eq!(parsed, fac);
493 }
494
495 #[test]
496 fn an_absent_pair_is_friendly_not_hostile() {
497 let fac = sample();
498
499 assert_eq!(fac.reputation(0, 1), 5);
501 assert_eq!(fac.reputation(1, 0), DEFAULT_REPUTATION);
502 assert_eq!(fac.reaction(1, 0), FacReaction::Friendly);
503 assert_eq!(fac.reaction(0, 1), FacReaction::Hostile);
504 }
505
506 #[test]
507 fn an_unknown_faction_id_still_answers_with_the_baseline() {
508 let fac = sample();
509
510 assert_eq!(fac.reputation(99, 42), DEFAULT_REPUTATION);
511 assert!(fac.faction(99).is_none());
512 }
513
514 #[test]
515 fn reaction_bands_match_the_documented_boundaries() {
516 assert_eq!(FacReaction::from_standing(0), FacReaction::Hostile);
517 assert_eq!(FacReaction::from_standing(10), FacReaction::Hostile);
518 assert_eq!(FacReaction::from_standing(11), FacReaction::Neutral);
519 assert_eq!(FacReaction::from_standing(89), FacReaction::Neutral);
520 assert_eq!(FacReaction::from_standing(90), FacReaction::Friendly);
521 assert_eq!(FacReaction::from_standing(100), FacReaction::Friendly);
522 }
523
524 #[test]
525 fn a_missing_faction_global_defaults_to_true() {
526 let mut structure = GffStruct::new(0);
527 structure.push_field("FactionName", GffValue::String("X".to_string()));
528
529 assert!(FacFaction::from_struct(&structure).global);
530 }
531
532 #[test]
533 fn a_negative_standing_reads_as_negative_not_as_four_billion() {
534 let mut structure = GffStruct::new(0);
537 structure.push_field("FactionRep", GffValue::UInt32(u32::MAX));
538
539 assert_eq!(read_standing(&structure), -1);
540 }
541
542 #[test]
543 fn out_of_range_standings_are_kept_but_clamp_when_asked() {
544 let hostile = FacReputation {
545 source: 0,
546 target: 1,
547 standing: -1,
548 };
549 let over = FacReputation {
550 source: 0,
551 target: 2,
552 standing: 500,
553 };
554
555 assert_eq!(hostile.standing, -1);
557 assert_eq!(over.standing, 500);
558 assert_eq!(hostile.effective_standing(), 0);
560 assert_eq!(over.effective_standing(), 100);
561 }
562
563 #[test]
564 fn a_pair_at_the_default_is_not_written() {
565 let fac = Fac {
566 factions: vec![FacFaction::default()],
567 reputations: vec![
568 FacReputation {
569 source: 0,
570 target: 0,
571 standing: i32::from(DEFAULT_REPUTATION),
572 },
573 FacReputation {
574 source: 0,
575 target: 1,
576 standing: 5,
577 },
578 ],
579 };
580
581 let bytes = write_fac_to_vec(&fac).expect("writes");
582 let parsed = read_fac_from_bytes(&bytes).expect("reads back");
583
584 assert_eq!(parsed.reputations.len(), 1);
587 assert_eq!(parsed.reputations[0].target, 1);
588 assert_eq!(parsed.reputation(0, 0), DEFAULT_REPUTATION);
589 }
590
591 #[test]
592 fn an_out_of_range_standing_survives_a_round_trip() {
593 let fac = Fac {
594 factions: vec![FacFaction::default()],
595 reputations: vec![FacReputation {
596 source: 0,
597 target: 0,
598 standing: -12,
599 }],
600 };
601
602 let bytes = write_fac_to_vec(&fac).expect("writes");
603 let parsed = read_fac_from_bytes(&bytes).expect("reads back");
604
605 assert_eq!(parsed.reputations[0].standing, -12);
606 }
607
608 #[test]
609 fn a_non_list_faction_field_is_rejected() {
610 let mut root = GffStruct::new(-1);
611 root.push_field("FactionList", GffValue::UInt32(7));
612 let gff = Gff::new(*b"FAC ", root);
613
614 let error = Fac::from_gff(&gff).expect_err("not a list");
615
616 assert!(matches!(
617 error,
618 FacError::TypeMismatch {
619 field: "FactionList",
620 ..
621 }
622 ));
623 }
624
625 #[test]
626 fn an_unrelated_file_type_is_rejected() {
627 let gff = Gff::new(*b"UTC ", GffStruct::new(-1));
628
629 assert!(matches!(
630 Fac::from_gff(&gff),
631 Err(FacError::UnsupportedFileType(_))
632 ));
633 }
634
635 #[test]
636 fn an_empty_table_reads_as_empty_rather_than_failing() {
637 let gff = Gff::new(*b"FAC ", GffStruct::new(-1));
638
639 let fac = Fac::from_gff(&gff).expect("empty FAC is valid");
640
641 assert!(fac.factions.is_empty());
642 assert!(fac.reputations.is_empty());
643 }
644}