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
|
||||
|
||||
* 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
|
||||
|
||||
|
|
|
@ -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 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()]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
24
src/lib.rs
24
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<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,11 +172,14 @@ 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) {
|
||||
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]
|
||||
|
|
Loading…
Reference in New Issue