1use std::io::{Cursor, Read, Write};
31
32use crate::gff_helpers::{
33 get_bool, get_f32, get_i32, get_locstring, get_resref, get_string, get_u16, get_u32, get_u64,
34 get_u8, upsert_field,
35};
36use rakata_core::{ResRef, StrRef};
37use rakata_formats::{
38 gff_schema::{FieldSchema, GffSchema, GffType},
39 read_gff, read_gff_from_bytes, write_gff, Gff, GffBinaryError, GffLocalizedString, GffStruct,
40 GffValue,
41};
42use thiserror::Error;
43
44#[derive(Debug, Clone, PartialEq)]
46pub struct IfoArea {
47 pub area_name: ResRef,
49 pub object_id: u32,
51}
52
53#[derive(Debug, Clone, PartialEq)]
55pub struct IfoExpansion {
56 pub expansion_name: GffLocalizedString,
58 pub expansion_id: i32,
60}
61
62#[derive(Debug, Clone, PartialEq)]
64pub struct IfoCutScene {
65 pub cutscene_name: ResRef,
67 pub cutscene_id: u32,
69}
70
71#[derive(Debug, Clone, PartialEq)]
73pub struct IfoPlayer {
74 pub community_name: String,
76 pub first_name: GffLocalizedString,
78 pub last_name: GffLocalizedString,
80 pub is_primary_player: bool,
82}
83
84#[derive(Debug, Clone, PartialEq)]
86pub struct IfoToken {
87 pub token_number: u32,
89 pub token_value: String,
91}
92
93#[derive(Debug, Clone, PartialEq)]
95pub struct Ifo {
96 pub is_save_game: bool,
99 pub is_nwm_file: bool,
101 pub nwm_res_name: String,
103
104 pub module_id: [u8; 32],
107 pub creator_id: i32,
109 pub version: u32,
111 pub tag: String,
113 pub name: GffLocalizedString,
115 pub description: GffLocalizedString,
117
118 pub start_movie: ResRef,
121 pub entry_area: ResRef,
123 pub entry_x: f32,
125 pub entry_y: f32,
127 pub entry_z: f32,
129 pub entry_dir_x: f32,
131 pub entry_dir_y: f32,
133
134 pub min_per_hour: u8,
137 pub dawn_hour: u8,
139 pub dusk_hour: u8,
141 pub xp_scale: u8,
143
144 pub start_year: u32,
147 pub start_month: u8,
149 pub start_day: u8,
151 pub start_hour: u8,
153 pub start_minute: u16,
155 pub start_second: u16,
157 pub start_millisecond: u16,
159 pub transition: u32,
161 pub pause_time: u32,
163 pub pause_day: u32,
165
166 pub effect_next_id: u64,
169 pub next_char_id_0: u32,
171 pub next_char_id_1: u32,
173 pub next_obj_id_0: u32,
175 pub next_obj_id_1: u32,
177
178 pub hak: String,
181
182 pub on_heartbeat: ResRef,
185 pub on_user_defined: ResRef,
187 pub on_mod_load: ResRef,
189 pub on_mod_start: ResRef,
191 pub on_client_enter: ResRef,
193 pub on_client_leave: ResRef,
195 pub on_activate_item: ResRef,
197 pub on_acquire_item: ResRef,
199 pub on_unacquire_item: ResRef,
201 pub on_player_death: ResRef,
203 pub on_player_dying: ResRef,
205 pub on_spawn_btn_down: ResRef,
207 pub on_player_rest: ResRef,
209 pub on_player_level_up: ResRef,
211 pub on_equip_item: ResRef,
213
214 pub areas: Vec<IfoArea>,
217 pub expansion_list: Vec<IfoExpansion>,
219 pub cutscene_list: Vec<IfoCutScene>,
221 pub player_list: Vec<IfoPlayer>,
223 pub tokens: Vec<IfoToken>,
225}
226
227impl Default for Ifo {
228 fn default() -> Self {
229 Self {
230 is_save_game: false,
231 is_nwm_file: false,
232 nwm_res_name: String::new(),
233 module_id: [0u8; 32],
234 creator_id: 0,
235 version: 0,
236 tag: String::new(),
237 name: GffLocalizedString::new(StrRef::invalid()),
238 description: GffLocalizedString::new(StrRef::invalid()),
239 start_movie: ResRef::blank(),
240 entry_area: ResRef::blank(),
241 entry_x: 0.0,
242 entry_y: 0.0,
243 entry_z: 0.0,
244 entry_dir_x: 0.0,
245 entry_dir_y: 0.0,
246 min_per_hour: 0,
247 dawn_hour: 0,
248 dusk_hour: 0,
249 xp_scale: 10,
250 start_year: 1340,
251 start_month: 6,
252 start_day: 1,
253 start_hour: 23,
254 start_minute: 0,
255 start_second: 0,
256 start_millisecond: 0,
257 transition: 0,
258 pause_time: 0,
259 pause_day: 0,
260 effect_next_id: 0,
261 next_char_id_0: 0,
262 next_char_id_1: 0,
263 next_obj_id_0: 0,
264 next_obj_id_1: 0,
265 hak: String::new(),
266 on_heartbeat: ResRef::blank(),
267 on_user_defined: ResRef::blank(),
268 on_mod_load: ResRef::blank(),
269 on_mod_start: ResRef::blank(),
270 on_client_enter: ResRef::blank(),
271 on_client_leave: ResRef::blank(),
272 on_activate_item: ResRef::blank(),
273 on_acquire_item: ResRef::blank(),
274 on_unacquire_item: ResRef::blank(),
275 on_player_death: ResRef::blank(),
276 on_player_dying: ResRef::blank(),
277 on_spawn_btn_down: ResRef::blank(),
278 on_player_rest: ResRef::blank(),
279 on_player_level_up: ResRef::blank(),
280 on_equip_item: ResRef::blank(),
281 areas: Vec::new(),
282 expansion_list: Vec::new(),
283 cutscene_list: Vec::new(),
284 player_list: Vec::new(),
285 tokens: Vec::new(),
286 }
287 }
288}
289
290impl Ifo {
291 pub fn new() -> Self {
293 Self::default()
294 }
295
296 pub fn from_gff(gff: &Gff) -> Result<Self, IfoError> {
298 if gff.file_type != *b"IFO " && gff.file_type != *b"GFF " {
299 return Err(IfoError::UnsupportedFileType(gff.file_type));
300 }
301
302 let root = &gff.root;
303
304 let is_save_game = get_bool(root, "Mod_IsSaveGame").unwrap_or(false);
305 let is_nwm_file = get_bool(root, "Mod_IsNWMFile").unwrap_or(false);
306 let nwm_res_name = if is_nwm_file {
307 get_string(root, "Mod_NWMResName").unwrap_or_default()
308 } else {
309 String::new()
310 };
311
312 let module_id = match root.field("Mod_ID") {
313 Some(GffValue::Binary(data)) if data.len() == 32 => {
314 <[u8; 32]>::try_from(data.as_slice()).expect("length verified above")
315 }
316 _ => [0u8; 32],
317 };
318
319 let areas = match root.field("Mod_Area_list") {
320 Some(GffValue::List(elements)) => elements
321 .iter()
322 .map(|s| IfoArea {
323 area_name: get_resref(s, "Area_Name").unwrap_or_default(),
324 object_id: get_u32(s, "ObjectId").unwrap_or(0),
325 })
326 .collect(),
327 _ => Vec::new(),
328 };
329
330 let expansion_list = match root.field("Mod_Expan_List") {
331 Some(GffValue::List(elements)) => elements
332 .iter()
333 .map(|s| IfoExpansion {
334 expansion_name: get_locstring(s, "Expansion_Name")
335 .cloned()
336 .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
337 expansion_id: get_i32(s, "Expansion_ID").unwrap_or(0),
338 })
339 .collect(),
340 _ => Vec::new(),
341 };
342
343 let cutscene_list = match root.field("Mod_CutSceneList") {
344 Some(GffValue::List(elements)) => elements
345 .iter()
346 .map(|s| IfoCutScene {
347 cutscene_name: get_resref(s, "CutScene_Name").unwrap_or_default(),
348 cutscene_id: get_u32(s, "CutScene_ID").unwrap_or(0),
349 })
350 .collect(),
351 _ => Vec::new(),
352 };
353
354 let player_list = match root.field("Mod_PlayerList") {
355 Some(GffValue::List(elements)) => elements
356 .iter()
357 .map(|s| IfoPlayer {
358 community_name: get_string(s, "Mod_CommntyName").unwrap_or_default(),
359 first_name: get_locstring(s, "Mod_FirstName")
360 .cloned()
361 .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
362 last_name: get_locstring(s, "Mod_LastName")
363 .cloned()
364 .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
365 is_primary_player: get_bool(s, "Mod_IsPrimaryPlr").unwrap_or(false),
366 })
367 .collect(),
368 _ => Vec::new(),
369 };
370
371 let tokens_list = match root.field("Mod_Tokens") {
372 Some(GffValue::List(elements)) => elements
373 .iter()
374 .map(|s| IfoToken {
375 token_number: get_u32(s, "Mod_TokensNumber").unwrap_or(0),
376 token_value: get_string(s, "Mod_TokensValue").unwrap_or_default(),
377 })
378 .collect(),
379 _ => Vec::new(),
380 };
381
382 Ok(Self {
383 is_save_game,
384 is_nwm_file,
385 nwm_res_name,
386 module_id,
387 creator_id: get_i32(root, "Mod_Creator_ID").unwrap_or(0),
388 version: get_u32(root, "Mod_Version").unwrap_or(0),
389 tag: get_string(root, "Mod_Tag").unwrap_or_default(),
390 name: get_locstring(root, "Mod_Name")
391 .cloned()
392 .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
393 description: get_locstring(root, "Mod_Description")
394 .cloned()
395 .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
396 start_movie: get_resref(root, "Mod_StartMovie").unwrap_or_default(),
397 entry_area: get_resref(root, "Mod_Entry_Area").unwrap_or_default(),
398 entry_x: get_f32(root, "Mod_Entry_X").unwrap_or(0.0),
399 entry_y: get_f32(root, "Mod_Entry_Y").unwrap_or(0.0),
400 entry_z: get_f32(root, "Mod_Entry_Z").unwrap_or(0.0),
401 entry_dir_x: get_f32(root, "Mod_Entry_Dir_X").unwrap_or(0.0),
402 entry_dir_y: get_f32(root, "Mod_Entry_Dir_Y").unwrap_or(0.0),
403 min_per_hour: get_u8(root, "Mod_MinPerHour").unwrap_or(0),
404 dawn_hour: get_u8(root, "Mod_DawnHour").unwrap_or(0),
405 dusk_hour: get_u8(root, "Mod_DuskHour").unwrap_or(0),
406 xp_scale: get_u8(root, "Mod_XPScale").unwrap_or(10),
407 start_year: get_u32(root, "Mod_StartYear").unwrap_or(1340),
408 start_month: get_u8(root, "Mod_StartMonth").unwrap_or(6),
409 start_day: get_u8(root, "Mod_StartDay").unwrap_or(1),
410 start_hour: get_u8(root, "Mod_StartHour").unwrap_or(23),
411 start_minute: get_u16(root, "Mod_StartMinute").unwrap_or(0),
412 start_second: get_u16(root, "Mod_StartSecond").unwrap_or(0),
413 start_millisecond: get_u16(root, "Mod_StartMiliSec").unwrap_or(0),
414 transition: get_u32(root, "Mod_Transition").unwrap_or(0),
415 pause_time: get_u32(root, "Mod_PauseTime").unwrap_or(0),
416 pause_day: get_u32(root, "Mod_PauseDay").unwrap_or(0),
417 effect_next_id: get_u64(root, "Mod_Effect_NxtId").unwrap_or(0),
418 next_char_id_0: get_u32(root, "Mod_NextCharId0").unwrap_or(0),
419 next_char_id_1: get_u32(root, "Mod_NextCharId1").unwrap_or(0),
420 next_obj_id_0: get_u32(root, "Mod_NextObjId0").unwrap_or(0),
421 next_obj_id_1: get_u32(root, "Mod_NextObjId1").unwrap_or(0),
422 hak: get_string(root, "Mod_Hak").unwrap_or_default(),
423 on_heartbeat: get_resref(root, "Mod_OnHeartbeat").unwrap_or_default(),
424 on_user_defined: get_resref(root, "Mod_OnUsrDefined").unwrap_or_default(),
425 on_mod_load: get_resref(root, "Mod_OnModLoad").unwrap_or_default(),
426 on_mod_start: get_resref(root, "Mod_OnModStart").unwrap_or_default(),
427 on_client_enter: get_resref(root, "Mod_OnClientEntr").unwrap_or_default(),
428 on_client_leave: get_resref(root, "Mod_OnClientLeav").unwrap_or_default(),
429 on_activate_item: get_resref(root, "Mod_OnActvtItem").unwrap_or_default(),
430 on_acquire_item: get_resref(root, "Mod_OnAcquirItem").unwrap_or_default(),
431 on_unacquire_item: get_resref(root, "Mod_OnUnAqreItem").unwrap_or_default(),
432 on_player_death: get_resref(root, "Mod_OnPlrDeath").unwrap_or_default(),
433 on_player_dying: get_resref(root, "Mod_OnPlrDying").unwrap_or_default(),
434 on_spawn_btn_down: get_resref(root, "Mod_OnSpawnBtnDn").unwrap_or_default(),
435 on_player_rest: get_resref(root, "Mod_OnPlrRest").unwrap_or_default(),
436 on_player_level_up: get_resref(root, "Mod_OnPlrLvlUp").unwrap_or_default(),
437 on_equip_item: get_resref(root, "Mod_OnEquipItem").unwrap_or_default(),
438 areas,
439 expansion_list,
440 cutscene_list,
441 player_list,
442 tokens: tokens_list,
443 })
444 }
445
446 pub fn to_gff(&self) -> Gff {
450 let mut root = GffStruct::new(-1);
451
452 upsert_field(
454 &mut root,
455 "Mod_IsSaveGame",
456 GffValue::UInt8(u8::from(self.is_save_game)),
457 );
458 upsert_field(
459 &mut root,
460 "Mod_IsNWMFile",
461 GffValue::UInt8(u8::from(self.is_nwm_file)),
462 );
463 if self.is_nwm_file {
464 upsert_field(
465 &mut root,
466 "Mod_NWMResName",
467 GffValue::String(self.nwm_res_name.clone()),
468 );
469 }
470
471 upsert_field(
473 &mut root,
474 "Mod_ID",
475 GffValue::Binary(self.module_id.to_vec()),
476 );
477 upsert_field(
478 &mut root,
479 "Mod_Creator_ID",
480 GffValue::Int32(self.creator_id),
481 );
482 upsert_field(&mut root, "Mod_Version", GffValue::UInt32(self.version));
483 upsert_field(&mut root, "Mod_Tag", GffValue::String(self.tag.clone()));
484 upsert_field(
485 &mut root,
486 "Mod_Name",
487 GffValue::LocalizedString(self.name.clone()),
488 );
489 upsert_field(
490 &mut root,
491 "Mod_Description",
492 GffValue::LocalizedString(self.description.clone()),
493 );
494
495 upsert_field(
497 &mut root,
498 "Mod_StartMovie",
499 GffValue::ResRef(self.start_movie),
500 );
501 upsert_field(
502 &mut root,
503 "Mod_Entry_Area",
504 GffValue::ResRef(self.entry_area),
505 );
506 upsert_field(&mut root, "Mod_Entry_X", GffValue::Single(self.entry_x));
507 upsert_field(&mut root, "Mod_Entry_Y", GffValue::Single(self.entry_y));
508 upsert_field(&mut root, "Mod_Entry_Z", GffValue::Single(self.entry_z));
509 upsert_field(
510 &mut root,
511 "Mod_Entry_Dir_X",
512 GffValue::Single(self.entry_dir_x),
513 );
514 upsert_field(
515 &mut root,
516 "Mod_Entry_Dir_Y",
517 GffValue::Single(self.entry_dir_y),
518 );
519
520 upsert_field(
522 &mut root,
523 "Mod_MinPerHour",
524 GffValue::UInt8(self.min_per_hour),
525 );
526 upsert_field(&mut root, "Mod_DawnHour", GffValue::UInt8(self.dawn_hour));
527 upsert_field(&mut root, "Mod_DuskHour", GffValue::UInt8(self.dusk_hour));
528 upsert_field(&mut root, "Mod_XPScale", GffValue::UInt8(self.xp_scale));
529
530 upsert_field(
532 &mut root,
533 "Mod_StartYear",
534 GffValue::UInt32(self.start_year),
535 );
536 upsert_field(
537 &mut root,
538 "Mod_StartMonth",
539 GffValue::UInt8(self.start_month),
540 );
541 upsert_field(&mut root, "Mod_StartDay", GffValue::UInt8(self.start_day));
542 upsert_field(&mut root, "Mod_StartHour", GffValue::UInt8(self.start_hour));
543 upsert_field(
544 &mut root,
545 "Mod_StartMinute",
546 GffValue::UInt16(self.start_minute),
547 );
548 upsert_field(
549 &mut root,
550 "Mod_StartSecond",
551 GffValue::UInt16(self.start_second),
552 );
553 upsert_field(
554 &mut root,
555 "Mod_StartMiliSec",
556 GffValue::UInt16(self.start_millisecond),
557 );
558 upsert_field(
559 &mut root,
560 "Mod_Transition",
561 GffValue::UInt32(self.transition),
562 );
563 upsert_field(
564 &mut root,
565 "Mod_PauseTime",
566 GffValue::UInt32(self.pause_time),
567 );
568 upsert_field(&mut root, "Mod_PauseDay", GffValue::UInt32(self.pause_day));
569
570 upsert_field(
572 &mut root,
573 "Mod_Effect_NxtId",
574 GffValue::UInt64(self.effect_next_id),
575 );
576 upsert_field(
577 &mut root,
578 "Mod_NextCharId0",
579 GffValue::UInt32(self.next_char_id_0),
580 );
581 upsert_field(
582 &mut root,
583 "Mod_NextCharId1",
584 GffValue::UInt32(self.next_char_id_1),
585 );
586 upsert_field(
587 &mut root,
588 "Mod_NextObjId0",
589 GffValue::UInt32(self.next_obj_id_0),
590 );
591 upsert_field(
592 &mut root,
593 "Mod_NextObjId1",
594 GffValue::UInt32(self.next_obj_id_1),
595 );
596
597 if !self.hak.is_empty() {
599 upsert_field(&mut root, "Mod_Hak", GffValue::String(self.hak.clone()));
600 }
601
602 upsert_field(
604 &mut root,
605 "Mod_OnHeartbeat",
606 GffValue::ResRef(self.on_heartbeat),
607 );
608 upsert_field(
609 &mut root,
610 "Mod_OnUsrDefined",
611 GffValue::ResRef(self.on_user_defined),
612 );
613 upsert_field(
614 &mut root,
615 "Mod_OnModLoad",
616 GffValue::ResRef(self.on_mod_load),
617 );
618 upsert_field(
619 &mut root,
620 "Mod_OnModStart",
621 GffValue::ResRef(self.on_mod_start),
622 );
623 upsert_field(
624 &mut root,
625 "Mod_OnClientEntr",
626 GffValue::ResRef(self.on_client_enter),
627 );
628 upsert_field(
629 &mut root,
630 "Mod_OnClientLeav",
631 GffValue::ResRef(self.on_client_leave),
632 );
633 upsert_field(
634 &mut root,
635 "Mod_OnActvtItem",
636 GffValue::ResRef(self.on_activate_item),
637 );
638 upsert_field(
639 &mut root,
640 "Mod_OnAcquirItem",
641 GffValue::ResRef(self.on_acquire_item),
642 );
643 upsert_field(
644 &mut root,
645 "Mod_OnUnAqreItem",
646 GffValue::ResRef(self.on_unacquire_item),
647 );
648 upsert_field(
649 &mut root,
650 "Mod_OnPlrDeath",
651 GffValue::ResRef(self.on_player_death),
652 );
653 upsert_field(
654 &mut root,
655 "Mod_OnPlrDying",
656 GffValue::ResRef(self.on_player_dying),
657 );
658 upsert_field(
659 &mut root,
660 "Mod_OnSpawnBtnDn",
661 GffValue::ResRef(self.on_spawn_btn_down),
662 );
663 upsert_field(
664 &mut root,
665 "Mod_OnPlrRest",
666 GffValue::ResRef(self.on_player_rest),
667 );
668 upsert_field(
669 &mut root,
670 "Mod_OnPlrLvlUp",
671 GffValue::ResRef(self.on_player_level_up),
672 );
673 upsert_field(
674 &mut root,
675 "Mod_OnEquipItem",
676 GffValue::ResRef(self.on_equip_item),
677 );
678
679 let area_structs: Vec<GffStruct> = self
681 .areas
682 .iter()
683 .map(|area| {
684 let mut s = GffStruct::new(6);
685 s.push_field("Area_Name", GffValue::ResRef(area.area_name));
686 if area.object_id != 0 {
687 s.push_field("ObjectId", GffValue::UInt32(area.object_id));
688 }
689 s
690 })
691 .collect();
692 upsert_field(&mut root, "Mod_Area_list", GffValue::List(area_structs));
693
694 let expansion_structs: Vec<GffStruct> = self
696 .expansion_list
697 .iter()
698 .map(|exp| {
699 let mut s = GffStruct::new(0);
700 s.push_field(
701 "Expansion_Name",
702 GffValue::LocalizedString(exp.expansion_name.clone()),
703 );
704 s.push_field("Expansion_ID", GffValue::Int32(exp.expansion_id));
705 s
706 })
707 .collect();
708 upsert_field(
709 &mut root,
710 "Mod_Expan_List",
711 GffValue::List(expansion_structs),
712 );
713
714 let cutscene_structs: Vec<GffStruct> = self
716 .cutscene_list
717 .iter()
718 .map(|cs| {
719 let mut s = GffStruct::new(1);
720 s.push_field("CutScene_Name", GffValue::ResRef(cs.cutscene_name));
721 s.push_field("CutScene_ID", GffValue::UInt32(cs.cutscene_id));
722 s
723 })
724 .collect();
725 upsert_field(
726 &mut root,
727 "Mod_CutSceneList",
728 GffValue::List(cutscene_structs),
729 );
730
731 if !self.player_list.is_empty() {
733 let player_structs: Vec<GffStruct> = self
734 .player_list
735 .iter()
736 .map(|p| {
737 let mut s = GffStruct::new(0);
738 s.push_field(
739 "Mod_CommntyName",
740 GffValue::String(p.community_name.clone()),
741 );
742 s.push_field(
743 "Mod_FirstName",
744 GffValue::LocalizedString(p.first_name.clone()),
745 );
746 s.push_field(
747 "Mod_LastName",
748 GffValue::LocalizedString(p.last_name.clone()),
749 );
750 s.push_field(
751 "Mod_IsPrimaryPlr",
752 GffValue::UInt8(u8::from(p.is_primary_player)),
753 );
754 s
755 })
756 .collect();
757 upsert_field(&mut root, "Mod_PlayerList", GffValue::List(player_structs));
758 }
759
760 if !self.tokens.is_empty() {
762 let token_structs: Vec<GffStruct> = self
763 .tokens
764 .iter()
765 .map(|t| {
766 let mut s = GffStruct::new(7);
767 s.push_field("Mod_TokensNumber", GffValue::UInt32(t.token_number));
768 s.push_field("Mod_TokensValue", GffValue::String(t.token_value.clone()));
769 s
770 })
771 .collect();
772 upsert_field(&mut root, "Mod_Tokens", GffValue::List(token_structs));
773 }
774
775 Gff::new(*b"IFO ", root)
776 }
777}
778
779#[derive(Debug, Error)]
781pub enum IfoError {
782 #[error("unsupported IFO file type: {0:?}")]
784 UnsupportedFileType([u8; 4]),
785 #[error(transparent)]
787 Gff(#[from] GffBinaryError),
788}
789
790#[cfg_attr(
792 feature = "tracing",
793 tracing::instrument(level = "debug", skip(reader))
794)]
795pub fn read_ifo<R: Read>(reader: &mut R) -> Result<Ifo, IfoError> {
796 let gff = read_gff(reader)?;
797 Ifo::from_gff(&gff)
798}
799
800#[cfg_attr(
802 feature = "tracing",
803 tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
804)]
805pub fn read_ifo_from_bytes(bytes: &[u8]) -> Result<Ifo, IfoError> {
806 let gff = read_gff_from_bytes(bytes)?;
807 Ifo::from_gff(&gff)
808}
809
810#[cfg_attr(
812 feature = "tracing",
813 tracing::instrument(level = "debug", skip(writer, ifo))
814)]
815pub fn write_ifo<W: Write>(writer: &mut W, ifo: &Ifo) -> Result<(), IfoError> {
816 let gff = ifo.to_gff();
817 write_gff(writer, &gff)?;
818 Ok(())
819}
820
821#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(ifo)))]
823pub fn write_ifo_to_vec(ifo: &Ifo) -> Result<Vec<u8>, IfoError> {
824 let mut cursor = Cursor::new(Vec::new());
825 write_ifo(&mut cursor, ifo)?;
826 Ok(cursor.into_inner())
827}
828
829static EXPANSION_LIST_CHILDREN: &[FieldSchema] = &[
831 FieldSchema {
832 label: "Expansion_Name",
833 expected_type: GffType::LocalizedString,
834 required: false,
835 children: None,
836 constraint: None,
837 },
838 FieldSchema {
839 label: "Expansion_ID",
840 expected_type: GffType::Int32,
841 required: false,
842 children: None,
843 constraint: None,
844 },
845];
846
847static CUTSCENE_LIST_CHILDREN: &[FieldSchema] = &[
849 FieldSchema {
850 label: "CutScene_Name",
851 expected_type: GffType::ResRef,
852 required: false,
853 children: None,
854 constraint: None,
855 },
856 FieldSchema {
857 label: "CutScene_ID",
858 expected_type: GffType::UInt32,
859 required: false,
860 children: None,
861 constraint: None,
862 },
863];
864
865static AREA_LIST_CHILDREN: &[FieldSchema] = &[
867 FieldSchema {
868 label: "Area_Name",
869 expected_type: GffType::ResRef,
870 required: false,
871 children: None,
872 constraint: None,
873 },
874 FieldSchema {
875 label: "ObjectId",
876 expected_type: GffType::UInt32,
877 required: false,
878 children: None,
879 constraint: None,
880 },
881];
882
883static PLAYER_LIST_CHILDREN: &[FieldSchema] = &[
885 FieldSchema {
886 label: "Mod_CommntyName",
887 expected_type: GffType::String,
888 required: false,
889 children: None,
890 constraint: None,
891 },
892 FieldSchema {
893 label: "Mod_FirstName",
894 expected_type: GffType::LocalizedString,
895 required: false,
896 children: None,
897 constraint: None,
898 },
899 FieldSchema {
900 label: "Mod_LastName",
901 expected_type: GffType::LocalizedString,
902 required: false,
903 children: None,
904 constraint: None,
905 },
906 FieldSchema {
907 label: "Mod_IsPrimaryPlr",
908 expected_type: GffType::UInt8,
909 required: false,
910 children: None,
911 constraint: None,
912 },
913];
914
915static TOKENS_LIST_CHILDREN: &[FieldSchema] = &[
917 FieldSchema {
918 label: "Mod_TokensNumber",
919 expected_type: GffType::UInt32,
920 required: false,
921 children: None,
922 constraint: None,
923 },
924 FieldSchema {
925 label: "Mod_TokensValue",
926 expected_type: GffType::String,
927 required: false,
928 children: None,
929 constraint: None,
930 },
931];
932
933impl GffSchema for Ifo {
934 fn schema() -> &'static [FieldSchema] {
935 static SCHEMA: &[FieldSchema] = &[
936 FieldSchema {
938 label: "Mod_ID",
939 expected_type: GffType::Binary,
940 required: false,
941 children: None,
942 constraint: None,
943 },
944 FieldSchema {
945 label: "Mod_Creator_ID",
946 expected_type: GffType::Int32,
947 required: false,
948 children: None,
949 constraint: None,
950 },
951 FieldSchema {
952 label: "Mod_Version",
953 expected_type: GffType::UInt32,
954 required: false,
955 children: None,
956 constraint: None,
957 },
958 FieldSchema {
959 label: "Mod_Name",
960 expected_type: GffType::LocalizedString,
961 required: false,
962 children: None,
963 constraint: None,
964 },
965 FieldSchema {
966 label: "Mod_Description",
967 expected_type: GffType::LocalizedString,
968 required: false,
969 children: None,
970 constraint: None,
971 },
972 FieldSchema {
973 label: "Mod_Tag",
974 expected_type: GffType::String,
975 required: false,
976 children: None,
977 constraint: None,
978 },
979 FieldSchema {
980 label: "Mod_IsSaveGame",
981 expected_type: GffType::UInt8,
982 required: false,
983 children: None,
984 constraint: None,
985 },
986 FieldSchema {
987 label: "Mod_IsNWMFile",
988 expected_type: GffType::UInt8,
989 required: false,
990 children: None,
991 constraint: None,
992 },
993 FieldSchema {
994 label: "Mod_NWMResName",
995 expected_type: GffType::String,
996 required: false,
997 children: None,
998 constraint: None,
999 },
1000 FieldSchema {
1002 label: "Mod_StartMovie",
1003 expected_type: GffType::ResRef,
1004 required: false,
1005 children: None,
1006 constraint: None,
1007 },
1008 FieldSchema {
1009 label: "Mod_Entry_Area",
1010 expected_type: GffType::ResRef,
1011 required: false,
1012 children: None,
1013 constraint: None,
1014 },
1015 FieldSchema {
1016 label: "Mod_Entry_X",
1017 expected_type: GffType::Single,
1018 required: false,
1019 children: None,
1020 constraint: None,
1021 },
1022 FieldSchema {
1023 label: "Mod_Entry_Y",
1024 expected_type: GffType::Single,
1025 required: false,
1026 children: None,
1027 constraint: None,
1028 },
1029 FieldSchema {
1030 label: "Mod_Entry_Z",
1031 expected_type: GffType::Single,
1032 required: false,
1033 children: None,
1034 constraint: None,
1035 },
1036 FieldSchema {
1037 label: "Mod_Entry_Dir_X",
1038 expected_type: GffType::Single,
1039 required: false,
1040 children: None,
1041 constraint: None,
1042 },
1043 FieldSchema {
1044 label: "Mod_Entry_Dir_Y",
1045 expected_type: GffType::Single,
1046 required: false,
1047 children: None,
1048 constraint: None,
1049 },
1050 FieldSchema {
1052 label: "Mod_MinPerHour",
1053 expected_type: GffType::UInt8,
1054 required: false,
1055 children: None,
1056 constraint: None,
1057 },
1058 FieldSchema {
1059 label: "Mod_DawnHour",
1060 expected_type: GffType::UInt8,
1061 required: false,
1062 children: None,
1063 constraint: None,
1064 },
1065 FieldSchema {
1066 label: "Mod_DuskHour",
1067 expected_type: GffType::UInt8,
1068 required: false,
1069 children: None,
1070 constraint: None,
1071 },
1072 FieldSchema {
1073 label: "Mod_XPScale",
1074 expected_type: GffType::UInt8,
1075 required: false,
1076 children: None,
1077 constraint: None,
1078 },
1079 FieldSchema {
1081 label: "Mod_StartYear",
1082 expected_type: GffType::UInt32,
1083 required: false,
1084 children: None,
1085 constraint: None,
1086 },
1087 FieldSchema {
1088 label: "Mod_StartMonth",
1089 expected_type: GffType::UInt8,
1090 required: false,
1091 children: None,
1092 constraint: None,
1093 },
1094 FieldSchema {
1095 label: "Mod_StartDay",
1096 expected_type: GffType::UInt8,
1097 required: false,
1098 children: None,
1099 constraint: None,
1100 },
1101 FieldSchema {
1102 label: "Mod_StartHour",
1103 expected_type: GffType::UInt8,
1104 required: false,
1105 children: None,
1106 constraint: None,
1107 },
1108 FieldSchema {
1109 label: "Mod_Transition",
1110 expected_type: GffType::UInt32,
1111 required: false,
1112 children: None,
1113 constraint: None,
1114 },
1115 FieldSchema {
1116 label: "Mod_StartMinute",
1117 expected_type: GffType::UInt16,
1118 required: false,
1119 children: None,
1120 constraint: None,
1121 },
1122 FieldSchema {
1123 label: "Mod_StartSecond",
1124 expected_type: GffType::UInt16,
1125 required: false,
1126 children: None,
1127 constraint: None,
1128 },
1129 FieldSchema {
1130 label: "Mod_StartMiliSec",
1131 expected_type: GffType::UInt16,
1132 required: false,
1133 children: None,
1134 constraint: None,
1135 },
1136 FieldSchema {
1137 label: "Mod_PauseTime",
1138 expected_type: GffType::UInt32,
1139 required: false,
1140 children: None,
1141 constraint: None,
1142 },
1143 FieldSchema {
1144 label: "Mod_PauseDay",
1145 expected_type: GffType::UInt32,
1146 required: false,
1147 children: None,
1148 constraint: None,
1149 },
1150 FieldSchema {
1152 label: "Mod_Effect_NxtId",
1153 expected_type: GffType::UInt64,
1154 required: false,
1155 children: None,
1156 constraint: None,
1157 },
1158 FieldSchema {
1159 label: "Mod_NextCharId0",
1160 expected_type: GffType::UInt32,
1161 required: false,
1162 children: None,
1163 constraint: None,
1164 },
1165 FieldSchema {
1166 label: "Mod_NextCharId1",
1167 expected_type: GffType::UInt32,
1168 required: false,
1169 children: None,
1170 constraint: None,
1171 },
1172 FieldSchema {
1173 label: "Mod_NextObjId0",
1174 expected_type: GffType::UInt32,
1175 required: false,
1176 children: None,
1177 constraint: None,
1178 },
1179 FieldSchema {
1180 label: "Mod_NextObjId1",
1181 expected_type: GffType::UInt32,
1182 required: false,
1183 children: None,
1184 constraint: None,
1185 },
1186 FieldSchema {
1188 label: "Mod_Hak",
1189 expected_type: GffType::String,
1190 required: false,
1191 children: None,
1192 constraint: None,
1193 },
1194 FieldSchema {
1196 label: "Mod_OnHeartbeat",
1197 expected_type: GffType::ResRef,
1198 required: false,
1199 children: None,
1200 constraint: None,
1201 },
1202 FieldSchema {
1203 label: "Mod_OnUsrDefined",
1204 expected_type: GffType::ResRef,
1205 required: false,
1206 children: None,
1207 constraint: None,
1208 },
1209 FieldSchema {
1210 label: "Mod_OnModLoad",
1211 expected_type: GffType::ResRef,
1212 required: false,
1213 children: None,
1214 constraint: None,
1215 },
1216 FieldSchema {
1217 label: "Mod_OnModStart",
1218 expected_type: GffType::ResRef,
1219 required: false,
1220 children: None,
1221 constraint: None,
1222 },
1223 FieldSchema {
1224 label: "Mod_OnClientEntr",
1225 expected_type: GffType::ResRef,
1226 required: false,
1227 children: None,
1228 constraint: None,
1229 },
1230 FieldSchema {
1231 label: "Mod_OnClientLeav",
1232 expected_type: GffType::ResRef,
1233 required: false,
1234 children: None,
1235 constraint: None,
1236 },
1237 FieldSchema {
1238 label: "Mod_OnActvtItem",
1239 expected_type: GffType::ResRef,
1240 required: false,
1241 children: None,
1242 constraint: None,
1243 },
1244 FieldSchema {
1245 label: "Mod_OnAcquirItem",
1246 expected_type: GffType::ResRef,
1247 required: false,
1248 children: None,
1249 constraint: None,
1250 },
1251 FieldSchema {
1252 label: "Mod_OnUnAqreItem",
1253 expected_type: GffType::ResRef,
1254 required: false,
1255 children: None,
1256 constraint: None,
1257 },
1258 FieldSchema {
1259 label: "Mod_OnPlrDeath",
1260 expected_type: GffType::ResRef,
1261 required: false,
1262 children: None,
1263 constraint: None,
1264 },
1265 FieldSchema {
1266 label: "Mod_OnPlrDying",
1267 expected_type: GffType::ResRef,
1268 required: false,
1269 children: None,
1270 constraint: None,
1271 },
1272 FieldSchema {
1273 label: "Mod_OnSpawnBtnDn",
1274 expected_type: GffType::ResRef,
1275 required: false,
1276 children: None,
1277 constraint: None,
1278 },
1279 FieldSchema {
1280 label: "Mod_OnPlrRest",
1281 expected_type: GffType::ResRef,
1282 required: false,
1283 children: None,
1284 constraint: None,
1285 },
1286 FieldSchema {
1287 label: "Mod_OnPlrLvlUp",
1288 expected_type: GffType::ResRef,
1289 required: false,
1290 children: None,
1291 constraint: None,
1292 },
1293 FieldSchema {
1294 label: "Mod_OnEquipItem",
1295 expected_type: GffType::ResRef,
1296 required: false,
1297 children: None,
1298 constraint: None,
1299 },
1300 FieldSchema {
1302 label: "Mod_Expan_List",
1303 expected_type: GffType::List,
1304 required: false,
1305 children: Some(EXPANSION_LIST_CHILDREN),
1306 constraint: None,
1307 },
1308 FieldSchema {
1309 label: "Mod_CutSceneList",
1310 expected_type: GffType::List,
1311 required: false,
1312 children: Some(CUTSCENE_LIST_CHILDREN),
1313 constraint: None,
1314 },
1315 FieldSchema {
1316 label: "Mod_Area_list",
1317 expected_type: GffType::List,
1318 required: false,
1319 children: Some(AREA_LIST_CHILDREN),
1320 constraint: None,
1321 },
1322 FieldSchema {
1323 label: "Mod_PlayerList",
1324 expected_type: GffType::List,
1325 required: false,
1326 children: Some(PLAYER_LIST_CHILDREN),
1327 constraint: None,
1328 },
1329 FieldSchema {
1330 label: "Mod_Tokens",
1331 expected_type: GffType::List,
1332 required: false,
1333 children: Some(TOKENS_LIST_CHILDREN),
1334 constraint: None,
1335 },
1336 FieldSchema {
1338 label: "SWVarTable",
1339 expected_type: GffType::Struct,
1340 required: false,
1341 children: None,
1342 constraint: None,
1343 },
1344 FieldSchema {
1345 label: "VarTable",
1346 expected_type: GffType::List,
1347 required: false,
1348 children: None,
1349 constraint: None,
1350 },
1351 ];
1352 SCHEMA
1353 }
1354}
1355
1356#[cfg(test)]
1357mod tests {
1358 use super::*;
1359
1360 fn make_test_ifo_gff() -> Gff {
1362 let mut root = GffStruct::new(-1);
1363 root.push_field("Mod_IsSaveGame", GffValue::UInt8(0));
1364 root.push_field("Mod_IsNWMFile", GffValue::UInt8(0));
1365 root.push_field("Mod_ID", GffValue::Binary(vec![0xAB; 32]));
1366 root.push_field("Mod_Creator_ID", GffValue::Int32(42));
1367 root.push_field("Mod_Version", GffValue::UInt32(3));
1368 root.push_field("Mod_Tag", GffValue::String("end_m01aa".into()));
1369 root.push_field(
1370 "Mod_Name",
1371 GffValue::LocalizedString(GffLocalizedString::new(StrRef::from_raw(42000))),
1372 );
1373 root.push_field(
1374 "Mod_Description",
1375 GffValue::LocalizedString(GffLocalizedString::new(StrRef::from_raw(42001))),
1376 );
1377 root.push_field("Mod_StartMovie", GffValue::resref_lit("leclogo"));
1378 root.push_field("Mod_Entry_Area", GffValue::resref_lit("m01aa"));
1379 root.push_field("Mod_Entry_X", GffValue::Single(10.5));
1380 root.push_field("Mod_Entry_Y", GffValue::Single(20.3));
1381 root.push_field("Mod_Entry_Z", GffValue::Single(0.0));
1382 root.push_field("Mod_Entry_Dir_X", GffValue::Single(0.0));
1383 root.push_field("Mod_Entry_Dir_Y", GffValue::Single(1.0));
1384 root.push_field("Mod_MinPerHour", GffValue::UInt8(2));
1385 root.push_field("Mod_DawnHour", GffValue::UInt8(6));
1386 root.push_field("Mod_DuskHour", GffValue::UInt8(18));
1387 root.push_field("Mod_XPScale", GffValue::UInt8(10));
1388
1389 root.push_field("Mod_OnHeartbeat", GffValue::resref_lit("k_mod_hb"));
1390 root.push_field("Mod_OnUsrDefined", GffValue::resref_lit(""));
1391 root.push_field("Mod_OnModLoad", GffValue::resref_lit("k_mod_load"));
1392 root.push_field("Mod_OnModStart", GffValue::resref_lit("k_mod_start"));
1393 root.push_field("Mod_OnClientEntr", GffValue::resref_lit("k_mod_enter"));
1394 root.push_field("Mod_OnClientLeav", GffValue::resref_lit(""));
1395 root.push_field("Mod_OnActvtItem", GffValue::resref_lit("k_act_item"));
1396 root.push_field("Mod_OnAcquirItem", GffValue::resref_lit("k_acq_item"));
1397 root.push_field("Mod_OnUnAqreItem", GffValue::resref_lit(""));
1398 root.push_field("Mod_OnPlrDeath", GffValue::resref_lit("k_plr_death"));
1399 root.push_field("Mod_OnPlrDying", GffValue::resref_lit("k_plr_dying"));
1400 root.push_field("Mod_OnSpawnBtnDn", GffValue::resref_lit(""));
1401 root.push_field("Mod_OnPlrRest", GffValue::resref_lit("k_plr_rest"));
1402 root.push_field("Mod_OnPlrLvlUp", GffValue::resref_lit(""));
1403 root.push_field("Mod_OnEquipItem", GffValue::resref_lit(""));
1404
1405 let mut a1 = GffStruct::new(6);
1407 a1.push_field("Area_Name", GffValue::resref_lit("m01aa"));
1408 let mut a2 = GffStruct::new(6);
1409 a2.push_field("Area_Name", GffValue::resref_lit("m01ab"));
1410 root.push_field("Mod_Area_list", GffValue::List(vec![a1, a2]));
1411
1412 root.push_field("Mod_Expan_List", GffValue::List(Vec::new()));
1414 root.push_field("Mod_CutSceneList", GffValue::List(Vec::new()));
1415
1416 Gff::new(*b"IFO ", root)
1417 }
1418
1419 fn make_save_game_ifo_gff() -> Gff {
1421 let mut gff = make_test_ifo_gff();
1422 for field in &mut gff.root.fields {
1424 if field.label == "Mod_IsSaveGame" {
1425 field.value = GffValue::UInt8(1);
1426 }
1427 }
1428
1429 gff.root.push_field("Mod_StartYear", GffValue::UInt32(1340));
1431 gff.root.push_field("Mod_StartMonth", GffValue::UInt8(6));
1432 gff.root.push_field("Mod_StartDay", GffValue::UInt8(1));
1433 gff.root.push_field("Mod_StartHour", GffValue::UInt8(23));
1434 gff.root.push_field("Mod_StartMinute", GffValue::UInt16(30));
1435 gff.root.push_field("Mod_StartSecond", GffValue::UInt16(15));
1436 gff.root
1437 .push_field("Mod_StartMiliSec", GffValue::UInt16(500));
1438 gff.root.push_field("Mod_Transition", GffValue::UInt32(1));
1439 gff.root.push_field("Mod_PauseTime", GffValue::UInt32(1000));
1440 gff.root.push_field("Mod_PauseDay", GffValue::UInt32(5));
1441
1442 gff.root
1444 .push_field("Mod_Effect_NxtId", GffValue::UInt64(999));
1445 gff.root.push_field("Mod_NextCharId0", GffValue::UInt32(10));
1446 gff.root.push_field("Mod_NextCharId1", GffValue::UInt32(20));
1447 gff.root.push_field("Mod_NextObjId0", GffValue::UInt32(100));
1448 gff.root.push_field("Mod_NextObjId1", GffValue::UInt32(200));
1449
1450 gff.root
1452 .push_field("Mod_Hak", GffValue::String("my_hak".into()));
1453
1454 gff.root.fields.retain(|f| f.label != "Mod_Area_list");
1456 let mut a1 = GffStruct::new(6);
1457 a1.push_field("Area_Name", GffValue::resref_lit("m01aa"));
1458 a1.push_field("ObjectId", GffValue::UInt32(0x7F00_0001));
1459 gff.root
1460 .push_field("Mod_Area_list", GffValue::List(vec![a1]));
1461
1462 let mut player = GffStruct::new(0);
1464 player.push_field("Mod_CommntyName", GffValue::String("TestPlayer".into()));
1465 use rakata_formats::GffLocalizedSubstring;
1466 let first = GffLocalizedString {
1467 string_ref: StrRef::invalid(),
1468 substrings: vec![GffLocalizedSubstring {
1469 string_id: 0,
1470 text: "Revan".into(),
1471 }],
1472 };
1473 player.push_field("Mod_FirstName", GffValue::LocalizedString(first));
1474 player.push_field(
1475 "Mod_LastName",
1476 GffValue::LocalizedString(GffLocalizedString::new(StrRef::invalid())),
1477 );
1478 player.push_field("Mod_IsPrimaryPlr", GffValue::UInt8(1));
1479 gff.root
1480 .push_field("Mod_PlayerList", GffValue::List(vec![player]));
1481
1482 let mut token = GffStruct::new(7);
1484 token.push_field("Mod_TokensNumber", GffValue::UInt32(0));
1485 token.push_field("Mod_TokensValue", GffValue::String("Revan".into()));
1486 gff.root
1487 .push_field("Mod_Tokens", GffValue::List(vec![token]));
1488
1489 gff.root.fields.retain(|f| f.label != "Mod_Expan_List");
1491 let mut exp = GffStruct::new(0);
1492 exp.push_field(
1493 "Expansion_Name",
1494 GffValue::LocalizedString(GffLocalizedString::new(StrRef::from_raw(100))),
1495 );
1496 exp.push_field("Expansion_ID", GffValue::Int32(1));
1497 gff.root
1498 .push_field("Mod_Expan_List", GffValue::List(vec![exp]));
1499
1500 gff.root.fields.retain(|f| f.label != "Mod_CutSceneList");
1502 let mut cs = GffStruct::new(1);
1503 cs.push_field("CutScene_Name", GffValue::resref_lit("cs_intro"));
1504 cs.push_field("CutScene_ID", GffValue::UInt32(0));
1505 gff.root
1506 .push_field("Mod_CutSceneList", GffValue::List(vec![cs]));
1507
1508 gff
1509 }
1510
1511 #[test]
1512 fn reads_core_ifo_fields() {
1513 let gff = make_test_ifo_gff();
1514 let ifo = Ifo::from_gff(&gff).expect("must parse");
1515
1516 assert_eq!(ifo.tag, "end_m01aa");
1517 assert_eq!(ifo.name.string_ref.raw(), 42000);
1518 assert_eq!(ifo.description.string_ref.raw(), 42001);
1519 assert_eq!(ifo.start_movie, "leclogo");
1520 assert_eq!(ifo.entry_area, "m01aa");
1521 assert_eq!(ifo.entry_x, 10.5);
1522 assert_eq!(ifo.entry_y, 20.3);
1523 assert_eq!(ifo.entry_z, 0.0);
1524 assert_eq!(ifo.entry_dir_x, 0.0);
1525 assert_eq!(ifo.entry_dir_y, 1.0);
1526 assert_eq!(ifo.min_per_hour, 2);
1527 assert_eq!(ifo.dawn_hour, 6);
1528 assert_eq!(ifo.dusk_hour, 18);
1529 assert_eq!(ifo.xp_scale, 10);
1530 }
1531
1532 #[test]
1533 fn reads_root_identity_fields() {
1534 let gff = make_test_ifo_gff();
1535 let ifo = Ifo::from_gff(&gff).expect("must parse");
1536
1537 assert!(!ifo.is_save_game);
1538 assert!(!ifo.is_nwm_file);
1539 assert!(ifo.nwm_res_name.is_empty());
1540 assert_eq!(ifo.module_id, [0xAB; 32]);
1541 assert_eq!(ifo.creator_id, 42);
1542 assert_eq!(ifo.version, 3);
1543 }
1544
1545 #[test]
1546 fn reads_scripts() {
1547 let gff = make_test_ifo_gff();
1548 let ifo = Ifo::from_gff(&gff).expect("must parse");
1549
1550 assert_eq!(ifo.on_heartbeat, "k_mod_hb");
1551 assert_eq!(ifo.on_user_defined, "");
1552 assert_eq!(ifo.on_mod_load, "k_mod_load");
1553 assert_eq!(ifo.on_mod_start, "k_mod_start");
1554 assert_eq!(ifo.on_client_enter, "k_mod_enter");
1555 assert_eq!(ifo.on_client_leave, "");
1556 assert_eq!(ifo.on_activate_item, "k_act_item");
1557 assert_eq!(ifo.on_acquire_item, "k_acq_item");
1558 assert_eq!(ifo.on_unacquire_item, "");
1559 assert_eq!(ifo.on_player_death, "k_plr_death");
1560 assert_eq!(ifo.on_player_dying, "k_plr_dying");
1561 assert_eq!(ifo.on_spawn_btn_down, "");
1562 assert_eq!(ifo.on_player_rest, "k_plr_rest");
1563 assert_eq!(ifo.on_player_level_up, "");
1564 assert_eq!(ifo.on_equip_item, "");
1565 }
1566
1567 #[test]
1568 fn reads_area_list() {
1569 let gff = make_test_ifo_gff();
1570 let ifo = Ifo::from_gff(&gff).expect("must parse");
1571
1572 assert_eq!(ifo.areas.len(), 2);
1573 assert_eq!(ifo.areas[0].area_name, "m01aa");
1574 assert_eq!(ifo.areas[0].object_id, 0);
1575 assert_eq!(ifo.areas[1].area_name, "m01ab");
1576 assert_eq!(ifo.areas[1].object_id, 0);
1577 }
1578
1579 #[test]
1580 fn reads_save_game_fields() {
1581 let gff = make_save_game_ifo_gff();
1582 let ifo = Ifo::from_gff(&gff).expect("must parse");
1583
1584 assert!(ifo.is_save_game);
1585 assert_eq!(ifo.start_year, 1340);
1586 assert_eq!(ifo.start_month, 6);
1587 assert_eq!(ifo.start_day, 1);
1588 assert_eq!(ifo.start_hour, 23);
1589 assert_eq!(ifo.start_minute, 30);
1590 assert_eq!(ifo.start_second, 15);
1591 assert_eq!(ifo.start_millisecond, 500);
1592 assert_eq!(ifo.transition, 1);
1593 assert_eq!(ifo.pause_time, 1000);
1594 assert_eq!(ifo.pause_day, 5);
1595 assert_eq!(ifo.effect_next_id, 999);
1596 assert_eq!(ifo.next_char_id_0, 10);
1597 assert_eq!(ifo.next_char_id_1, 20);
1598 assert_eq!(ifo.next_obj_id_0, 100);
1599 assert_eq!(ifo.next_obj_id_1, 200);
1600 assert_eq!(ifo.hak, "my_hak");
1601 }
1602
1603 #[test]
1604 fn reads_area_object_id() {
1605 let gff = make_save_game_ifo_gff();
1606 let ifo = Ifo::from_gff(&gff).expect("must parse");
1607
1608 assert_eq!(ifo.areas.len(), 1);
1609 assert_eq!(ifo.areas[0].area_name, "m01aa");
1610 assert_eq!(ifo.areas[0].object_id, 0x7F00_0001);
1611 }
1612
1613 #[test]
1614 fn reads_player_list() {
1615 let gff = make_save_game_ifo_gff();
1616 let ifo = Ifo::from_gff(&gff).expect("must parse");
1617
1618 assert_eq!(ifo.player_list.len(), 1);
1619 assert_eq!(ifo.player_list[0].community_name, "TestPlayer");
1620 assert!(ifo.player_list[0].is_primary_player);
1621 assert_eq!(ifo.player_list[0].first_name.substrings[0].text, "Revan");
1622 }
1623
1624 #[test]
1625 fn reads_tokens() {
1626 let gff = make_save_game_ifo_gff();
1627 let ifo = Ifo::from_gff(&gff).expect("must parse");
1628
1629 assert_eq!(ifo.tokens.len(), 1);
1630 assert_eq!(ifo.tokens[0].token_number, 0);
1631 assert_eq!(ifo.tokens[0].token_value, "Revan");
1632 }
1633
1634 #[test]
1635 fn reads_expansion_list() {
1636 let gff = make_save_game_ifo_gff();
1637 let ifo = Ifo::from_gff(&gff).expect("must parse");
1638
1639 assert_eq!(ifo.expansion_list.len(), 1);
1640 assert_eq!(ifo.expansion_list[0].expansion_name.string_ref.raw(), 100);
1641 assert_eq!(ifo.expansion_list[0].expansion_id, 1);
1642 }
1643
1644 #[test]
1645 fn reads_cutscene_list() {
1646 let gff = make_save_game_ifo_gff();
1647 let ifo = Ifo::from_gff(&gff).expect("must parse");
1648
1649 assert_eq!(ifo.cutscene_list.len(), 1);
1650 assert_eq!(ifo.cutscene_list[0].cutscene_name, "cs_intro");
1651 assert_eq!(ifo.cutscene_list[0].cutscene_id, 0);
1652 }
1653
1654 #[test]
1655 fn all_fields_survive_typed_roundtrip() {
1656 let gff = make_save_game_ifo_gff();
1657 let ifo = Ifo::from_gff(&gff).expect("typed parse");
1658 let bytes = write_ifo_to_vec(&ifo).expect("write succeeds");
1659 let reparsed = read_ifo_from_bytes(&bytes).expect("reparse succeeds");
1660
1661 assert_eq!(ifo, reparsed);
1662 }
1663
1664 #[test]
1665 fn typed_edits_roundtrip_through_gff_writer() {
1666 let gff = make_test_ifo_gff();
1667 let mut ifo = Ifo::from_gff(&gff).expect("must parse");
1668 ifo.tag = "end_m01ab".into();
1669 ifo.entry_area = ResRef::new("m01ab").expect("valid test resref");
1670 ifo.entry_x = 50.0;
1671 ifo.areas.push(IfoArea {
1672 area_name: ResRef::new("m01ac").expect("valid test resref"),
1673 object_id: 0,
1674 });
1675
1676 let bytes = write_ifo_to_vec(&ifo).expect("write succeeds");
1677 let reparsed = read_ifo_from_bytes(&bytes).expect("reparse succeeds");
1678
1679 assert_eq!(reparsed.tag, "end_m01ab");
1680 assert_eq!(reparsed.entry_area, "m01ab");
1681 assert_eq!(reparsed.entry_x, 50.0);
1682 assert_eq!(reparsed.areas.len(), 3);
1683 assert_eq!(reparsed.areas[2].area_name, "m01ac");
1684 }
1685
1686 #[test]
1687 fn read_ifo_from_reader_matches_bytes_path() {
1688 let gff = make_test_ifo_gff();
1689 let bytes = {
1690 let mut c = Cursor::new(Vec::new());
1691 write_gff(&mut c, &gff).expect("test fixture must be valid");
1692 c.into_inner()
1693 };
1694
1695 let mut cursor = Cursor::new(&bytes);
1696 let via_reader = read_ifo(&mut cursor).expect("reader parse succeeds");
1697 let via_bytes = read_ifo_from_bytes(&bytes).expect("bytes parse succeeds");
1698
1699 assert_eq!(via_reader, via_bytes);
1700 }
1701
1702 #[test]
1703 fn rejects_non_ifo_file_type() {
1704 let mut gff = make_test_ifo_gff();
1705 gff.file_type = *b"UTT ";
1706
1707 let err = Ifo::from_gff(&gff).expect_err("UTT must be rejected as IFO input");
1708 assert!(matches!(
1709 err,
1710 IfoError::UnsupportedFileType(file_type) if file_type == *b"UTT "
1711 ));
1712 }
1713
1714 #[test]
1715 fn write_ifo_matches_direct_gff_writer() {
1716 let gff = make_test_ifo_gff();
1717 let ifo = Ifo::from_gff(&gff).expect("must parse");
1718
1719 let via_typed = write_ifo_to_vec(&ifo).expect("typed write succeeds");
1720
1721 let mut direct = Cursor::new(Vec::new());
1722 write_gff(&mut direct, &ifo.to_gff()).expect("direct write succeeds");
1723
1724 assert_eq!(via_typed, direct.into_inner());
1725 }
1726
1727 #[test]
1728 fn empty_area_list_ok() {
1729 let mut gff = make_test_ifo_gff();
1730 gff.root.fields.retain(|f| f.label != "Mod_Area_list");
1731
1732 let ifo = Ifo::from_gff(&gff).expect("must parse");
1733 assert!(ifo.areas.is_empty());
1734 }
1735
1736 #[test]
1737 fn schema_field_count() {
1738 assert_eq!(Ifo::schema().len(), 58);
1739 }
1740
1741 #[test]
1742 fn schema_no_duplicate_labels() {
1743 let schema = Ifo::schema();
1744 let mut labels: Vec<&str> = schema.iter().map(|f| f.label).collect();
1745 labels.sort();
1746 let before = labels.len();
1747 labels.dedup();
1748 assert_eq!(before, labels.len(), "duplicate labels in IFO schema");
1749 }
1750
1751 #[test]
1752 fn schema_lists_have_children() {
1753 let schema = Ifo::schema();
1754 let expan = schema
1755 .iter()
1756 .find(|f| f.label == "Mod_Expan_List")
1757 .expect("test fixture must be valid");
1758 assert_eq!(expan.children.expect("test fixture must be valid").len(), 2);
1759 let cutscene = schema
1760 .iter()
1761 .find(|f| f.label == "Mod_CutSceneList")
1762 .expect("test fixture must be valid");
1763 assert_eq!(
1764 cutscene.children.expect("test fixture must be valid").len(),
1765 2
1766 );
1767 let area = schema
1768 .iter()
1769 .find(|f| f.label == "Mod_Area_list")
1770 .expect("test fixture must be valid");
1771 assert_eq!(area.children.expect("test fixture must be valid").len(), 2);
1772 let player = schema
1773 .iter()
1774 .find(|f| f.label == "Mod_PlayerList")
1775 .expect("test fixture must be valid");
1776 assert_eq!(
1777 player.children.expect("test fixture must be valid").len(),
1778 4
1779 );
1780 let tokens = schema
1781 .iter()
1782 .find(|f| f.label == "Mod_Tokens")
1783 .expect("test fixture must be valid");
1784 assert_eq!(
1785 tokens.children.expect("test fixture must be valid").len(),
1786 2
1787 );
1788 }
1789
1790 #[test]
1791 fn nwm_res_name_only_read_when_nwm_file() {
1792 let mut gff = make_test_ifo_gff();
1793 gff.root
1794 .push_field("Mod_NWMResName", GffValue::String("some_nwm".into()));
1795
1796 let ifo = Ifo::from_gff(&gff).expect("must parse");
1798 assert!(ifo.nwm_res_name.is_empty());
1799
1800 for field in &mut gff.root.fields {
1802 if field.label == "Mod_IsNWMFile" {
1803 field.value = GffValue::UInt8(1);
1804 }
1805 }
1806 let ifo = Ifo::from_gff(&gff).expect("must parse");
1807 assert_eq!(ifo.nwm_res_name, "some_nwm");
1808 }
1809}