Skip to main content

fallout_core/fallout1/
sections.rs

1use std::io::{self, Read, Seek};
2
3use crate::reader::BigEndianReader;
4
5use super::types::{
6    KILL_TYPE_COUNT, PC_STAT_COUNT, PERK_COUNT, SAVEABLE_STAT_COUNT, SKILL_COUNT,
7    TAGGED_SKILL_COUNT,
8};
9use crate::object::GameObject;
10
11// --- Handler 1: Player Combat ID ---
12
13pub fn parse_player_combat_id<R: Read + Seek>(r: &mut BigEndianReader<R>) -> io::Result<i32> {
14    r.read_i32()
15}
16
17// --- Handler 2: Game Global Variables ---
18
19pub struct GlobalVarsSection {
20    pub global_vars: Vec<i32>,
21    pub water_movie_played: bool,
22}
23
24/// Auto-detect the number of global variables and parse handler 2.
25///
26/// The challenge: handler 2 writes `int32[N] + uint8` but N is not stored
27/// in the file. We detect N by trying candidates and validating that
28/// handler 3's map file list follows with reasonable data.
29pub fn parse_game_global_vars<R: Read + Seek>(
30    r: &mut BigEndianReader<R>,
31) -> io::Result<GlobalVarsSection> {
32    let start_pos = r.position()?;
33
34    // Try candidate N values. Fallout 1 vanilla typically has ~600-700 global vars.
35    let detected_n = detect_global_var_count(r, start_pos)?;
36
37    // Now read the actual data
38    r.seek_to(start_pos)?;
39    let global_vars = r.read_i32_vec(detected_n)?;
40    let water_flag = r.read_u8()?;
41
42    Ok(GlobalVarsSection {
43        global_vars,
44        water_movie_played: water_flag != 0,
45    })
46}
47
48/// Try different N values until handler 3's map file list validates.
49fn detect_global_var_count<R: Read + Seek>(
50    r: &mut BigEndianReader<R>,
51    handler2_start: u64,
52) -> io::Result<usize> {
53    for n in 100..2000 {
54        // Handler 3 starts at: handler2_start + n*4 + 1
55        let handler3_pos = handler2_start + (n * 4) as u64 + 1;
56        r.seek_to(handler3_pos)?;
57
58        if let Ok(file_count) = r.read_i32()
59            && file_count > 0
60            && file_count < 200
61            && let Ok(filename) = r.read_null_terminated_string(16)
62            && !filename.is_empty()
63            && filename.is_ascii()
64            && filename.to_uppercase().ends_with(".SAV")
65        {
66            return Ok(n);
67        }
68    }
69
70    Err(io::Error::new(
71        io::ErrorKind::InvalidData,
72        "could not detect global variable count",
73    ))
74}
75
76// --- Handler 3: Map File List ---
77
78pub struct MapFileListSection {
79    pub map_files: Vec<String>,
80    pub automap_size: i32,
81}
82
83pub fn parse_map_file_list<R: Read + Seek>(
84    r: &mut BigEndianReader<R>,
85) -> io::Result<MapFileListSection> {
86    let file_count = r.read_i32()?;
87    let mut map_files = Vec::with_capacity(file_count as usize);
88
89    for _ in 0..file_count {
90        let filename = r.read_null_terminated_string(16)?;
91        map_files.push(filename);
92    }
93
94    let automap_size = r.read_i32()?;
95
96    Ok(MapFileListSection {
97        map_files,
98        automap_size,
99    })
100}
101
102// --- Handler 5: Player Object ---
103
104pub struct PlayerObjectSection {
105    pub player_object: GameObject,
106    pub center_tile: i32,
107}
108
109pub fn parse_player_object<R: Read + Seek>(
110    r: &mut BigEndianReader<R>,
111) -> io::Result<PlayerObjectSection> {
112    let player_object = GameObject::parse(r)?;
113    let center_tile = r.read_i32()?;
114    Ok(PlayerObjectSection {
115        player_object,
116        center_tile,
117    })
118}
119
120// --- Handler 6: Critter Proto Data ---
121
122#[derive(Debug)]
123pub struct CritterProtoData {
124    pub sneak_working: i32,
125    pub flags: i32,
126    pub base_stats: [i32; SAVEABLE_STAT_COUNT],
127    pub bonus_stats: [i32; SAVEABLE_STAT_COUNT],
128    pub skills: [i32; SKILL_COUNT],
129    pub body_type: i32,
130    pub experience: i32,
131    pub kill_type: i32,
132}
133
134pub fn parse_critter_proto<R: Read + Seek>(
135    r: &mut BigEndianReader<R>,
136) -> io::Result<CritterProtoData> {
137    let sneak_working = r.read_i32()?;
138    let flags = r.read_i32()?;
139    let base_stats = r.read_i32_array::<SAVEABLE_STAT_COUNT>()?;
140    let bonus_stats = r.read_i32_array::<SAVEABLE_STAT_COUNT>()?;
141    let skills = r.read_i32_array::<SKILL_COUNT>()?;
142    let body_type = r.read_i32()?;
143    let experience = r.read_i32()?;
144    let kill_type = r.read_i32()?;
145
146    Ok(CritterProtoData {
147        sneak_working,
148        flags,
149        base_stats,
150        bonus_stats,
151        skills,
152        body_type,
153        experience,
154        kill_type,
155    })
156}
157
158// --- Handler 7: Kill Counts ---
159
160pub fn parse_kill_counts<R: Read + Seek>(
161    r: &mut BigEndianReader<R>,
162) -> io::Result<[i32; KILL_TYPE_COUNT]> {
163    r.read_i32_array::<KILL_TYPE_COUNT>()
164}
165
166// --- Handler 8: Tagged Skills ---
167
168pub fn parse_tagged_skills<R: Read + Seek>(
169    r: &mut BigEndianReader<R>,
170) -> io::Result<[i32; TAGGED_SKILL_COUNT]> {
171    r.read_i32_array::<TAGGED_SKILL_COUNT>()
172}
173
174// --- Handler 10: Perks ---
175
176pub fn parse_perks<R: Read + Seek>(r: &mut BigEndianReader<R>) -> io::Result<[i32; PERK_COUNT]> {
177    r.read_i32_array::<PERK_COUNT>()
178}
179
180// --- Handler 11: Combat State ---
181
182#[derive(Debug)]
183pub struct CombatState {
184    pub combat_state_flags: u32,
185    pub combat_data: Option<CombatData>,
186}
187
188#[derive(Debug)]
189pub struct CombatData {
190    pub turn_running: i32,
191    pub free_move: i32,
192    pub exps: i32,
193    pub list_com: i32,
194    pub list_noncom: i32,
195    pub list_total: i32,
196    pub dude_cid: i32,
197    pub combatant_cids: Vec<i32>,
198}
199
200pub fn parse_combat_state<R: Read + Seek>(r: &mut BigEndianReader<R>) -> io::Result<CombatState> {
201    let combat_state_flags = r.read_u32()?;
202
203    // isInCombat() checks bit 0x01. Default state is 0x02 (not in combat).
204    if (combat_state_flags & 0x01) == 0 {
205        return Ok(CombatState {
206            combat_state_flags,
207            combat_data: None,
208        });
209    }
210
211    let turn_running = r.read_i32()?;
212    let free_move = r.read_i32()?;
213    let exps = r.read_i32()?;
214    let list_com = r.read_i32()?;
215    let list_noncom = r.read_i32()?;
216    let list_total = r.read_i32()?;
217    let dude_cid = r.read_i32()?;
218    let combatant_cids = r.read_i32_vec(list_total as usize)?;
219
220    Ok(CombatState {
221        combat_state_flags,
222        combat_data: Some(CombatData {
223            turn_running,
224            free_move,
225            exps,
226            list_com,
227            list_noncom,
228            list_total,
229            dude_cid,
230            combatant_cids,
231        }),
232    })
233}
234
235// --- Handler 13: PC Stats ---
236
237#[derive(Debug)]
238pub struct PcStats {
239    pub unspent_skill_points: i32,
240    pub level: i32,
241    pub experience: i32,
242    pub reputation: i32,
243    pub karma: i32,
244}
245
246pub fn parse_pc_stats<R: Read + Seek>(r: &mut BigEndianReader<R>) -> io::Result<PcStats> {
247    let stats = r.read_i32_array::<PC_STAT_COUNT>()?;
248    Ok(PcStats {
249        unspent_skill_points: stats[0],
250        level: stats[1],
251        experience: stats[2],
252        reputation: stats[3],
253        karma: stats[4],
254    })
255}
256
257// --- Handler 15: Event Queue ---
258
259/// Skip the event queue. Returns Ok(()) if successfully parsed and skipped.
260pub fn skip_event_queue<R: Read + Seek>(r: &mut BigEndianReader<R>) -> io::Result<()> {
261    let count = r.read_i32()?;
262    if !(0..=10_000).contains(&count) {
263        return Err(io::Error::new(
264            io::ErrorKind::InvalidData,
265            format!("invalid event queue count: {count}"),
266        ));
267    }
268
269    for _ in 0..count {
270        // 12-byte header: time (4) + type (4) + objectId (4)
271        let _time = r.read_i32()?;
272        let event_type = r.read_i32()?;
273        let _object_id = r.read_i32()?;
274
275        // Type-specific data sizes
276        let extra_bytes: u64 = match event_type {
277            0 => 12, // Drug
278            1 => 0,  // Knockout
279            2 => 4,  // Withdrawal
280            3 => 8,  // Script
281            4 => 0,  // Game time
282            5 => 0,  // Poison
283            6 => 0,  // Radiation
284            _ => {
285                return Err(io::Error::new(
286                    io::ErrorKind::InvalidData,
287                    format!("unknown event type: {event_type}"),
288                ));
289            }
290        };
291        r.skip(extra_bytes)?;
292    }
293
294    Ok(())
295}
296
297// --- Handler 16: Traits ---
298
299pub fn parse_traits<R: Read + Seek>(r: &mut BigEndianReader<R>) -> io::Result<[i32; 2]> {
300    let trait1 = r.read_i32()?;
301    let trait2 = r.read_i32()?;
302    if !is_trait_value_valid(trait1) || !is_trait_value_valid(trait2) {
303        return Err(io::Error::new(
304            io::ErrorKind::InvalidData,
305            format!("invalid trait values: [{trait1}, {trait2}]"),
306        ));
307    }
308    Ok([trait1, trait2])
309}
310
311fn is_trait_value_valid(v: i32) -> bool {
312    v == -1 || (0..16).contains(&v)
313}