bitsy_parser/
game.rs

1use crate::{Dialogue, Ending, Font, Image, Item, Palette, Room, Sprite, TextDirection, Tile, Variable, transform_line_endings, new_unique_id, try_id, Instance, Error};
2
3use loe::TransformMode;
4
5use std::collections::HashMap;
6use std::borrow::BorrowMut;
7use std::fmt;
8use crate::error::NotFound;
9
10/// in very early versions of Bitsy, room tiles were defined as single alphanumeric characters -
11/// so there was a maximum of 36 unique tiles. later versions are comma-separated.
12/// RoomFormat is implemented here so we can save in the original format.
13#[derive(Debug, Eq, PartialEq, Copy, Clone)]
14pub enum RoomFormat {Contiguous, CommaSeparated}
15
16#[derive(Debug)]
17pub struct InvalidRoomFormat;
18
19impl RoomFormat {
20    fn from(str: &str) -> Result<RoomFormat, InvalidRoomFormat> {
21        match str {
22            "0" => Ok(RoomFormat::Contiguous),
23            "1" => Ok(RoomFormat::CommaSeparated),
24            _   => Err(InvalidRoomFormat),
25        }
26    }
27}
28
29impl fmt::Display for RoomFormat {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        write!(f, "{}", match &self {
32            RoomFormat::Contiguous     => 0,
33            RoomFormat::CommaSeparated => 1,
34        })
35    }
36}
37
38/// in very early versions of Bitsy, a room was called a "set"
39#[derive(Debug, Eq, PartialEq, Copy, Clone)]
40pub enum RoomType {Room, Set}
41
42impl ToString for RoomType {
43    fn to_string(&self) -> String {
44        match &self {
45            RoomType::Set  => "SET",
46            RoomType::Room => "ROOM",
47        }.to_string()
48    }
49}
50
51#[derive(Debug, Eq, PartialEq, Copy, Clone)]
52pub struct Version {
53    pub major: u8,
54    pub minor: u8,
55}
56
57#[derive(Debug)]
58pub enum VersionError {
59    MissingParts,
60    ExtraneousParts,
61    MalformedInteger,
62}
63
64impl fmt::Display for VersionError {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        write!(f, "{}", match self {
67            VersionError::MissingParts     => "Not enough parts supplied for version",
68            VersionError::ExtraneousParts  => "Too many parts supplied for version",
69            VersionError::MalformedInteger => "Version did not contain valid integers",
70        })
71    }
72}
73
74impl std::error::Error for VersionError {}
75
76impl Version {
77    fn from(str: &str) -> Result<Version, VersionError> {
78        let parts: Vec<&str> = str.split('.').collect();
79
80        if parts.len() < 2 {
81            Err(VersionError::MissingParts)
82        } else if parts.len() > 2 {
83            Err(VersionError::ExtraneousParts)
84        } else if let (Ok(major), Ok(minor)) = (parts[0].parse(), parts[1].parse()) {
85            Ok(Version { major, minor })
86        } else {
87            Err(VersionError::MalformedInteger)
88        }
89    }
90}
91
92#[derive(Clone, Debug, PartialEq)]
93pub struct Game {
94    pub name: String,
95    pub version: Option<Version>,
96    pub room_format: Option<RoomFormat>,
97    pub(crate) room_type: RoomType,
98    pub font: Font,
99    pub custom_font: Option<String>, // used if font is Font::Custom
100    pub text_direction: TextDirection,
101    pub palettes: Vec<Palette>,
102    pub rooms: Vec<Room>,
103    pub tiles: Vec<Tile>,
104    pub sprites: Vec<Sprite>,
105    pub items: Vec<Item>,
106    pub dialogues: Vec<Dialogue>,
107    pub endings: Vec<Ending>,
108    pub variables: Vec<Variable>,
109    pub font_data: Option<String>, // todo make this an actual struct for parsing
110    pub(crate) line_endings_crlf: bool, // otherwise lf (unix/mac)
111}
112
113impl Game {
114    pub fn from(string: String) -> Result<(Game, Vec<crate::Error>), crate::error::NotFound> {
115        if string.trim() == "" {
116            return Err(crate::error::NotFound::Anything);
117        }
118
119        let mut warnings = Vec::new();
120
121        let line_endings_crlf = string.contains("\r\n");
122        let mut string = string;
123        if line_endings_crlf {
124            string = transform_line_endings(string, TransformMode::LF)
125        }
126
127        let string = string.trim_start_matches('\n').to_string();
128        let mut segments = crate::segments_from_str(&string);
129
130        let mut name = "".to_string();
131
132        // game names can be empty - so when we strip out the leading whitespace above,
133        // it means that the first segment might not be the game name.
134        // so, check if the first segment is actually the next segment of game data
135        // to avoid setting the game name to "# BITSY VERSION 7.0" or something
136        if
137            segments[0].starts_with("\"\"\"") // multi-line game name
138            ||
139            (
140                ! segments[0].starts_with("# BITSY VERSION ")
141                &&
142                ! segments[0].starts_with("! ROOM_FORMAT ")
143                &&
144                ! segments[0].starts_with("PAL ")
145                &&
146                ! segments[0].starts_with("DEFAULT_FONT ")
147                &&
148                ! segments[0].starts_with("TEXT_DIRECTION ")
149            )
150        {
151            name = segments[0].to_string();
152            segments = segments[1..].to_owned();
153        }
154
155        let segments = segments;
156
157        let name = name;
158        let mut dialogues: Vec<Dialogue> = Vec::new();
159        let mut endings: Vec<Ending> = Vec::new();
160        let mut variables: Vec<Variable> = Vec::new();
161        let mut font_data: Option<String> = None;
162
163        let mut version = None;
164        let mut room_format = None;
165        let mut room_type = RoomType::Room;
166        let mut font = Font::AsciiSmall;
167        let mut custom_font = None;
168        let mut text_direction = TextDirection::LeftToRight;
169        let mut palettes: Vec<Palette> = Vec::new();
170        let mut rooms: Vec<Room> = Vec::new();
171        let mut tiles: Vec<Tile> = Vec::new();
172        let mut sprites: Vec<Sprite> = Vec::new();
173        let mut items: Vec<Item> = Vec::new();
174        let mut avatar_exists = false;
175
176        for segment in segments {
177            if segment.starts_with("# BITSY VERSION") {
178                let segment = segment.replace("# BITSY VERSION ", "");
179                let result = Version::from(&segment);
180
181                if let Ok(v) = result {
182                    version = Some(v);
183                } else {
184                    warnings.push(Error::Version);
185                }
186            } else if segment.starts_with("! ROOM_FORMAT") {
187                let segment = segment.replace("! ROOM_FORMAT ", "");
188                room_format = Some(
189                    RoomFormat::from(&segment).unwrap_or(RoomFormat::CommaSeparated)
190                );
191            } else if segment.starts_with("DEFAULT_FONT") {
192                let segment = segment.replace("DEFAULT_FONT ", "");
193
194                font = Font::from(&segment);
195
196                if font == Font::Custom {
197                    custom_font = Some(segment.to_string());
198                }
199            } else if segment.trim() == "TEXT_DIRECTION RTL" {
200                text_direction = TextDirection::RightToLeft;
201            } else if segment.starts_with("PAL ") {
202                let result = Palette::from_str(&segment);
203                if let Ok((palette, mut errors)) = result {
204                    palettes.push(palette);
205                    warnings.append(&mut errors);
206                } else {
207                    warnings.push(result.unwrap_err());
208                }
209            } else if segment.starts_with("ROOM ") || segment.starts_with("SET ") {
210                if segment.starts_with("SET ") {
211                    room_type = RoomType::Set;
212                }
213                rooms.push(Room::from(segment));
214            } else if segment.starts_with("TIL ") {
215                tiles.push(Tile::from(segment));
216            } else if segment.starts_with("SPR ") {
217                let result = Sprite::from_str(&segment);
218
219                if let Ok(sprite) = result {
220                    avatar_exists |= sprite.id == "A";
221
222                    sprites.push(sprite);
223                } else {
224                    warnings.push(result.unwrap_err());
225                }
226            } else if segment.starts_with("ITM ") {
227                let result = Item::from_str(&segment);
228
229                if let Ok(item) = result {
230                    items.push(item);
231                } else {
232                    warnings.push(result.unwrap_err());
233                }
234            } else if segment.starts_with("DLG ") {
235                let result = Dialogue::from_str(&segment);
236
237                if let Ok(dialogue) = result {
238                    dialogues.push(dialogue);
239                } else {
240                    warnings.push(result.unwrap_err());
241                }
242            } else if segment.starts_with("END ") {
243                let result = Ending::from_str(&segment);
244
245                if let Ok(ending) = result {
246                    endings.push(ending);
247                } else {
248                    warnings.push(result.unwrap_err());
249                }
250            } else if segment.starts_with("VAR ") {
251                variables.push(Variable::from(segment));
252            } else if segment.starts_with("FONT ") {
253                font_data = Some(segment);
254            }
255        }
256
257        if ! avatar_exists {
258            warnings.push(crate::Error::Game { missing: NotFound::Avatar });
259        }
260
261        Ok(
262            (
263                Game {
264                    name,
265                    version,
266                    room_format,
267                    room_type,
268                    font,
269                    custom_font,
270                    text_direction,
271                    palettes,
272                    rooms,
273                    tiles,
274                    sprites,
275                    items,
276                    dialogues,
277                    endings,
278                    variables,
279                    font_data,
280                    line_endings_crlf,
281                },
282                warnings
283            )
284        )
285    }
286
287    /// todo refactor this into "get T by ID", taking a Vec<T> and an ID name?
288    pub fn get_sprite_by_id(&self, id: String) -> Result<&Sprite, crate::error::NotFound> {
289        let index = self.sprites.iter().position(
290            |sprite| sprite.id == id
291        );
292
293        match index {
294            Some(index) => Ok(&self.sprites[index]),
295            None => Err(crate::error::NotFound::Sprite),
296        }
297    }
298
299    pub fn get_tile_by_id(&self, id: String) -> Result<&Tile, crate::error::NotFound> {
300        let index = self.tiles.iter().position(
301            |tile| tile.id == id
302        );
303
304        match index {
305            Some(index) => Ok(&self.tiles[index]),
306            None => Err(crate::error::NotFound::Tile),
307        }
308    }
309
310    pub fn get_room_by_id(&self, id: String) -> Result<&Room, crate::error::NotFound> {
311        let index = self.rooms.iter().position(
312            |room| room.id == id
313        );
314
315        match index {
316            Some(index) => Ok(&self.rooms[index]),
317            None => Err(crate::error::NotFound::Room),
318        }
319    }
320
321    pub fn get_avatar(&self) -> Result<&Sprite, crate::error::NotFound> {
322        self.get_sprite_by_id("A".to_string())
323    }
324
325    // todo result
326    pub fn get_tiles_by_ids(&self, ids: Vec<String>) -> Vec<&Tile> {
327        let mut tiles: Vec<&Tile> = Vec::new();
328
329        for id in ids {
330            if let Ok(tile) = self.get_tile_by_id(id) {
331                tiles.push(tile);
332            }
333        }
334
335        tiles
336    }
337
338    pub fn get_tiles_for_room(&self, id: String) -> Result<Vec<&Tile>, crate::error::NotFound> {
339        let room = self.get_room_by_id(id)?;
340        let mut tile_ids = room.tiles.clone();
341        tile_ids.sort();
342        tile_ids.dedup();
343        // remove 0 as this isn't a real tile
344        let zero_index  = tile_ids.iter()
345            .position(|i| i == "0");
346        if let Some(zero_index) = zero_index {
347            tile_ids.remove(zero_index);
348        }
349        // remove Ok once this function returns a result
350        Ok(self.get_tiles_by_ids(tile_ids))
351    }
352
353    // return? array of changes made? error/ok?
354    pub fn merge(&mut self, game: &Game) {
355        // ignore title, version, room format, room type, font, text direction
356
357        let mut palette_id_changes:  HashMap<String, String> = HashMap::new();
358        let mut tile_id_changes:     HashMap<String, String> = HashMap::new();
359        let mut dialogue_id_changes: HashMap<String, String> = HashMap::new();
360        let mut ending_id_changes:   HashMap<String, String> = HashMap::new();
361        let mut item_id_changes:     HashMap<String, String> = HashMap::new();
362        let mut room_id_changes:     HashMap<String, String> = HashMap::new();
363        let mut sprite_id_changes:   HashMap<String, String> = HashMap::new();
364
365        fn insert_if_different(map: &mut HashMap<String, String>, old: String, new: String) {
366            if old != new && ! map.contains_key(&old) {
367                map.insert(old, new);
368            }
369        }
370
371        // alternatively - instead of handling these types in a specific order,
372        // we could calculate the new IDs for each type first,
373        // then handle the sections one by one
374
375        // a room has a palette, so handle palettes before rooms
376        for palette in &game.palettes {
377            insert_if_different(
378                palette_id_changes.borrow_mut(),
379                palette.id.clone(),
380                self.add_palette(palette.clone())
381            );
382        }
383
384        // a room has tiles, so handle before room
385        for tile in &game.tiles {
386            insert_if_different(
387                tile_id_changes.borrow_mut(),
388                tile.id.clone(),
389                self.add_tile(tile.clone())
390            );
391        }
392
393        for variable in &game.variables {
394            // don't change ID - just avoid duplicates
395            if ! self.variable_ids().contains(&variable.id) {
396                self.add_variable(variable.clone());
397            }
398        }
399
400        for item in &game.items {
401            let old_id = item.id.clone();
402            let new_id = try_id(&self.item_ids(), &item.id);
403            insert_if_different(item_id_changes.borrow_mut(), old_id, new_id)
404        }
405
406        // a sprite has a dialogue, so handle before sprites
407        // dialogue can have variables, so handle before after variables
408        for dialogue in &game.dialogues {
409            let mut dialogue = dialogue.clone();
410
411            for (old, new) in &item_id_changes {
412                // todo is there a better way of doing this?
413                dialogue.contents = dialogue.contents.replace(
414                    &format!("item \"{}\"", old),
415                    &format!("item \"{}\"", new)
416                );
417            }
418
419            let old_id = dialogue.id.clone();
420            let new_id = self.add_dialogue(dialogue);
421            insert_if_different(dialogue_id_changes.borrow_mut(), old_id, new_id);
422        }
423
424        // an ending lives in a room, so handle endings before rooms
425        for ending in &game.endings {
426            insert_if_different(
427                ending_id_changes.borrow_mut(),
428                ending.id.clone(),
429                self.add_ending(ending.clone())
430            );
431        }
432
433        // an item has a dialogue ID, so we need to handle these after dialogues
434        // an item instance lives in a room so these must be handled before rooms
435        for item in &game.items {
436            let mut item = item.clone();
437
438            if item_id_changes.contains_key(&item.id) {
439                item.id = item_id_changes[&item.id].clone();
440            }
441
442            if let Some(key) = item.dialogue_id.clone() {
443                if let Some(change) = dialogue_id_changes.get(&key) {
444                    item.dialogue_id = Some(change.clone());
445                }
446            }
447
448            self.add_item(item);
449        }
450
451        // calculate all of the new room IDs first
452        // to insert any new room, we need to know the new IDs of every room
453        // to maintain the integrity of exits and endings
454
455        let mut all_room_ids = self.room_ids();
456
457        for room in &game.rooms {
458            let old = room.id.clone();
459            let new = try_id(&all_room_ids, &room.id);
460            insert_if_different(room_id_changes.borrow_mut(), old, new.clone());
461            all_room_ids.push(new);
462        }
463
464        // needs to be handled after palettes, tiles, items, exits, endings
465        // and before sprites
466        for room in &game.rooms {
467            let mut room = room.clone();
468
469            if let Some(room_id_change) = room_id_changes.get(&room.id) {
470                room.id = room_id_change.clone();
471            }
472
473            if let Some(key) = room.palette_id.clone() {
474                if let Some(change) = palette_id_changes.get(&key) {
475                    room.palette_id = Some(change.clone());
476                }
477            }
478
479            room.change_tile_ids(&tile_id_changes);
480
481            room.items = room.items.iter().map(|instance|
482                if item_id_changes.contains_key(&instance.id) {
483                    Instance {
484                        position: instance.position.clone(),
485                        id: item_id_changes[&instance.id].clone()
486                    }
487                } else {
488                    instance.clone()
489                }
490            ).collect();
491
492            room.exits = room.exits.iter().map(|exit| {
493                let mut exit = exit.clone();
494
495                let key = exit.exit.room_id.clone();
496
497                if let Some(change) = room_id_changes.get(&key) {
498                    exit.exit.room_id = change.clone();
499                }
500
501                if let Some(key) = exit.dialogue_id.clone() {
502                    if let Some(dialogue_change) = dialogue_id_changes.get(&key) {
503                        exit.dialogue_id = Some(dialogue_change.clone());
504                    }
505                }
506
507                exit
508            }).collect();
509
510            room.endings = room.endings.iter().map(|ending| {
511                let mut ending = ending.clone();
512                let key = ending.id.clone();
513
514                if let Some(change) = ending_id_changes.get(&key) {
515                    ending.id = change.clone();
516                }
517
518                ending
519            }).collect();
520
521            self.add_room(room);
522        }
523
524        // a sprite has a dialogue ID, so we need to handle these after dialogues
525        // a sprite has a position in a room, so we need to handle these after the rooms
526        for sprite in &game.sprites {
527            let mut sprite = sprite.clone();
528            // avoid having two avatars
529            if sprite.id == "A" {
530                sprite.id = "0".to_string(); // just a default value for replacement
531            }
532
533            if let Some(key) = sprite.dialogue_id.clone() {
534                if dialogue_id_changes.contains_key(&key) {
535                    sprite.dialogue_id = Some(dialogue_id_changes[&key].clone());
536                }
537            }
538
539            if let Some(key) = sprite.room_id.clone() {
540                if let Some(change) = room_id_changes.get(&key) {
541                    sprite.room_id = Some(change.clone());
542                }
543            }
544
545            let old_id = sprite.id.clone();
546            let new_id = self.add_sprite(sprite);
547            insert_if_different(sprite_id_changes.borrow_mut(), old_id, new_id);
548        }
549    }
550}
551
552impl ToString for Game {
553    fn to_string(&self) -> String {
554        let mut segments: Vec<String> = Vec::new();
555
556        // todo refactor
557
558        for palette in &self.palettes {
559            segments.push(palette.to_string());
560        }
561
562        for room in &self.rooms {
563            segments.push(room.to_string(self.room_format(), self.room_type));
564        }
565
566        for tile in &self.tiles {
567            segments.push(tile.to_string());
568        }
569
570        for sprite in &self.sprites {
571            segments.push(sprite.to_string());
572        }
573
574        for item in &self.items {
575            segments.push(item.to_string());
576        }
577
578        for dialogue in &self.dialogues {
579            // this replacement is silly but see segments_from_string() for explanation
580            segments.push(dialogue.to_string().replace("\"\"\"\n\"\"\"", ""));
581        }
582
583        for ending in &self.endings {
584            segments.push(ending.to_string());
585        }
586
587        for variable in &self.variables {
588            segments.push(variable.to_string());
589        }
590
591        if self.font_data.is_some() {
592            segments.push(self.font_data.to_owned().unwrap())
593        }
594
595        transform_line_endings(
596            format!(
597                "{}{}{}{}{}\n\n{}\n\n",
598                &self.name,
599                &self.version_line(),
600                &self.room_format_line(),
601                &self.font_line(),
602                &self.text_direction_line(),
603                segments.join("\n\n"),
604            ),
605            if self.line_endings_crlf {TransformMode::CRLF} else {TransformMode::LF}
606        )
607    }
608}
609
610impl Game {
611    // todo dedupe
612    pub fn palette_ids(&self) -> Vec<String> {
613        self.palettes.iter().map(|palette| palette.id.clone()).collect()
614    }
615
616    pub fn tile_ids(&self) -> Vec<String> {
617        self.tiles.iter().map(|tile| tile.id.clone()).collect()
618    }
619
620    pub fn sprite_ids(&self) -> Vec<String> {
621        self.sprites.iter().map(|sprite| sprite.id.clone()).collect()
622    }
623    pub fn room_ids(&self) -> Vec<String> {
624        self.rooms.iter().map(|room| room.id.clone()).collect()
625    }
626
627    pub fn item_ids(&self) -> Vec<String> {
628        self.items.iter().map(|item| item.id.clone()).collect()
629    }
630
631    pub fn dialogue_ids(&self) -> Vec<String> {
632        self.dialogues.iter().map(|dialogue| dialogue.id.clone()).collect()
633    }
634
635    pub fn ending_ids(&self) -> Vec<String> {
636        self.endings.iter().map(|ending| ending.id.clone()).collect()
637    }
638
639    pub fn variable_ids(&self) -> Vec<String> {
640        self.variables.iter().map(|variable| variable.id.clone()).collect()
641    }
642
643    // todo dedupe?
644
645    pub fn new_palette_id(&self) -> String {
646        new_unique_id(self.palette_ids())
647    }
648
649    /// first available tile ID.
650    /// e.g. if current tile IDs are [0, 2, 3] the result will be `1`
651    ///      if current tile IDs are [0, 1, 2] the result will be `3`
652    pub fn new_tile_id(&self) -> String {
653        let mut ids = self.tile_ids();
654        // don't allow 0 - this is a reserved ID for an implicit background tile
655        ids.push("0".to_string());
656        new_unique_id(ids)
657    }
658
659    pub fn new_sprite_id(&self) -> String {
660        new_unique_id(self.sprite_ids())
661    }
662
663    pub fn new_room_id(&self) -> String {
664        new_unique_id(self.room_ids())
665    }
666
667    pub fn new_item_id(&self) -> String {
668        new_unique_id(self.item_ids())
669    }
670
671    pub fn new_dialogue_id(&self) -> String {
672        new_unique_id(self.dialogue_ids())
673    }
674
675    pub fn new_ending_id(&self) -> String {
676        new_unique_id(self.ending_ids())
677    }
678
679    pub fn new_variable_id(&self) -> String {
680        new_unique_id(self.variable_ids())
681    }
682
683    pub fn get_palette(&self, id: &str) -> Option<&Palette> {
684        self.palettes.iter().find(|palette| palette.id == id)
685    }
686
687    /// todo refactor?
688    pub fn get_tile_id(&self, matching_tile: &Tile) -> Option<String> {
689        for tile in &self.tiles {
690            if tile == matching_tile {
691                return Some(tile.id.clone());
692            }
693        }
694
695        None
696    }
697
698    pub fn find_tile_with_animation(&self, animation: &[Image]) -> Option<&Tile> {
699        self.tiles.iter().find(|&tile| tile.animation_frames.as_slice() == animation)
700    }
701
702    /// adds a palette safely and returns the ID
703    pub fn add_palette(&mut self, mut palette: Palette) -> String {
704        let new_id = try_id(&self.palette_ids(), &palette.id);
705        if new_id != palette.id {
706            palette.id = new_id.clone();
707        }
708        self.palettes.push(palette);
709        new_id
710    }
711
712    /// adds a tile safely and returns the ID
713    pub fn add_tile(&mut self, mut tile: Tile) -> String {
714        if tile.id == "0" || self.tile_ids().contains(&tile.id) {
715            let new_id = self.new_tile_id();
716            if new_id != tile.id {
717                tile.id = new_id;
718            }
719        }
720
721        let id = tile.id.clone();
722        self.tiles.push(tile);
723        id
724    }
725
726    /// adds a sprite safely and returns the ID
727    pub fn add_sprite(&mut self, mut sprite: Sprite) -> String {
728        let new_id = try_id(&self.sprite_ids(), &sprite.id);
729        if new_id != sprite.id {
730            sprite.id = new_id.clone();
731        }
732        self.sprites.push(sprite);
733        new_id
734    }
735
736    /// adds an item safely and returns the ID
737    pub fn add_item(&mut self, mut item: Item) -> String {
738        let new_id = try_id(&self.item_ids(), &item.id);
739        if new_id != item.id {
740            item.id = new_id.clone();
741        }
742        self.items.push(item);
743        new_id
744    }
745
746    /// adds a dialogue safely and returns the ID
747    pub fn add_dialogue(&mut self, mut dialogue: Dialogue) -> String {
748        let new_id = try_id(&self.dialogue_ids(), &dialogue.id);
749        if new_id != dialogue.id {
750            dialogue.id = new_id.clone();
751        }
752        self.dialogues.push(dialogue);
753        new_id
754    }
755
756    /// adds an ending safely and returns the ID
757    pub fn add_ending(&mut self, mut ending: Ending) -> String {
758        let new_id = try_id(&self.ending_ids(), &ending.id);
759        if new_id != ending.id {
760            ending.id = new_id.clone();
761        }
762        self.endings.push(ending);
763        new_id
764    }
765
766    /// Safely adds a room and returns the room ID (a new ID will be generated if clashing)
767    /// You will need to be mindful that the room's palette, tile, exit and ending IDs
768    /// will be valid after adding.
769    pub fn add_room(&mut self, mut room: Room) -> String {
770        let new_id = try_id(&self.room_ids(), &room.id);
771        if new_id != room.id {
772            room.id = new_id.clone();
773        }
774        self.rooms.push(room);
775        new_id
776    }
777
778    pub fn add_variable(&mut self, mut variable: Variable) -> String {
779        let new_id = try_id(&self.variable_ids(), &variable.id);
780        if new_id != variable.id {
781            variable.id = new_id.clone();
782        }
783        new_id
784    }
785
786    /// todo I think I need a generic `dedupe(&mut self, Vec<T>)` function
787    /// it would have to take a closure for comparing a given T (see the background_tile below)
788    /// and a closure for what to do with the changed IDs
789    pub fn dedupe_tiles(&mut self) {
790        let mut tiles_temp = self.tiles.clone();
791        let mut unique_tiles: Vec<Tile> = Vec::new();
792        let mut tile_id_changes: HashMap<String, String> = HashMap::new();
793
794        while !tiles_temp.is_empty() {
795            let tile = tiles_temp.pop().unwrap();
796
797            if tile == crate::mock::tile_background() {
798                tile_id_changes.insert(tile.id, "0".to_string());
799            } else if tiles_temp.contains(&tile) {
800                tile_id_changes.insert(
801                    tile.id.clone(),
802                    self.get_tile_id(&tile).unwrap()
803                );
804            } else {
805                unique_tiles.push(tile);
806            }
807        }
808
809        for room in &mut self.rooms {
810            room.change_tile_ids(&tile_id_changes);
811        }
812
813        unique_tiles.reverse();
814
815        self.tiles = unique_tiles;
816    }
817
818    fn version_line(&self) -> String {
819        if self.version.is_some() {
820            format!(
821                "\n\n# BITSY VERSION {}.{}",
822                self.version.as_ref().unwrap().major, self.version.as_ref().unwrap().minor
823            )
824        } else {
825            "".to_string()
826        }
827    }
828
829    fn room_format_line(&self) -> String {
830        if self.room_format.is_some() {
831            format!("\n\n! ROOM_FORMAT {}", self.room_format.unwrap().to_string())
832        } else {
833            "".to_string()
834        }
835    }
836
837    fn font_line(&self) -> String {
838        match self.font {
839            Font::AsciiSmall => "".to_string(),
840            Font::Custom     => format!("\n\nDEFAULT_FONT {}", self.custom_font.as_ref().unwrap()),
841            _                => format!("\n\nDEFAULT_FONT {}", self.font.to_string().unwrap()),
842        }
843    }
844
845    fn text_direction_line(&self) -> &str {
846        match self.text_direction {
847            TextDirection::RightToLeft => "\n\nTEXT_DIRECTION RTL",
848            _ => "",
849        }
850    }
851
852    /// older bitsy games do not specify a version, but we can infer 1.0
853    pub fn version(&self) -> Version {
854        self.version.unwrap_or(Version { major: 1, minor: 0 })
855    }
856
857    /// older bitsy games do not specify a room format, but we can infer 0
858    pub fn room_format(&self) -> RoomFormat {
859        self.room_format.unwrap_or(RoomFormat::Contiguous)
860    }
861}
862
863#[cfg(test)]
864mod test {
865    use crate::{TextDirection, Font, Version, Game, Tile, Image, Palette, Colour};
866
867    #[test]
868    fn game_from_string() {
869        let (output, _) = Game::from(include_str!["test-resources/default.bitsy"].to_string()).unwrap();
870        let expected = crate::mock::game_default();
871
872        assert_eq!(output, expected);
873    }
874
875    #[test]
876    fn game_to_string() {
877        let output = crate::mock::game_default().to_string();
878        let expected = include_str!["test-resources/default.bitsy"].to_string();
879        assert_eq!(output, expected);
880    }
881
882    #[test]
883    fn tile_ids() {
884        assert_eq!(crate::mock::game_default().tile_ids(), vec!["a".to_string()]);
885    }
886
887    #[test]
888    fn new_tile_id() {
889        // default tile has an id of 10 ("a"), and 0 is reserved
890        assert_eq!(crate::mock::game_default().new_tile_id(), "1".to_string());
891
892        // for a game with a gap in the tile IDs, check the gap is used
893
894        let mut game = crate::mock::game_default();
895        let mut tiles: Vec<Tile> = Vec::new();
896
897        // 0 is reserved; upper bound is non-inclusive
898        for n in 1..10 {
899            if n != 4 {
900                let mut new_tile = crate::mock::tile_default();
901                new_tile.id = format!("{}", n).to_string();
902                tiles.push(new_tile);
903            }
904        }
905
906        game.tiles = tiles;
907
908        assert_eq!(game.new_tile_id(), "4".to_string());
909
910        // fill in the space created above, then test that tile IDs get sorted
911
912        let mut new_tile = crate::mock::tile_default();
913        new_tile.id = "4".to_string();
914        game.tiles.push(new_tile);
915
916        assert_eq!(game.new_tile_id(), "a".to_string());
917    }
918
919    #[test]
920    fn add_tile() {
921        let mut game = crate::mock::game_default();
922        let new_id = game.add_tile(crate::mock::tile_default());
923        assert_eq!(new_id, "1".to_string());
924        assert_eq!(game.tiles.len(), 2);
925        let new_id = game.add_tile(crate::mock::tile_default());
926        assert_eq!(new_id, "2".to_string());
927        assert_eq!(game.tiles.len(), 3);
928    }
929
930    #[test]
931    fn arabic() {
932        let (game, _) = Game::from(include_str!("test-resources/arabic.bitsy").to_string()).unwrap();
933
934        assert_eq!(game.font, Font::Arabic);
935        assert_eq!(game.text_direction, TextDirection::RightToLeft);
936    }
937
938    #[test]
939    fn version_formatting() {
940        let mut game = crate::mock::game_default();
941        game.version = Some(Version { major: 5, minor: 0 });
942        assert!(game.to_string().contains("# BITSY VERSION 5.0"))
943    }
944
945    #[test]
946    fn get_tiles_for_room() {
947        assert_eq!(
948            crate::mock::game_default().get_tiles_for_room("0".to_string()).unwrap(),
949            vec![&crate::mock::tile_default()]
950        )
951    }
952
953    #[test]
954    fn add_item() {
955        let mut game = crate::mock::game_default();
956        game.add_item(crate::mock::item());
957        game.add_item(crate::mock::item());
958
959        let expected = vec![
960            "0".to_string(), "1".to_string(), "6".to_string(), "2".to_string()
961        ];
962
963        assert_eq!(game.item_ids(), expected);
964    }
965
966    #[test]
967    fn merge() {
968        // try merging two default games
969        let mut game = crate::mock::game_default();
970        game.merge(&crate::mock::game_default());
971
972        assert_eq!(game.room_ids(), vec!["0".to_string(), "1".to_string()]);
973        assert_eq!(game.tile_ids(), vec!["a".to_string(), "1".to_string()]); // 0 is reserved
974        // duplicate avatar (SPR A) gets converted into a normal sprite
975        assert_eq!(
976            game.sprite_ids(),
977            vec!["A".to_string(), "a".to_string(), "0".to_string(), "1".to_string()]
978        );
979        assert_eq!(
980            game.item_ids(),
981            vec!["0".to_string(), "1".to_string(), "2".to_string(), "3".to_string()]
982        );
983        assert_eq!(
984            game.dialogue_ids(),
985            vec![
986                "0".to_string(),
987                "1".to_string(),
988                "2".to_string(),
989                "3".to_string(),
990                "4".to_string(),
991                "5".to_string()
992            ]
993        );
994        assert_eq!(game.palette_ids(), vec!["0".to_string(), "1".to_string()]);
995        assert_eq!(
996            game.get_room_by_id("1".to_string()).unwrap().palette_id,
997            Some("1".to_string())
998        );
999
1000        // test sprites in non-zero rooms in merged game
1001        let mut game_a = crate::mock::game_default();
1002        let mut game_b = crate::mock::game_default();
1003        let mut room = crate::mock::room();
1004        let mut sprite = crate::mock::sprite();
1005        let room_id = "2".to_string();
1006        room.id = room_id.clone();
1007        sprite.room_id = Some(room_id.clone());
1008        game_b.add_sprite(sprite);
1009        game_a.merge(&game_b);
1010        assert_eq!(game_a.get_sprite_by_id("2".to_string()).unwrap().room_id, Some(room_id));
1011    }
1012
1013    #[test]
1014    fn dedupe_tiles() {
1015        let mut game = crate::mock::game_default();
1016        game.add_tile(crate::mock::tile_default());
1017        game.add_tile(crate::mock::tile_default());
1018        game.add_tile(crate::mock::tile_background());
1019        game.dedupe_tiles();
1020        assert_eq!(game.tiles, vec![crate::mock::tile_default()]);
1021
1022        let tile_a = Tile {
1023            id: "0".to_string(),
1024            name: Some("apple".to_string()),
1025            wall: Some(true),
1026            animation_frames: vec![Image {
1027                pixels: vec![
1028                    0,1,1,0,1,1,0,1,
1029                    0,1,1,0,1,1,0,1,
1030                    1,0,1,0,1,0,0,1,
1031                    1,0,1,0,1,0,0,1,
1032                    0,0,0,0,1,1,1,1,
1033                    0,0,0,0,1,1,1,1,
1034                    1,1,0,1,1,0,1,1,
1035                    1,1,0,1,1,0,1,1,
1036                ]
1037            }],
1038            colour_id: Some(1)
1039        };
1040
1041        let tile_b = Tile {
1042            id: "1".to_string(),
1043            name: Some("frogspawn".to_string()),
1044            wall: Some(false),
1045            animation_frames: vec![Image {
1046                pixels: vec![
1047                    1,0,1,0,1,0,0,1,
1048                    0,1,1,0,1,1,0,1,
1049                    0,1,1,0,1,1,0,1,
1050                    1,1,0,1,1,0,1,1,
1051                    1,0,1,0,1,0,0,1,
1052                    0,0,0,0,1,1,1,1,
1053                    0,0,0,0,1,1,1,1,
1054                    1,1,0,1,1,0,1,1,
1055                ]
1056            }],
1057            colour_id: None
1058        };
1059
1060        game.add_tile(tile_a.clone());
1061        game.add_tile(tile_b.clone());
1062        game.add_tile(tile_a.clone());
1063        game.add_tile(tile_b.clone());
1064
1065        game.dedupe_tiles();
1066
1067        assert_eq!(game.tiles, vec![crate::mock::tile_default(), tile_a, tile_b]);
1068    }
1069
1070    #[test]
1071    fn find_tile_with_animation() {
1072        let game = crate::mock::game_default();
1073        let animation = vec![Image { pixels: vec![
1074            1, 1, 1, 1, 1, 1, 1, 1,
1075            1, 0, 0, 0, 0, 0, 0, 1,
1076            1, 0, 0, 0, 0, 0, 0, 1,
1077            1, 0, 0, 1, 1, 0, 0, 1,
1078            1, 0, 0, 1, 1, 0, 0, 1,
1079            1, 0, 0, 0, 0, 0, 0, 1,
1080            1, 0, 0, 0, 0, 0, 0, 1,
1081            1, 1, 1, 1, 1, 1, 1, 1,
1082        ]}];
1083        let output = game.find_tile_with_animation(&animation);
1084        let expected = Some(&game.tiles[0]);
1085        assert_eq!(output, expected);
1086    }
1087
1088    #[test]
1089    fn empty_game_data_throws_error() {
1090        assert_eq!(Game::from("".to_string()        ).unwrap_err(), crate::error::NotFound::Anything);
1091        assert_eq!(Game::from(" \n \r\n".to_string()).unwrap_err(), crate::error::NotFound::Anything);
1092    }
1093
1094    #[test]
1095    fn get_palette() {
1096        let mut game = crate::mock::game_default();
1097        let new_palette = Palette {
1098            id: "1".to_string(),
1099            name: Some("sadness".to_string()),
1100            colours: vec![
1101                Colour { red: 133, green: 131, blue: 111 },
1102                Colour { red: 105, green: 93,  blue: 104 },
1103                Colour { red: 62,  green: 74,  blue: 76  },
1104            ]
1105        };
1106        game.add_palette(new_palette.clone());
1107        assert_eq!(game.get_palette("0").unwrap(), &crate::mock::game_default().palettes[0]);
1108        assert_eq!(game.get_palette("1").unwrap(), &new_palette);
1109        assert_eq!(game.get_palette("2"), None);
1110    }
1111}