Skip to main content

plasma_prp/age/
description.rs

1use anyhow::{Context, Result};
2use std::path::Path;
3
4/// Page loading flags.
5pub const PAGE_PREVENT_AUTO_LOAD: u8 = 0x01;
6pub const PAGE_LOAD_IF_SDL_PRESENT: u8 = 0x02;
7pub const PAGE_IS_LOCAL_ONLY: u8 = 0x04;
8pub const PAGE_IS_VOLATILE: u8 = 0x08;
9
10/// A page entry from a .age file.
11#[derive(Debug, Clone)]
12pub struct PageEntry {
13    pub name: String,
14    pub seq_suffix: u32,
15    pub flags: u8,
16}
17
18impl PageEntry {
19    /// Whether this page should be loaded automatically when the age loads.
20    pub fn auto_load(&self) -> bool {
21        self.flags & PAGE_PREVENT_AUTO_LOAD == 0
22    }
23}
24
25/// Parsed .age file describing an age's metadata and page list.
26#[derive(Debug, Clone)]
27pub struct AgeDescription {
28    pub age_name: String,
29    pub start_date_time: u32,
30    pub day_length: f32,
31    pub max_capacity: i32,
32    pub linger_time: i32,
33    pub sequence_prefix: i32,
34    pub release_version: u32,
35    pub pages: Vec<PageEntry>,
36}
37
38impl AgeDescription {
39    /// Parse a .age file from disk.
40    pub fn from_file(path: &Path) -> Result<Self> {
41        let age_name = path.file_stem()
42            .and_then(|s| s.to_str())
43            .unwrap_or("unknown")
44            .to_string();
45
46        let content = std::fs::read_to_string(path)
47            .with_context(|| format!("Failed to read {}", path.display()))?;
48
49        Self::parse(&age_name, &content)
50    }
51
52    /// Parse .age file content.
53    pub fn parse(age_name: &str, content: &str) -> Result<Self> {
54        let mut desc = Self {
55            age_name: age_name.to_string(),
56            start_date_time: 0,
57            day_length: 24.0,
58            max_capacity: -1,
59            linger_time: -1,
60            sequence_prefix: 0,
61            release_version: 0,
62            pages: Vec::new(),
63        };
64
65        let mut next_suffix = 1u32;
66
67        for line in content.lines() {
68            let line = line.trim();
69            if line.is_empty() || line.starts_with('#') || line.starts_with('[') {
70                continue;
71            }
72
73            let (key, value) = match line.split_once('=') {
74                Some(kv) => kv,
75                None => continue,
76            };
77
78            match key.trim() {
79                "StartDateTime" => {
80                    desc.start_date_time = value.trim().parse().unwrap_or(0);
81                }
82                "DayLength" => {
83                    desc.day_length = value.trim().parse().unwrap_or(24.0);
84                }
85                "MaxCapacity" => {
86                    desc.max_capacity = value.trim().parse().unwrap_or(-1);
87                }
88                "LingerTime" => {
89                    desc.linger_time = value.trim().parse().unwrap_or(-1);
90                }
91                "SequencePrefix" => {
92                    desc.sequence_prefix = value.trim().parse().unwrap_or(0);
93                }
94                "ReleaseVersion" => {
95                    desc.release_version = value.trim().parse().unwrap_or(0);
96                }
97                "Page" => {
98                    let parts: Vec<&str> = value.trim().split(',').collect();
99                    let name = parts[0].to_string();
100                    let seq_suffix = if parts.len() > 1 {
101                        parts[1].parse().unwrap_or(next_suffix)
102                    } else {
103                        next_suffix
104                    };
105                    let flags = if parts.len() > 2 {
106                        parts[2].parse().unwrap_or(0)
107                    } else {
108                        0
109                    };
110
111                    next_suffix = seq_suffix + 1;
112
113                    desc.pages.push(PageEntry { name, seq_suffix, flags });
114                }
115                _ => {}
116            }
117        }
118
119        Ok(desc)
120    }
121
122    /// Get pages that should auto-load (no kPreventAutoLoad flag).
123    pub fn auto_load_pages(&self) -> Vec<&PageEntry> {
124        self.pages.iter().filter(|p| p.auto_load()).collect()
125    }
126
127    /// Calculate the plLocation sequence number for a page.
128    pub fn page_location(&self, page: &PageEntry) -> u32 {
129        if self.sequence_prefix < 0 {
130            let combined = ((-self.sequence_prefix as u32) << 16) | (page.seq_suffix & 0xFFFF);
131            combined // Negative prefixes use reserved locations
132        } else {
133            ((self.sequence_prefix as u32) << 16) | (page.seq_suffix & 0xFFFF)
134        }
135    }
136
137    /// Get the .prp filename for a page.
138    pub fn prp_filename(&self, page: &PageEntry) -> String {
139        format!("{}_District_{}.prp", self.age_name, page.name)
140    }
141
142    /// Common pages appended to every age (Textures, BuiltIn).
143    pub fn common_page_filenames(&self) -> Vec<String> {
144        vec![
145            format!("{}_District_Textures.prp", self.age_name),
146            format!("{}_District_BuiltIn.prp", self.age_name),
147        ]
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_parse_cleft_age() {
157        let content = r#"StartDateTime=0000000000
158DayLength=24.000000
159MaxCapacity=1
160LingerTime=180
161SequencePrefix=7
162ReleaseVersion=0
163Page=BookRoom,2
164Page=Cleft,1
165Page=clftAtrusGoggles,30,1
166Page=Desert,0,1
167"#;
168        let desc = AgeDescription::parse("Cleft", content).unwrap();
169        assert_eq!(desc.age_name, "Cleft");
170        assert_eq!(desc.sequence_prefix, 7);
171        assert_eq!(desc.max_capacity, 1);
172        assert_eq!(desc.linger_time, 180);
173        assert_eq!(desc.pages.len(), 4);
174
175        // BookRoom: auto-load (no flag)
176        assert!(desc.pages[0].auto_load());
177        assert_eq!(desc.pages[0].name, "BookRoom");
178        assert_eq!(desc.pages[0].seq_suffix, 2);
179
180        // Cleft: auto-load (no flag)
181        assert!(desc.pages[1].auto_load());
182
183        // clftAtrusGoggles: flag=1 (prevent auto-load)
184        assert!(!desc.pages[2].auto_load());
185
186        // Check auto-load list
187        let auto = desc.auto_load_pages();
188        assert_eq!(auto.len(), 2);
189
190        // Check prp filename
191        assert_eq!(desc.prp_filename(&desc.pages[0]), "Cleft_District_BookRoom.prp");
192
193        // Check location
194        assert_eq!(desc.page_location(&desc.pages[0]), (7 << 16) | 2);
195    }
196}