skeleton of merge functionality
This commit is contained in:
parent
b08e514b12
commit
5f736cc653
|
@ -38,10 +38,16 @@ some more practical uses would be things like:
|
||||||
## todo
|
## todo
|
||||||
|
|
||||||
* implement Result return types on ::from functions so that we can handle errors
|
* 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
|
* 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?
|
* implement PartialEq for tiles etc. for the sake of checking for duplicate tiles?
|
||||||
* dedupe functions for tiles, sprites, etc.
|
* 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
|
### tidy up
|
||||||
|
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
250
src/game.rs
250
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 loe::TransformMode;
|
||||||
use std::str::FromStr;
|
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 -
|
/// 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.
|
/// 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))
|
Ok(self.get_tiles_by_ids(tile_ids))
|
||||||
}
|
}
|
||||||
|
|
||||||
// return? array of changes made? status?
|
// return? array of changes made? error/ok?
|
||||||
pub fn merge(game: Game) {
|
pub fn merge(&mut self, game: Game) {
|
||||||
// ignore title, version, room format, room type, font, text direction
|
// ignore title, version, room format, room type, font, text direction
|
||||||
|
|
||||||
// maybe we need hashmaps of old->new IDs, for each type of ID?
|
// 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
|
// we need functions for "add sprite" etc. that return the newly generated valid ID
|
||||||
|
|
||||||
// for each new palette:
|
let mut palette_id_changes: HashMap<String, String> = HashMap::new();
|
||||||
// check if a matching palette exists
|
let mut tile_id_changes: HashMap<String, String> = HashMap::new();
|
||||||
// if yes, replace any room palette IDs with the extant palette ID
|
let mut dialogue_id_changes: HashMap<String, String> = HashMap::new();
|
||||||
// check if palette ID clashes with an existing palette ID
|
let mut ending_id_changes: HashMap<String, String> = HashMap::new();
|
||||||
// check if palette name clashes and if so, give a new name ("[name] 2" or something)
|
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:
|
fn insert_if_different(map: &mut HashMap<String, String>, old: String, new: String) {
|
||||||
// check if a matching tile exists
|
if old != new {
|
||||||
// if yes, replace room tile IDs with that of the extant tile
|
map.insert(old, new);
|
||||||
// if no, give a new unique tile ID and change room tile IDs to this
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
// a sprite has a dialogue, so handle before sprites
|
||||||
// give room a new unique ID
|
// dialogue can have variables, so handle before after variables
|
||||||
// check room name - if duplicate, append "2" or something?
|
for dialogue in game.dialogues {
|
||||||
// convert tile/item IDs to new ones
|
let mut dialogue = dialogue.clone();
|
||||||
// add room to game
|
|
||||||
|
|
||||||
// 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
|
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]
|
#[inline]
|
||||||
fn version_line(&self) -> String {
|
fn version_line(&self) -> String {
|
||||||
if self.version.is_some() {
|
if self.version.is_some() {
|
||||||
|
@ -714,4 +896,24 @@ mod test {
|
||||||
game.add_item(crate::mock::item());
|
game.add_item(crate::mock::item());
|
||||||
assert_eq!(game.item_ids(), vec!["0".to_string(), "6".to_string(), "1".to_string()]);
|
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()]
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
18
src/lib.rs
18
src/lib.rs
|
@ -37,11 +37,12 @@ use std::fmt::Display;
|
||||||
use text::{Font, TextDirection};
|
use text::{Font, TextDirection};
|
||||||
use tile::Tile;
|
use tile::Tile;
|
||||||
use variable::Variable;
|
use variable::Variable;
|
||||||
|
use std::num::ParseIntError;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct Instance {
|
pub struct Instance {
|
||||||
position: Position,
|
position: Position,
|
||||||
id: String, // item / ending.rs id
|
id: String, // item / ending id
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
@ -75,8 +76,8 @@ impl AnimationFrames for Vec<Image> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn from_base36(str: &str) -> u64 {
|
fn from_base36(str: &str) -> Result<u64, ParseIntError> {
|
||||||
u64::from_str_radix(str, 36).expect(&format!("Invalid base36 string: {}", str))
|
u64::from_str_radix(str, 36)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// this doesn't work inside ToBase36 for some reason
|
/// 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;
|
let mut new_id: u64 = 0;
|
||||||
|
|
||||||
for id in ids {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
new_id += 1;
|
new_id += 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return to_base36(new_id);
|
return to_base36(new_id);
|
||||||
}
|
}
|
||||||
|
@ -209,9 +213,9 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_from_base36() {
|
fn test_from_base36() {
|
||||||
assert_eq!(from_base36("0"), 0);
|
assert_eq!(from_base36("0").unwrap(), 0);
|
||||||
assert_eq!(from_base36("0z"), 35);
|
assert_eq!(from_base36("0z").unwrap(), 35);
|
||||||
assert_eq!(from_base36("11"), 37);
|
assert_eq!(from_base36("11").unwrap(), 37);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
Loading…
Reference in New Issue