openstranded_common_building/
building_def.rs1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum BuildingGroup {
11 Building,
13 Storage,
15 Production,
17 #[serde(untagged)]
19 Custom(String),
20}
21
22impl BuildingGroup {
23 #[must_use]
25 pub fn from_inf(s: &str) -> Self {
26 match s.trim() {
27 "building" => Self::Building,
28 "storage" => Self::Storage,
29 "production" => Self::Production,
30 other => Self::Custom(other.to_owned()),
31 }
32 }
33
34 #[must_use]
36 pub fn as_str(&self) -> &str {
37 match self {
38 Self::Building => "building",
39 Self::Storage => "storage",
40 Self::Production => "production",
41 Self::Custom(s) => s.as_str(),
42 }
43 }
44}
45
46impl Default for BuildingGroup {
47 fn default() -> Self {
48 Self::Building
49 }
50}
51
52impl std::fmt::Display for BuildingGroup {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 f.write_str(self.as_str())
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(rename_all = "snake_case")]
63pub enum BuildSpace {
64 Land,
66 LandAndWater,
68 Water,
70 Shore,
72 Hill,
74 ShallowWater,
76 AtObject,
78}
79
80impl BuildSpace {
81 #[must_use]
83 pub fn from_inf(s: &str) -> Option<Self> {
84 match s.trim() {
85 "land" => Some(Self::Land),
86 "land and water" => Some(Self::LandAndWater),
87 "water" => Some(Self::Water),
88 "shore" => Some(Self::Shore),
89 "hill" => Some(Self::Hill),
90 "shallow water" => Some(Self::ShallowWater),
91 "at object" => Some(Self::AtObject),
92 _ => None,
93 }
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
105pub struct BuildingRequirement {
106 pub item_id: u32,
108
109 #[serde(default = "default_count")]
111 pub count: u32,
112}
113
114const fn default_count() -> u32 {
115 1
116}
117
118impl BuildingRequirement {
119 #[must_use]
121 pub fn from_inf_value(s: &str) -> Self {
122 let parts: Vec<&str> = s.splitn(2, ',').collect();
123 let item_id = parts.first().and_then(|p| p.trim().parse().ok()).unwrap_or(0);
124 let count = parts
125 .get(1)
126 .and_then(|p| p.trim().parse().ok())
127 .unwrap_or(1);
128 Self { item_id, count }
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
151pub struct BuildingDef {
152 pub id: u32,
154
155 #[serde(default)]
157 pub group: BuildingGroup,
158
159 #[serde(default)]
162 pub object_id: Option<u32>,
163
164 #[serde(default)]
167 pub unit_id: Option<u32>,
168
169 #[serde(default)]
171 pub requirements: Vec<BuildingRequirement>,
172
173 #[serde(default)]
175 pub build_space: Option<BuildSpace>,
176
177 #[serde(default)]
179 pub building_site: Option<u32>,
180
181 #[serde(default)]
183 pub at_objects: Vec<u32>,
184
185 #[serde(default)]
187 pub script: Option<String>,
188}
189
190fn first_str<'a>(fields: &'a HashMap<String, Vec<String>>, key: &str) -> Option<&'a str> {
193 fields.get(key)?.first().map(String::as_str)
194}
195
196fn first_string(fields: &HashMap<String, Vec<String>>, key: &str) -> Option<String> {
197 first_str(fields, key).map(ToOwned::to_owned)
198}
199
200fn first_u32(fields: &HashMap<String, Vec<String>>, key: &str) -> Option<u32> {
201 first_str(fields, key)?.parse().ok()
202}
203
204fn parse_requirements(fields: &HashMap<String, Vec<String>>) -> Vec<BuildingRequirement> {
205 fields
206 .get("req")
207 .map(|vals| {
208 vals.iter()
209 .map(|v| BuildingRequirement::from_inf_value(v))
210 .collect()
211 })
212 .unwrap_or_default()
213}
214
215fn parse_u32_list(fields: &HashMap<String, Vec<String>>, key: &str) -> Vec<u32> {
216 fields
217 .get(key)
218 .map(|vals| {
219 vals.iter()
220 .filter_map(|v| v.trim().parse().ok())
221 .collect()
222 })
223 .unwrap_or_default()
224}
225
226impl BuildingDef {
227 #[must_use]
233 pub fn from_inf_fields(fields: &HashMap<String, Vec<String>>) -> Option<Self> {
234 let id = first_u32(fields, "id")?;
235
236 let group = first_str(fields, "group")
237 .map(BuildingGroup::from_inf)
238 .unwrap_or_default();
239
240 let object_id = first_u32(fields, "objectid");
241 let unit_id = first_u32(fields, "unitid");
242
243 let requirements = parse_requirements(fields);
244
245 let build_space = first_str(fields, "buildspace").and_then(BuildSpace::from_inf);
246
247 let building_site = first_u32(fields, "buildingsite");
248
249 let at_objects = parse_u32_list(fields, "atobject");
250
251 let script = None;
254
255 Some(Self {
256 id,
257 group,
258 object_id,
259 unit_id,
260 requirements,
261 build_space,
262 building_site,
263 at_objects,
264 script,
265 })
266 }
267
268 #[must_use]
270 pub fn with_script(mut self, script: Option<String>) -> Self {
271 self.script = script;
272 self
273 }
274}
275
276#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
285 fn test_building_group_from_inf() {
286 assert_eq!(BuildingGroup::from_inf("building"), BuildingGroup::Building);
287 assert_eq!(BuildingGroup::from_inf("storage"), BuildingGroup::Storage);
288 assert_eq!(BuildingGroup::from_inf("production"), BuildingGroup::Production);
289 assert_eq!(
290 BuildingGroup::from_inf("custom"),
291 BuildingGroup::Custom("custom".into())
292 );
293 }
294
295 #[test]
296 fn test_building_group_as_str() {
297 assert_eq!(BuildingGroup::Building.as_str(), "building");
298 assert_eq!(BuildingGroup::Custom("fort".into()).as_str(), "fort");
299 }
300
301 #[test]
302 fn test_building_group_display() {
303 assert_eq!(BuildingGroup::Storage.to_string(), "storage");
304 }
305
306 #[test]
309 fn test_build_space_from_inf() {
310 assert_eq!(BuildSpace::from_inf("land"), Some(BuildSpace::Land));
311 assert_eq!(
312 BuildSpace::from_inf("land and water"),
313 Some(BuildSpace::LandAndWater)
314 );
315 assert_eq!(BuildSpace::from_inf("water"), Some(BuildSpace::Water));
316 assert_eq!(BuildSpace::from_inf("shore"), Some(BuildSpace::Shore));
317 assert_eq!(BuildSpace::from_inf("hill"), Some(BuildSpace::Hill));
318 assert_eq!(
319 BuildSpace::from_inf("shallow water"),
320 Some(BuildSpace::ShallowWater)
321 );
322 assert_eq!(
323 BuildSpace::from_inf("at object"),
324 Some(BuildSpace::AtObject)
325 );
326 assert_eq!(BuildSpace::from_inf("invalid"), None);
327 }
328
329 #[test]
332 fn test_building_req_minimal() {
333 let req = BuildingRequirement::from_inf_value("24");
334 assert_eq!(req.item_id, 24);
335 assert_eq!(req.count, 1);
336 }
337
338 #[test]
339 fn test_building_req_with_count() {
340 let req = BuildingRequirement::from_inf_value("24,5");
341 assert_eq!(req.item_id, 24);
342 assert_eq!(req.count, 5);
343 }
344
345 #[test]
348 fn test_building_from_inf_fields_minimal() {
349 let mut fields = HashMap::new();
350 fields.insert("id".into(), vec!["1".into()]);
351 fields.insert("group".into(), vec!["building".into()]);
352 fields.insert("objectid".into(), vec!["184".into()]);
353
354 let def = BuildingDef::from_inf_fields(&fields).unwrap();
355 assert_eq!(def.id, 1);
356 assert_eq!(def.group, BuildingGroup::Building);
357 assert_eq!(def.object_id, Some(184));
358 assert!(def.unit_id.is_none());
359 assert!(def.requirements.is_empty());
360 }
361
362 #[test]
363 fn test_building_with_requirements() {
364 let mut fields = HashMap::new();
365 fields.insert("id".into(), vec!["8".into()]);
366 fields.insert("group".into(), vec!["building".into()]);
367 fields.insert("objectid".into(), vec!["190".into()]);
368 fields.insert(
369 "req".into(),
370 vec!["7,10".into(), "24,25".into(), "26,30".into()],
371 );
372 fields.insert("buildspace".into(), vec!["at object".into()]);
373 fields.insert("atobject".into(), vec!["13".into(), "16".into(), "17".into()]);
374
375 let def = BuildingDef::from_inf_fields(&fields).unwrap();
376 assert_eq!(def.requirements.len(), 3);
377 assert_eq!(def.requirements[0].item_id, 7);
378 assert_eq!(def.requirements[0].count, 10);
379 assert_eq!(def.build_space, Some(BuildSpace::AtObject));
380 assert_eq!(def.at_objects, vec![13, 16, 17]);
381 }
382
383 #[test]
384 fn test_building_with_unitid() {
385 let mut fields = HashMap::new();
386 fields.insert("id".into(), vec!["5".into()]);
387 fields.insert("group".into(), vec!["production".into()]);
388 fields.insert("unitid".into(), vec!["10".into()]);
389
390 let def = BuildingDef::from_inf_fields(&fields).unwrap();
391 assert_eq!(def.unit_id, Some(10));
392 assert!(def.object_id.is_none());
393 assert_eq!(def.group, BuildingGroup::Production);
394 }
395
396 #[test]
397 fn test_building_missing_id() {
398 let fields = HashMap::new();
399 assert!(BuildingDef::from_inf_fields(&fields).is_none());
400 }
401
402 #[test]
403 fn test_building_with_script() {
404 let mut fields = HashMap::new();
405 fields.insert("id".into(), vec!["2".into()]);
406 fields.insert("objectid".into(), vec!["165".into()]);
407 fields.insert("req".into(), vec!["24,20".into(), "15,30".into()]);
408
409 let def = BuildingDef::from_inf_fields(&fields)
410 .unwrap()
411 .with_script(Some("msg \"Built!\";".into()));
412 assert_eq!(def.script.as_deref(), Some("msg \"Built!\";"));
413 }
414
415 #[test]
416 fn test_building_with_buildingsite() {
417 let mut fields = HashMap::new();
418 fields.insert("id".into(), vec!["1".into()]);
419 fields.insert("objectid".into(), vec!["150".into()]);
420 fields.insert("buildingsite".into(), vec!["150".into()]);
421
422 let def = BuildingDef::from_inf_fields(&fields).unwrap();
423 assert_eq!(def.building_site, Some(150));
424 }
425
426 #[test]
427 fn test_building_serde_derives_compile() {
428 let def = BuildingDef {
429 id: 1,
430 group: BuildingGroup::Building,
431 object_id: Some(184),
432 unit_id: None,
433 requirements: vec![],
434 build_space: None,
435 building_site: None,
436 at_objects: vec![],
437 script: None,
438 };
439 assert_eq!(def.id, 1);
440 }
441}