use anyhow::{Context, Result};
use std::path::Path;
pub const PAGE_PREVENT_AUTO_LOAD: u8 = 0x01;
pub const PAGE_LOAD_IF_SDL_PRESENT: u8 = 0x02;
pub const PAGE_IS_LOCAL_ONLY: u8 = 0x04;
pub const PAGE_IS_VOLATILE: u8 = 0x08;
#[derive(Debug, Clone)]
pub struct PageEntry {
pub name: String,
pub seq_suffix: u32,
pub flags: u8,
}
impl PageEntry {
pub fn auto_load(&self) -> bool {
self.flags & PAGE_PREVENT_AUTO_LOAD == 0
}
}
#[derive(Debug, Clone)]
pub struct AgeDescription {
pub age_name: String,
pub start_date_time: u32,
pub day_length: f32,
pub max_capacity: i32,
pub linger_time: i32,
pub sequence_prefix: i32,
pub release_version: u32,
pub pages: Vec<PageEntry>,
}
impl AgeDescription {
pub fn from_file(path: &Path) -> Result<Self> {
let age_name = path.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string();
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read {}", path.display()))?;
Self::parse(&age_name, &content)
}
pub fn parse(age_name: &str, content: &str) -> Result<Self> {
let mut desc = Self {
age_name: age_name.to_string(),
start_date_time: 0,
day_length: 24.0,
max_capacity: -1,
linger_time: -1,
sequence_prefix: 0,
release_version: 0,
pages: Vec::new(),
};
let mut next_suffix = 1u32;
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with('[') {
continue;
}
let (key, value) = match line.split_once('=') {
Some(kv) => kv,
None => continue,
};
match key.trim() {
"StartDateTime" => {
desc.start_date_time = value.trim().parse().unwrap_or(0);
}
"DayLength" => {
desc.day_length = value.trim().parse().unwrap_or(24.0);
}
"MaxCapacity" => {
desc.max_capacity = value.trim().parse().unwrap_or(-1);
}
"LingerTime" => {
desc.linger_time = value.trim().parse().unwrap_or(-1);
}
"SequencePrefix" => {
desc.sequence_prefix = value.trim().parse().unwrap_or(0);
}
"ReleaseVersion" => {
desc.release_version = value.trim().parse().unwrap_or(0);
}
"Page" => {
let parts: Vec<&str> = value.trim().split(',').collect();
let name = parts[0].to_string();
let seq_suffix = if parts.len() > 1 {
parts[1].parse().unwrap_or(next_suffix)
} else {
next_suffix
};
let flags = if parts.len() > 2 {
parts[2].parse().unwrap_or(0)
} else {
0
};
next_suffix = seq_suffix + 1;
desc.pages.push(PageEntry { name, seq_suffix, flags });
}
_ => {}
}
}
Ok(desc)
}
pub fn auto_load_pages(&self) -> Vec<&PageEntry> {
self.pages.iter().filter(|p| p.auto_load()).collect()
}
pub fn page_location(&self, page: &PageEntry) -> u32 {
if self.sequence_prefix < 0 {
let combined = ((-self.sequence_prefix as u32) << 16) | (page.seq_suffix & 0xFFFF);
combined } else {
((self.sequence_prefix as u32) << 16) | (page.seq_suffix & 0xFFFF)
}
}
pub fn prp_filename(&self, page: &PageEntry) -> String {
format!("{}_District_{}.prp", self.age_name, page.name)
}
pub fn common_page_filenames(&self) -> Vec<String> {
vec![
format!("{}_District_Textures.prp", self.age_name),
format!("{}_District_BuiltIn.prp", self.age_name),
]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_cleft_age() {
let content = r#"StartDateTime=0000000000
DayLength=24.000000
MaxCapacity=1
LingerTime=180
SequencePrefix=7
ReleaseVersion=0
Page=BookRoom,2
Page=Cleft,1
Page=clftAtrusGoggles,30,1
Page=Desert,0,1
"#;
let desc = AgeDescription::parse("Cleft", content).unwrap();
assert_eq!(desc.age_name, "Cleft");
assert_eq!(desc.sequence_prefix, 7);
assert_eq!(desc.max_capacity, 1);
assert_eq!(desc.linger_time, 180);
assert_eq!(desc.pages.len(), 4);
assert!(desc.pages[0].auto_load());
assert_eq!(desc.pages[0].name, "BookRoom");
assert_eq!(desc.pages[0].seq_suffix, 2);
assert!(desc.pages[1].auto_load());
assert!(!desc.pages[2].auto_load());
let auto = desc.auto_load_pages();
assert_eq!(auto.len(), 2);
assert_eq!(desc.prp_filename(&desc.pages[0]), "Cleft_District_BookRoom.prp");
assert_eq!(desc.page_location(&desc.pages[0]), (7 << 16) | 2);
}
}