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