bitsy_parser/
room.rs

1use crate::{
2    optional_data_line,
3    Exit,
4    ExitInstance,
5    Instance,
6    Position,
7    RoomType,
8    RoomFormat,
9    Transition
10};
11
12use std::collections::HashMap;
13
14#[derive(Clone, Debug, Eq, PartialEq)]
15pub struct Room {
16    pub id: String,
17    /// palette ID was optional in very early versions
18    pub palette_id: Option<String>,
19    pub name: Option<String>,
20    /// tile IDs
21    pub tiles: Vec<String>,
22    pub items: Vec<Instance>,
23    pub exits: Vec<ExitInstance>,
24    pub endings: Vec<Instance>,
25    /// old method of handling walls - a comma-separated list of tile IDs
26    pub walls: Option<Vec<String>>,
27}
28
29impl Room {
30    fn name_line(&self) -> String {
31        optional_data_line("NAME", self.name.as_ref())
32    }
33
34    fn wall_line(&self) -> String {
35        if let Some(walls) = &self.walls {
36            optional_data_line("WAL", Some(walls.join(",")))
37        } else {
38            "".to_string()
39        }
40    }
41
42    fn palette_line(&self) -> String {
43        match &self.palette_id {
44            Some(id) => optional_data_line("PAL", Some(id.clone())),
45            None => "".to_string(),
46        }
47    }
48}
49
50impl From<String> for Room {
51    fn from(string: String) -> Room {
52        let string = string.replace("ROOM ", "");
53        let string = string.replace("SET ", "");
54        let mut lines: Vec<&str> = string.lines().collect();
55        let id = lines[0].to_string();
56        let mut name = None;
57        let mut palette_id = None;
58        let mut items: Vec<Instance> = Vec::new();
59        let mut exits: Vec<ExitInstance> = Vec::new();
60        let mut endings: Vec<Instance> = Vec::new();
61        let mut walls = None;
62
63        loop {
64            let last_line = lines.pop().unwrap();
65
66            if last_line.starts_with("WAL") {
67                let last_line = last_line.replace("WAL ", "");
68                let ids: Vec<&str> = last_line.split(',').collect();
69                walls = Some(ids.iter().map(|&id| id.to_string()).collect());
70            } else if last_line.starts_with("NAME") {
71                name = Some(last_line.replace("NAME ", "").to_string());
72            } else if last_line.starts_with("PAL") {
73                palette_id = Some(last_line.replace("PAL ", ""));
74            } else if last_line.starts_with("ITM") {
75                let last_line = last_line.replace("ITM ", "");
76                let item_position: Vec<&str> = last_line.split(' ').collect();
77                let item_id = item_position[0];
78                let position = item_position[1];
79
80                if let Ok(position) = Position::from_str(position) {
81                    items.push(Instance { position, id: item_id.to_string() });
82                }
83            } else if last_line.starts_with("EXT") {
84                let last_line = last_line.replace("EXT ", "");
85                let parts: Vec<&str> = last_line.split(' ').collect();
86                let position = Position::from_str(parts[0]);
87
88                if let Ok(position) = position {
89                    let exit = Exit::from_str(
90                        &format!("{} {}", parts[1], parts[2])
91                    );
92
93                    if let Ok(exit) = exit {
94                        let mut transition = None;
95                        let mut dialogue_id = None;
96
97                        let chunks = parts[3..].chunks(2);
98
99                        for chunk in chunks {
100                            if chunk[0] == "FX" {
101                                transition = Some(Transition::from_str(chunk[1]).unwrap());
102                            } else if chunk[0] == "DLG" {
103                                dialogue_id = Some(chunk[1].to_string());
104                            }
105                        }
106
107                        exits.push(ExitInstance { position, exit, transition, dialogue_id });
108                    }
109                }
110            } else if last_line.starts_with("END") {
111                let last_line = last_line.replace("END ", "");
112                let ending_position: Vec<&str> = last_line.split(' ').collect();
113                let ending = ending_position[0].to_string();
114                let position = ending_position[1];
115                let position = Position::from_str(position);
116
117                if let Ok(position) = position {
118                    endings.push(Instance { position, id: ending });
119                }
120            } else {
121                lines.push(last_line);
122                break;
123            }
124        }
125
126        let lines = &lines[1..];
127        let dimension = lines.len(); // x or y, e.g. `16` for 16x16
128        let mut tiles: Vec<String> = Vec::new();
129
130        for line in lines.iter() {
131            let comma_separated = line.contains(','); // old room format?
132            let mut line: Vec<&str> = line
133                .split(if comma_separated {","} else {""})
134                .collect();
135
136            if ! comma_separated { line = line[1..].to_owned(); }
137            let line = line[..dimension].to_owned();
138
139            for tile_id in line {
140                tiles.push(tile_id.to_string());
141            }
142        }
143
144        items.reverse();
145        exits.reverse();
146        endings.reverse();
147
148        Room {
149            id,
150            palette_id,
151            name,
152            tiles,
153            items,
154            exits,
155            endings,
156            walls,
157        }
158    }
159}
160
161impl Room {
162    pub fn to_string(&self, room_format: RoomFormat, room_type: RoomType) -> String {
163        let mut tiles = String::new();
164        let mut items = String::new();
165        let mut exits = String::new();
166        let mut endings = String::new();
167
168        for line in self.tiles.chunks(16) {
169            for tile in line {
170                tiles.push_str(tile);
171                if room_format == RoomFormat::CommaSeparated {
172                    tiles.push(',');
173                }
174            }
175
176            if room_format == RoomFormat::CommaSeparated {
177                tiles.pop(); // remove trailing comma
178            }
179
180            tiles.push('\n');
181        }
182
183        tiles.pop(); // remove trailing newline
184
185        for instance in &self.items {
186            items.push_str(&format!(
187                "\nITM {} {}",
188                instance.id,
189                instance.position.to_string()
190            ));
191        }
192
193        for instance in &self.exits {
194            exits.push_str(&format!(
195                "\nEXT {} {}{}{}{}",
196                instance.position.to_string(),
197                instance.exit.to_string(),
198                match &instance.transition {
199                    Some(transition) => transition,
200                    None => &Transition::None,
201                }.to_string(),
202                if instance.dialogue_id.is_some() {" DLG "} else {""},
203                instance.dialogue_id.as_ref().unwrap_or(&"".to_string()),
204            ));
205        }
206
207        for instance in &self.endings {
208            endings.push_str(&format!(
209                "\nEND {} {}",
210                instance.id,
211                instance.position.to_string()
212            ));
213        }
214
215        format!(
216            "{} {}\n{}{}{}{}{}{}{}",
217            room_type.to_string(),
218            self.id,
219            tiles,
220            self.name_line(),
221            self.wall_line(),
222            items,
223            exits,
224            endings,
225            self.palette_line()
226        )
227    }
228
229    /// "changes" is a hash of old -> new tile IDs
230    pub fn change_tile_ids(&mut self, changes: &HashMap<String, String>) {
231        self.tiles = self.tiles.iter().map(|tile_id|
232            changes.get(tile_id).unwrap_or(tile_id).clone()
233        ).collect();
234    }
235}
236
237#[cfg(test)]
238mod test {
239    use crate::{Room, RoomType, RoomFormat};
240
241    #[test]
242    fn room_from_string() {
243        assert_eq!(
244            Room::from(include_str!("test-resources/room").to_string()),
245            crate::mock::room()
246        );
247    }
248
249    #[test]
250    fn room_to_string() {
251        assert_eq!(
252            crate::mock::room().to_string(RoomFormat::CommaSeparated, RoomType::Room),
253            include_str!("test-resources/room").to_string()
254        );
255    }
256
257    #[test]
258    fn room_walls_array() {
259        let output = Room::from(include_str!("test-resources/room-with-walls").to_string());
260
261        assert_eq!(output.walls, Some(vec!["a".to_string(), "f".to_string()]));
262    }
263}