From 5f736cc65335e4362c2892c388d8d1a058c08d0e Mon Sep 17 00:00:00 2001 From: Max Bradbury Date: Sat, 27 Jun 2020 19:28:17 +0100 Subject: [PATCH] skeleton of merge functionality --- README.md | 8 +- src/bin/bitsy-merge.rs | 26 +++++ src/game.rs | 250 +++++++++++++++++++++++++++++++++++++---- src/lib.rs | 24 ++-- 4 files changed, 273 insertions(+), 35 deletions(-) create mode 100644 src/bin/bitsy-merge.rs diff --git a/README.md b/README.md index 4db68a0..53f44ee 100644 --- a/README.md +++ b/README.md @@ -38,10 +38,16 @@ some more practical uses would be things like: ## todo * implement Result return types on ::from functions so that we can handle errors -* replace Image with Vec or something. seems like a pointless abstraction +* replace Image with Vec or something. seems like a pointless abstraction. * replace game avatar with helper functions to get and set the sprite with an ID of A * implement PartialEq for tiles etc. for the sake of checking for duplicate tiles? * dedupe functions for tiles, sprites, etc. +* tests for merge function +* merge function places merged avatar in room 0 instead of the merged room 0 with a new ID. +* actually - any sprites from the merged game are in game A's rooms +* merged rooms have the wrong palette IDs and tile IDs +* exits in merged rooms do not work - the on-screen position is correct, but the room ID is wrong +* add update function (i.e. migrate an old game version to the current one) - would this work? ### tidy up diff --git a/src/bin/bitsy-merge.rs b/src/bin/bitsy-merge.rs new file mode 100644 index 0000000..e7832f2 --- /dev/null +++ b/src/bin/bitsy-merge.rs @@ -0,0 +1,26 @@ +extern crate bitsy_parser; +use bitsy_parser::game::Game; +use std::{env, fs}; + +fn main() { + let game_a = env::args() + .nth(1) + .expect("No main game specified. Usage: `bitsy-merge main.bitsy additional.bitsy output.bitsy`"); + + // todo allow numerous additional games + let game_b = env::args() + .nth(2) + .expect("No additional game specified. Usage: `bitsy-merge main.bitsy additional.bitsy output.bitsy`"); + + let outfile = env::args() + .nth(3) + .expect("No output file specified. Usage: `bitsy-merge main.bitsy additional.bitsy output.bitsy`"); + + let mut game_a = Game::from(fs::read_to_string(game_a).unwrap()).unwrap(); + let game_b = Game::from(fs::read_to_string(game_b).unwrap()).unwrap(); + + game_a.merge(game_b); + + fs::write(outfile, game_a.to_string()) + .expect("Failed to write output file"); +} diff --git a/src/game.rs b/src/game.rs index 44607e7..50e16ba 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,6 +1,8 @@ -use crate::{Dialogue, Ending, Font, Item, Palette, Room, Sprite, TextDirection, Tile, Variable, transform_line_endings, segments_from_string, new_unique_id, try_id}; +use crate::{Dialogue, Ending, Font, Item, Palette, Room, Sprite, TextDirection, Tile, Variable, transform_line_endings, segments_from_string, new_unique_id, try_id, Instance}; use loe::TransformMode; use std::str::FromStr; +use std::collections::HashMap; +use std::borrow::BorrowMut; /// in very early versions of Bitsy, room tiles were defined as single alphanumeric characters - /// so there was a maximum of 36 unique tiles. later versions are comma-separated. @@ -328,42 +330,193 @@ impl Game { Ok(self.get_tiles_by_ids(tile_ids)) } - // return? array of changes made? status? - pub fn merge(game: Game) { + // return? array of changes made? error/ok? + pub fn merge(&mut self, game: Game) { // ignore title, version, room format, room type, font, text direction // maybe we need hashmaps of old->new IDs, for each type of ID? // we need functions for "add sprite" etc. that return the newly generated valid ID - // for each new palette: - // check if a matching palette exists - // if yes, replace any room palette IDs with the extant palette ID - // check if palette ID clashes with an existing palette ID - // check if palette name clashes and if so, give a new name ("[name] 2" or something) + let mut palette_id_changes: HashMap = HashMap::new(); + let mut tile_id_changes: HashMap = HashMap::new(); + let mut dialogue_id_changes: HashMap = HashMap::new(); + let mut ending_id_changes: HashMap = HashMap::new(); + let mut item_id_changes: HashMap = HashMap::new(); + let mut room_id_changes: HashMap = HashMap::new(); + let mut sprite_id_changes: HashMap = HashMap::new(); - // for each tile: - // check if a matching tile exists - // if yes, replace room tile IDs with that of the extant tile - // if no, give a new unique tile ID and change room tile IDs to this + fn insert_if_different(map: &mut HashMap, old: String, new: String) { + if old != new { + map.insert(old, new); + } + } - // ignore avatar (maybe convert to normal sprite instead?) + // alternatively - instead of handling these types in a specific order, + // we could calculate the new IDs for each type first, + // then handle the sections one by one - // for each item + // a room has a palette, so handle palettes before rooms + for palette in game.palettes { + let new_id = self.add_palette(palette.clone()); + insert_if_different(palette_id_changes.borrow_mut(), palette.id.clone(),new_id); + } - // for each dialogue item + // a room has tiles, so handle before room + for tile in game.tiles { + let new_id = self.add_tile(tile.clone()); + insert_if_different(tile_id_changes.borrow_mut(), tile.id.clone(), new_id); + } - // for each ending + for variable in game.variables { + // don't change ID - just avoid duplicates + if ! self.variable_ids().contains(&variable.id) { + self.add_variable(variable.clone()); + } + } - // for each variable + for item in &game.items { + let old_id = item.id.clone(); + let new_id = try_id(self.item_ids(), item.id.clone()); + insert_if_different(item_id_changes.borrow_mut(), old_id, new_id) + } - // for each room - // give room a new unique ID - // check room name - if duplicate, append "2" or something? - // convert tile/item IDs to new ones - // add room to game + // a sprite has a dialogue, so handle before sprites + // dialogue can have variables, so handle before after variables + for dialogue in game.dialogues { + let mut dialogue = dialogue.clone(); - // for each sprite - // + for (old, new) in &item_id_changes { + // todo is there a better way of doing this? + dialogue.contents = dialogue.contents.replace( + &format!("item \"{}\"", old), + &format!("item \"{}\"", new) + ); + } + + let old_id = dialogue.id.clone(); + let new_id = self.add_dialogue(dialogue); + insert_if_different(dialogue_id_changes.borrow_mut(), old_id, new_id); + } + + // an ending lives in a room, so handle endings before rooms + for ending in game.endings { + let new_id = self.add_ending(ending.clone()); + insert_if_different(ending_id_changes.borrow_mut(), ending.id.clone(), new_id); + } + + // an item has a dialogue ID, so we need to handle these after dialogues + // an item instance lives in a room so these must be handled before rooms + for item in &game.items { + let mut item = item.clone(); + + if item_id_changes.contains_key(&item.id) { + item.id = item_id_changes[&item.id].clone(); + } + + if item.dialogue_id.is_some() { + let key = item.dialogue_id.clone().unwrap(); + if dialogue_id_changes.contains_key(&key) { + item.dialogue_id = Some(dialogue_id_changes[&key].clone()); + } + } + + self.add_item(item); + } + + // calculate all of the new room IDs first + // to insert any new room, we need to know the new IDs of every room + // to maintain the integrity of exits and endings + for room in &game.rooms { + let new_id = try_id(self.room_ids(), room.id.clone()); + insert_if_different(room_id_changes.borrow_mut(), room.id.clone(), new_id); + } + + // needs to be handled after palettes, tiles, items, exits, endings + // and before sprites + for room in &game.rooms { + let mut room = room.clone(); + + if room_id_changes.contains_key(&room.id) { + room.id = room_id_changes[&room.id].clone(); + } + + if room.palette_id.is_some() { + let key = room.palette_id.clone().unwrap(); + if palette_id_changes.contains_key(&key) { + room.palette_id = Some(room_id_changes[&key].clone()); + } + } + + let mut tiles = Vec::new(); + for tile_id in &room.tiles { + tiles.push(if tile_id_changes.contains_key(tile_id) { + tile_id_changes.get(tile_id).unwrap() + } else { + tile_id + }.clone()); + } + room.tiles = tiles; + + room.items = room.items.iter().map(|instance| + if item_id_changes.contains_key(&instance.id) { + Instance { + position: instance.position.clone(), + id: item_id_changes[&instance.id].clone() + } + } else { + instance.clone() + } + ).collect(); + + room.exits = room.exits.iter().map(|exit| { + let mut exit = exit.clone(); + if room_id_changes.contains_key(&exit.exit.room_id) { + exit.exit.room_id = room_id_changes[&exit.exit.room_id].clone(); + } + exit + }).collect(); + + room.endings = room.endings.iter().map(|ending| { + let mut ending = ending.clone(); + if ending_id_changes.contains_key(&ending.id) { + ending.id = ending_id_changes[&ending.id].clone(); + } + ending + }).collect(); + + self.add_room(room.to_owned()); + } + + // a sprite has a dialogue ID, so we need to handle these after dialogues + // a sprite has a position in a room, so we need to handle these after the rooms + for sprite in game.sprites { + let mut sprite = sprite.clone(); + // avoid having two avatars + if sprite.id == "A".to_string() { + sprite.id = "0".to_string(); // just a default value for replacement + } + + if sprite.dialogue_id.is_some() { + let key = sprite.dialogue_id.clone().unwrap(); + if dialogue_id_changes.contains_key(&key) { + sprite.dialogue_id = Some(dialogue_id_changes[&key].clone()); + } + } + + if sprite.room_id.is_some() { + let key = sprite.room_id.clone().unwrap(); + if room_id_changes.contains_key(&key) { + sprite.room_id = Some(room_id_changes[&key].clone()); + } + } + + let old_id = sprite.id.clone(); + let new_id = self.add_sprite(sprite); + insert_if_different(sprite_id_changes.borrow_mut(), old_id, new_id); + } + + // does deduplication need to be its own function? + // should this function just add everything and we can dedupe later? } } @@ -559,6 +712,35 @@ impl Game { new_id } + pub fn add_ending(&mut self, mut ending: Ending) -> String { + let new_id = try_id(self.ending_ids(), ending.id.clone()); + if new_id != ending.id { + ending.id = new_id.clone(); + } + self.endings.push(ending); + new_id + } + + /// Safely adds a room and returns the room ID (a new ID will be generated if clashing) + /// You will need to be mindful that the room's palette, tile, exit and ending IDs + /// will be valid after adding. + pub fn add_room(&mut self, mut room: Room) -> String { + let new_id = try_id(self.room_ids(), room.id.clone()); + if new_id != room.id { + room.id = new_id.clone(); + } + self.rooms.push(room); + new_id + } + + pub fn add_variable(&mut self, mut variable: Variable) -> String { + let new_id = try_id(self.variable_ids(), variable.id.clone()); + if new_id != variable.id { + variable.id = new_id.clone(); + } + new_id + } + #[inline] fn version_line(&self) -> String { if self.version.is_some() { @@ -714,4 +896,24 @@ mod test { game.add_item(crate::mock::item()); assert_eq!(game.item_ids(), vec!["0".to_string(), "6".to_string(), "1".to_string()]); } + + #[test] + fn test_merge() { + // try merging two default games + let mut game = crate::mock::game_default(); + game.merge(crate::mock::game_default()); + + assert_eq!(game.room_ids(), vec!["0".to_string(), "1".to_string()]); + assert_eq!(game.tile_ids(), vec!["a".to_string(), "1".to_string()]); // 0 is reserved + // duplicate avatar (SPR A) gets converted into a normal sprite + assert_eq!( + game.sprite_ids(), + vec!["A".to_string(), "a".to_string(), "0".to_string(), "1".to_string()] + ); + assert_eq!(game.item_ids(), vec!["0".to_string(), "1".to_string()]); + assert_eq!( + game.dialogue_ids(), + vec!["SPR_0".to_string(), "ITM_0".to_string(), "0".to_string(), "1".to_string()] + ) + } } diff --git a/src/lib.rs b/src/lib.rs index 3964ef7..c35c86f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,11 +37,12 @@ use std::fmt::Display; use text::{Font, TextDirection}; use tile::Tile; use variable::Variable; +use std::num::ParseIntError; #[derive(Clone, Debug, Eq, PartialEq)] pub struct Instance { position: Position, - id: String, // item / ending.rs id + id: String, // item / ending id } #[derive(Clone, Debug, Eq, PartialEq)] @@ -75,8 +76,8 @@ impl AnimationFrames for Vec { } #[inline] -fn from_base36(str: &str) -> u64 { - u64::from_str_radix(str, 36).expect(&format!("Invalid base36 string: {}", str)) +fn from_base36(str: &str) -> Result { + u64::from_str_radix(str, 36) } /// this doesn't work inside ToBase36 for some reason @@ -171,11 +172,14 @@ fn new_unique_id(mut ids: Vec) -> String { let mut new_id: u64 = 0; for id in ids { - if new_id != from_base36(&id) { - break; - } + let id= from_base36(&id); + if id.is_ok() { + if new_id != id.unwrap() { + break; + } - new_id += 1; + new_id += 1; + } } return to_base36(new_id); @@ -209,9 +213,9 @@ mod test { #[test] fn test_from_base36() { - assert_eq!(from_base36("0"), 0); - assert_eq!(from_base36("0z"), 35); - assert_eq!(from_base36("11"), 37); + assert_eq!(from_base36("0").unwrap(), 0); + assert_eq!(from_base36("0z").unwrap(), 35); + assert_eq!(from_base36("11").unwrap(), 37); } #[test]