use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BuildingGroup {
Building,
Storage,
Production,
#[serde(untagged)]
Custom(String),
}
impl BuildingGroup {
#[must_use]
pub fn from_inf(s: &str) -> Self {
match s.trim() {
"building" => Self::Building,
"storage" => Self::Storage,
"production" => Self::Production,
other => Self::Custom(other.to_owned()),
}
}
#[must_use]
pub fn as_str(&self) -> &str {
match self {
Self::Building => "building",
Self::Storage => "storage",
Self::Production => "production",
Self::Custom(s) => s.as_str(),
}
}
}
impl Default for BuildingGroup {
fn default() -> Self {
Self::Building
}
}
impl std::fmt::Display for BuildingGroup {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BuildSpace {
Land,
LandAndWater,
Water,
Shore,
Hill,
ShallowWater,
AtObject,
}
impl BuildSpace {
#[must_use]
pub fn from_inf(s: &str) -> Option<Self> {
match s.trim() {
"land" => Some(Self::Land),
"land and water" => Some(Self::LandAndWater),
"water" => Some(Self::Water),
"shore" => Some(Self::Shore),
"hill" => Some(Self::Hill),
"shallow water" => Some(Self::ShallowWater),
"at object" => Some(Self::AtObject),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BuildingRequirement {
pub item_id: u32,
#[serde(default = "default_count")]
pub count: u32,
}
const fn default_count() -> u32 {
1
}
impl BuildingRequirement {
#[must_use]
pub fn from_inf_value(s: &str) -> Self {
let parts: Vec<&str> = s.splitn(2, ',').collect();
let item_id = parts.first().and_then(|p| p.trim().parse().ok()).unwrap_or(0);
let count = parts
.get(1)
.and_then(|p| p.trim().parse().ok())
.unwrap_or(1);
Self { item_id, count }
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BuildingDef {
pub id: u32,
#[serde(default)]
pub group: BuildingGroup,
#[serde(default)]
pub object_id: Option<u32>,
#[serde(default)]
pub unit_id: Option<u32>,
#[serde(default)]
pub requirements: Vec<BuildingRequirement>,
#[serde(default)]
pub build_space: Option<BuildSpace>,
#[serde(default)]
pub building_site: Option<u32>,
#[serde(default)]
pub at_objects: Vec<u32>,
#[serde(default)]
pub script: Option<String>,
}
fn first_str<'a>(fields: &'a HashMap<String, Vec<String>>, key: &str) -> Option<&'a str> {
fields.get(key)?.first().map(String::as_str)
}
fn first_string(fields: &HashMap<String, Vec<String>>, key: &str) -> Option<String> {
first_str(fields, key).map(ToOwned::to_owned)
}
fn first_u32(fields: &HashMap<String, Vec<String>>, key: &str) -> Option<u32> {
first_str(fields, key)?.parse().ok()
}
fn parse_requirements(fields: &HashMap<String, Vec<String>>) -> Vec<BuildingRequirement> {
fields
.get("req")
.map(|vals| {
vals.iter()
.map(|v| BuildingRequirement::from_inf_value(v))
.collect()
})
.unwrap_or_default()
}
fn parse_u32_list(fields: &HashMap<String, Vec<String>>, key: &str) -> Vec<u32> {
fields
.get(key)
.map(|vals| {
vals.iter()
.filter_map(|v| v.trim().parse().ok())
.collect()
})
.unwrap_or_default()
}
impl BuildingDef {
#[must_use]
pub fn from_inf_fields(fields: &HashMap<String, Vec<String>>) -> Option<Self> {
let id = first_u32(fields, "id")?;
let group = first_str(fields, "group")
.map(BuildingGroup::from_inf)
.unwrap_or_default();
let object_id = first_u32(fields, "objectid");
let unit_id = first_u32(fields, "unitid");
let requirements = parse_requirements(fields);
let build_space = first_str(fields, "buildspace").and_then(BuildSpace::from_inf);
let building_site = first_u32(fields, "buildingsite");
let at_objects = parse_u32_list(fields, "atobject");
let script = None;
Some(Self {
id,
group,
object_id,
unit_id,
requirements,
build_space,
building_site,
at_objects,
script,
})
}
#[must_use]
pub fn with_script(mut self, script: Option<String>) -> Self {
self.script = script;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_building_group_from_inf() {
assert_eq!(BuildingGroup::from_inf("building"), BuildingGroup::Building);
assert_eq!(BuildingGroup::from_inf("storage"), BuildingGroup::Storage);
assert_eq!(BuildingGroup::from_inf("production"), BuildingGroup::Production);
assert_eq!(
BuildingGroup::from_inf("custom"),
BuildingGroup::Custom("custom".into())
);
}
#[test]
fn test_building_group_as_str() {
assert_eq!(BuildingGroup::Building.as_str(), "building");
assert_eq!(BuildingGroup::Custom("fort".into()).as_str(), "fort");
}
#[test]
fn test_building_group_display() {
assert_eq!(BuildingGroup::Storage.to_string(), "storage");
}
#[test]
fn test_build_space_from_inf() {
assert_eq!(BuildSpace::from_inf("land"), Some(BuildSpace::Land));
assert_eq!(
BuildSpace::from_inf("land and water"),
Some(BuildSpace::LandAndWater)
);
assert_eq!(BuildSpace::from_inf("water"), Some(BuildSpace::Water));
assert_eq!(BuildSpace::from_inf("shore"), Some(BuildSpace::Shore));
assert_eq!(BuildSpace::from_inf("hill"), Some(BuildSpace::Hill));
assert_eq!(
BuildSpace::from_inf("shallow water"),
Some(BuildSpace::ShallowWater)
);
assert_eq!(
BuildSpace::from_inf("at object"),
Some(BuildSpace::AtObject)
);
assert_eq!(BuildSpace::from_inf("invalid"), None);
}
#[test]
fn test_building_req_minimal() {
let req = BuildingRequirement::from_inf_value("24");
assert_eq!(req.item_id, 24);
assert_eq!(req.count, 1);
}
#[test]
fn test_building_req_with_count() {
let req = BuildingRequirement::from_inf_value("24,5");
assert_eq!(req.item_id, 24);
assert_eq!(req.count, 5);
}
#[test]
fn test_building_from_inf_fields_minimal() {
let mut fields = HashMap::new();
fields.insert("id".into(), vec!["1".into()]);
fields.insert("group".into(), vec!["building".into()]);
fields.insert("objectid".into(), vec!["184".into()]);
let def = BuildingDef::from_inf_fields(&fields).unwrap();
assert_eq!(def.id, 1);
assert_eq!(def.group, BuildingGroup::Building);
assert_eq!(def.object_id, Some(184));
assert!(def.unit_id.is_none());
assert!(def.requirements.is_empty());
}
#[test]
fn test_building_with_requirements() {
let mut fields = HashMap::new();
fields.insert("id".into(), vec!["8".into()]);
fields.insert("group".into(), vec!["building".into()]);
fields.insert("objectid".into(), vec!["190".into()]);
fields.insert(
"req".into(),
vec!["7,10".into(), "24,25".into(), "26,30".into()],
);
fields.insert("buildspace".into(), vec!["at object".into()]);
fields.insert("atobject".into(), vec!["13".into(), "16".into(), "17".into()]);
let def = BuildingDef::from_inf_fields(&fields).unwrap();
assert_eq!(def.requirements.len(), 3);
assert_eq!(def.requirements[0].item_id, 7);
assert_eq!(def.requirements[0].count, 10);
assert_eq!(def.build_space, Some(BuildSpace::AtObject));
assert_eq!(def.at_objects, vec![13, 16, 17]);
}
#[test]
fn test_building_with_unitid() {
let mut fields = HashMap::new();
fields.insert("id".into(), vec!["5".into()]);
fields.insert("group".into(), vec!["production".into()]);
fields.insert("unitid".into(), vec!["10".into()]);
let def = BuildingDef::from_inf_fields(&fields).unwrap();
assert_eq!(def.unit_id, Some(10));
assert!(def.object_id.is_none());
assert_eq!(def.group, BuildingGroup::Production);
}
#[test]
fn test_building_missing_id() {
let fields = HashMap::new();
assert!(BuildingDef::from_inf_fields(&fields).is_none());
}
#[test]
fn test_building_with_script() {
let mut fields = HashMap::new();
fields.insert("id".into(), vec!["2".into()]);
fields.insert("objectid".into(), vec!["165".into()]);
fields.insert("req".into(), vec!["24,20".into(), "15,30".into()]);
let def = BuildingDef::from_inf_fields(&fields)
.unwrap()
.with_script(Some("msg \"Built!\";".into()));
assert_eq!(def.script.as_deref(), Some("msg \"Built!\";"));
}
#[test]
fn test_building_with_buildingsite() {
let mut fields = HashMap::new();
fields.insert("id".into(), vec!["1".into()]);
fields.insert("objectid".into(), vec!["150".into()]);
fields.insert("buildingsite".into(), vec!["150".into()]);
let def = BuildingDef::from_inf_fields(&fields).unwrap();
assert_eq!(def.building_site, Some(150));
}
#[test]
fn test_building_serde_derives_compile() {
let def = BuildingDef {
id: 1,
group: BuildingGroup::Building,
object_id: Some(184),
unit_id: None,
requirements: vec![],
build_space: None,
building_site: None,
at_objects: vec![],
script: None,
};
assert_eq!(def.id, 1);
}
}