skeleton of merge functionality

This commit is contained in:
Max Bradbury 2020-06-27 19:28:17 +01:00
parent b08e514b12
commit 5f736cc653
4 changed files with 273 additions and 35 deletions

View File

@ -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<u8> or something. seems like a pointless abstraction
* replace Image with Vec<u8> 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

26
src/bin/bitsy-merge.rs Normal file
View File

@ -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");
}

View File

@ -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<String, String> = HashMap::new();
let mut tile_id_changes: HashMap<String, String> = HashMap::new();
let mut dialogue_id_changes: HashMap<String, String> = HashMap::new();
let mut ending_id_changes: HashMap<String, String> = HashMap::new();
let mut item_id_changes: HashMap<String, String> = HashMap::new();
let mut room_id_changes: HashMap<String, String> = HashMap::new();
let mut sprite_id_changes: HashMap<String, String> = 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<String, String>, 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()]
)
}
}

View File

@ -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<Image> {
}
#[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, ParseIntError> {
u64::from_str_radix(str, 36)
}
/// this doesn't work inside ToBase36 for some reason
@ -171,12 +172,15 @@ fn new_unique_id(mut ids: Vec<String>) -> String {
let mut new_id: u64 = 0;
for id in ids {
if new_id != from_base36(&id) {
let id= from_base36(&id);
if id.is_ok() {
if new_id != id.unwrap() {
break;
}
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]