initiative_core/world/command/
parse.rs

1use crate::utils::{capitalize, quoted_words, CaseInsensitiveStr};
2use crate::world::command::ParsedThing;
3use crate::world::npc::NpcData;
4use crate::world::place::PlaceData;
5use crate::world::Field;
6use std::str::FromStr;
7
8fn split_name(input: &str) -> Option<(&str, &str)> {
9    let (named, comma) = quoted_words(input).fold((None, None), |(named, comma), word| {
10        if named.is_none() && word.as_str().in_ci(&["named", "called"]) {
11            (Some(word), comma)
12        } else if word.as_str().ends_with(',') {
13            (named, Some(word))
14        } else {
15            (named, comma)
16        }
17    });
18
19    let (name, description) = if let Some(word) = named {
20        // "a boy named Sue"
21        (&input[word.range().end..], &input[..word.range().start])
22    } else if let Some(word) = comma {
23        // "Nott the Brave, a goblin"
24        (
25            input[..word.range().end].trim_end_matches(','),
26            &input[word.range().end..],
27        )
28    } else {
29        return None;
30    };
31
32    if let (Some(name_start), Some(name_end)) =
33        quoted_words(name).fold((None, None), |(name_start, _), word| {
34            (
35                name_start.or_else(|| Some(word.range().start)),
36                Some(word.range().end),
37            )
38        })
39    {
40        let name = &name[name_start..name_end];
41        if let Some(name_stripped) = name.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
42            Some((name_stripped, description))
43        } else {
44            Some((name, description))
45        }
46    } else {
47        None
48    }
49}
50
51impl FromStr for ParsedThing<PlaceData> {
52    type Err = ();
53
54    fn from_str(input: &str) -> Result<Self, Self::Err> {
55        let mut place = PlaceData::default();
56        let mut unknown_words = Vec::new();
57        let mut word_count = 0;
58
59        let description = if let Some((name, description)) = split_name(input) {
60            place.name = Field::new(capitalize(name));
61            description
62        } else {
63            input
64        };
65
66        for word in quoted_words(description) {
67            let word_str = &word.as_str();
68            word_count += 1;
69
70            if word_str.in_ci(&["a", "an"]) {
71                word_count -= 1;
72            } else if let Ok(place_type) = word_str.parse() {
73                place.subtype = Field::new(place_type);
74            } else {
75                unknown_words.push(word.range().to_owned());
76            }
77        }
78
79        if unknown_words.is_empty() || unknown_words.len() <= word_count / 2 {
80            Ok(ParsedThing {
81                thing_data: place,
82                unknown_words,
83                word_count,
84            })
85        } else {
86            Err(())
87        }
88    }
89}
90
91impl FromStr for ParsedThing<NpcData> {
92    type Err = ();
93
94    fn from_str(input: &str) -> Result<Self, Self::Err> {
95        let mut npc = NpcData::default();
96        let mut unknown_words = Vec::new();
97        let mut word_count = 0;
98
99        let description = if let Some((name, description)) = split_name(input) {
100            npc.name = Field::new(capitalize(name));
101            description
102        } else {
103            input
104        };
105
106        for word in quoted_words(description) {
107            let word_str = &word.as_str();
108            word_count += 1;
109
110            if word_str.in_ci(&["a", "an"]) {
111                word_count -= 1;
112            } else if word_str.in_ci(&["character", "npc", "person"]) {
113                // ignore
114            } else if let Ok(gender) = word_str.parse() {
115                npc.gender = Field::new(gender);
116
117                if let Ok(age) = word_str.parse() {
118                    // Terms like "boy" and "woman" imply both age and gender, although let's treat
119                    // them as secondary to other specifiers. "Old boy" and "baby woman" sound a
120                    // bit odd but are presumably elderly and infant, respectively.
121                    npc.age.replace(age);
122                    npc.age.lock();
123                }
124            } else if let Ok(age) = word_str.parse() {
125                npc.age = Field::new(age);
126            } else if let Ok(species) = word_str.parse() {
127                npc.species = Field::new(species);
128
129                if let Ok(ethnicity) = word_str.parse() {
130                    npc.ethnicity.replace(ethnicity);
131                    npc.ethnicity.lock();
132                }
133            } else if let Ok(ethnicity) = word_str.parse() {
134                npc.ethnicity = Field::new(ethnicity);
135            } else if let Some(Ok(age_years)) =
136                word_str.strip_suffix_ci("-year-old").map(|s| s.parse())
137            {
138                npc.age_years = Field::new(age_years);
139            } else {
140                unknown_words.push(word.range().to_owned());
141            }
142        }
143
144        if unknown_words.is_empty() || unknown_words.len() <= word_count / 2 {
145            Ok(ParsedThing {
146                thing_data: npc,
147                unknown_words,
148                word_count,
149            })
150        } else {
151            Err(())
152        }
153    }
154}
155
156#[cfg(test)]
157mod test {
158    use super::*;
159    use crate::world::npc::{Age, Gender, Species};
160    use crate::world::place::PlaceType;
161
162    #[test]
163    fn place_from_str_test() {
164        {
165            let place: ParsedThing<PlaceData> = "inn".parse().unwrap();
166            assert_eq!(
167                Field::Locked("inn".parse::<PlaceType>().ok()),
168                place.thing_data.subtype,
169            );
170            assert_eq!(0, place.unknown_words.len());
171            assert_eq!(1, place.word_count);
172        }
173
174        {
175            let place = "building named foo bar"
176                .parse::<ParsedThing<PlaceData>>()
177                .unwrap();
178            assert_eq!(
179                Some("Foo bar"),
180                place.thing_data.name.value().map(|s| s.as_str()),
181            );
182            assert_eq!(0, place.unknown_words.len());
183            assert_eq!(1, place.word_count);
184        }
185
186        {
187            let place: ParsedThing<PlaceData> = "The Prancing Pony, an inn".parse().unwrap();
188            assert_eq!(
189                Field::Locked(Some("The Prancing Pony".to_string())),
190                place.thing_data.name,
191            );
192            assert_eq!(
193                Field::Locked("inn".parse::<PlaceType>().ok()),
194                place.thing_data.subtype,
195            );
196            assert_eq!(0, place.unknown_words.len());
197            assert_eq!(1, place.word_count);
198        }
199
200        {
201            let place: ParsedThing<PlaceData> = "\"The Prancing Pony\", an inn".parse().unwrap();
202            assert_eq!(
203                Field::Locked(Some("The Prancing Pony".to_string())),
204                place.thing_data.name,
205            );
206            assert_eq!(
207                Field::Locked("inn".parse::<PlaceType>().ok()),
208                place.thing_data.subtype,
209            );
210            assert_eq!(0, place.unknown_words.len());
211            assert_eq!(1, place.word_count);
212        }
213
214        {
215            let place: ParsedThing<PlaceData> = "a place called home".parse().unwrap();
216            assert_eq!(
217                Field::Locked(Some("Home".to_string())),
218                place.thing_data.name
219            );
220            assert_eq!(Some(&PlaceType::Any), place.thing_data.subtype.value());
221            assert_eq!(0, place.unknown_words.len());
222            assert_eq!(1, place.word_count);
223        }
224    }
225
226    #[test]
227    fn npc_from_str_test() {
228        {
229            let npc: ParsedThing<NpcData> = "npc".parse().unwrap();
230            assert_eq!(NpcData::default(), npc.thing_data);
231            assert_eq!(0, npc.unknown_words.len());
232            assert_eq!(1, npc.word_count);
233        }
234        assert_eq!(
235            "npc".parse::<ParsedThing<NpcData>>().unwrap(),
236            "NPC".parse::<ParsedThing<NpcData>>().unwrap(),
237        );
238
239        {
240            let npc: ParsedThing<NpcData> = "elf".parse().unwrap();
241            assert_eq!(Field::Locked(Some(Species::Elf)), npc.thing_data.species);
242            assert_eq!(0, npc.unknown_words.len());
243            assert_eq!(1, npc.word_count);
244        }
245        assert_eq!(
246            "elf".parse::<ParsedThing<NpcData>>().unwrap(),
247            "ELF".parse::<ParsedThing<NpcData>>().unwrap(),
248        );
249
250        {
251            let npc: ParsedThing<NpcData> = "Potato Johnson, a non-binary elf".parse().unwrap();
252            assert_eq!(
253                Field::Locked(Some("Potato Johnson".to_string())),
254                npc.thing_data.name,
255            );
256            assert_eq!(Field::Locked(Some(Species::Elf)), npc.thing_data.species);
257            assert_eq!(
258                Field::Locked(Some(Gender::NonBinaryThey)),
259                npc.thing_data.gender
260            );
261            assert_eq!(0, npc.unknown_words.len());
262            assert_eq!(2, npc.word_count);
263        }
264        assert_eq!(
265            "Potato Johnson, a non-binary elf"
266                .parse::<ParsedThing<NpcData>>()
267                .unwrap(),
268            "Potato Johnson, A NON-BINARY ELF"
269                .parse::<ParsedThing<NpcData>>()
270                .unwrap(),
271        );
272
273        {
274            let npc: ParsedThing<NpcData> = "37-year-old boy named sue".parse().unwrap();
275            assert_eq!(Field::Locked(Some("Sue".to_string())), npc.thing_data.name);
276            assert_eq!(
277                Field::Locked(Some(Gender::Masculine)),
278                npc.thing_data.gender
279            );
280            assert_eq!(Field::Locked(Some(Age::Child)), npc.thing_data.age);
281            assert_eq!(Field::Locked(Some(37)), npc.thing_data.age_years);
282            assert_eq!(0, npc.unknown_words.len());
283            assert_eq!(2, npc.word_count);
284        }
285        assert_eq!(
286            "37-year-old boy named sue"
287                .parse::<ParsedThing<NpcData>>()
288                .unwrap(),
289            "37-YEAR-OLD BOY NAMED sue"
290                .parse::<ParsedThing<NpcData>>()
291                .unwrap(),
292        );
293
294        {
295            assert!("potato".parse::<ParsedThing<NpcData>>().is_err());
296        }
297    }
298}