Skip to content

Commit 16e9bde

Browse files
authored
Switch serialization format to json (#58)
* switch to serde_json serialization and keep backward compat * put backward compat code in a separate module * test backward compat * update changelog
1 parent 8da6036 commit 16e9bde

6 files changed

Lines changed: 124 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Changelog
22

33
## Unreleased
4+
### Changed
5+
* Game data is now serialized to JSON to allow extending it without breaking backwards compatibility.
6+
47
## [0.4.0](https://github.com/facundoolano/rpg-cli/releases/tag/0.4.0) - 2021-06-05
58
### Added
69
* This Changelog

Cargo.lock

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ clap = "3.0.0-beta.2"
1616
typetag = "0.1"
1717
dunce = "1.0.1"
1818
once_cell = "1.7.2"
19+
serde_json = "1.0.64"

src/character/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::randomizer::{random, Randomizer};
99
use class::Class;
1010

1111
#[derive(Serialize, Deserialize, Debug)]
12+
#[serde(default)]
1213
pub struct Character {
1314
#[serde(skip, default = "default_class")]
1415
class: &'static Class,
@@ -25,6 +26,12 @@ pub struct Character {
2526
pub speed: i32,
2627
}
2728

29+
impl Default for Character {
30+
fn default() -> Self {
31+
Character::player()
32+
}
33+
}
34+
2835
// Always attach the static hero class to deserialized characters
2936
fn default_class() -> &'static Class {
3037
&Class::HERO

src/game/game040.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use super::tombstone::Tombstone;
2+
use crate::character::Character;
3+
use crate::item::Item;
4+
use crate::location::Location;
5+
use serde::{Deserialize, Serialize};
6+
use std::collections::HashMap;
7+
8+
/// v0.4.0 of the game struct, kept for backwards compatibility when upgrading
9+
/// the game data file to the new version
10+
/// FIXME this will be removed on a subsequent version
11+
#[derive(Serialize, Deserialize)]
12+
struct Game040 {
13+
pub player: Character,
14+
pub location: Location,
15+
pub gold: i32,
16+
inventory: HashMap<String, Vec<Box<dyn Item>>>,
17+
tombstones: HashMap<Location, Tombstone>,
18+
}
19+
20+
/// Get a new Game instance out of a v0.4.0 one
21+
pub fn deserialize(data: &[u8]) -> Result<super::Game, bincode::Error> {
22+
let mut v4game: Game040 = bincode::deserialize(&data)?;
23+
let mut new_game = super::Game::new();
24+
std::mem::swap(&mut new_game.player, &mut v4game.player);
25+
std::mem::swap(&mut new_game.location, &mut v4game.location);
26+
std::mem::swap(&mut new_game.inventory, &mut v4game.inventory);
27+
new_game.tombstones = v4game
28+
.tombstones
29+
.drain()
30+
.map(|(l, t)| (l.to_string(), t))
31+
.collect();
32+
new_game.gold = v4game.gold;
33+
Ok(new_game)
34+
}
35+
36+
#[cfg(test)]
37+
mod tests {
38+
use super::*;
39+
use crate::item;
40+
use item::equipment::Equipment;
41+
42+
#[test]
43+
fn test_backwards_compatibility() {
44+
// build a game040 with everything
45+
let mut game_v4 = Game040 {
46+
location: Location::home(),
47+
player: Character::player(),
48+
gold: 10,
49+
inventory: HashMap::new(),
50+
tombstones: HashMap::new(),
51+
};
52+
game_v4.player.sword = Some(item::equipment::Sword::new(1));
53+
game_v4.player.shield = Some(item::equipment::Shield::new(1));
54+
game_v4
55+
.inventory
56+
.insert("potion".to_string(), vec![Box::new(item::Potion::new(1))]);
57+
58+
let mut tombstone_game = super::super::Game::new();
59+
tombstone_game.add_item("potion", Box::new(item::Potion::new(1)));
60+
game_v4
61+
.tombstones
62+
.insert(Location::home(), Tombstone::drop(&mut tombstone_game));
63+
64+
let data = bincode::serialize(&game_v4).unwrap();
65+
let mut new_game = deserialize(&data).unwrap();
66+
67+
assert_eq!(10, new_game.gold);
68+
assert!(new_game.location.is_home());
69+
assert!(new_game.player.sword.is_some());
70+
assert!(new_game.player.shield.is_some());
71+
assert_eq!(1 as usize, *new_game.inventory().get("potion").unwrap());
72+
// pick up tombstone @ home
73+
new_game.visit_home();
74+
assert_eq!(2 as usize, *new_game.inventory().get("potion").unwrap());
75+
}
76+
}

src/game/mod.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use std::{fs, io, path};
1212
use tombstone::Tombstone;
1313

1414
pub mod battle;
15+
mod game040;
1516
pub mod tombstone;
1617

1718
#[derive(Debug)]
@@ -22,12 +23,13 @@ pub enum Error {
2223
}
2324

2425
#[derive(Serialize, Deserialize)]
26+
#[serde(default)]
2527
pub struct Game {
2628
pub player: Character,
2729
pub location: Location,
2830
pub gold: i32,
2931
inventory: HashMap<String, Vec<Box<dyn Item>>>,
30-
tombstones: HashMap<Location, Tombstone>,
32+
tombstones: HashMap<String, Tombstone>,
3133
}
3234

3335
impl Game {
@@ -43,7 +45,13 @@ impl Game {
4345

4446
pub fn load() -> Result<Self, Error> {
4547
let data = fs::read(data_file()).or(Err(Error::NoDataFile))?;
46-
let game: Game = bincode::deserialize(&data).unwrap();
48+
let game: Game = if let Ok(game) = serde_json::from_slice(&data) {
49+
game
50+
} else {
51+
// if json deserialization fails, attempt bincode assuming
52+
// it may be a file from v0.4.0
53+
game040::deserialize(&data).unwrap()
54+
};
4755
Ok(game)
4856
}
4957

@@ -53,7 +61,7 @@ impl Game {
5361
fs::create_dir(&rpg_dir).unwrap();
5462
}
5563

56-
let data = bincode::serialize(&self).unwrap();
64+
let data = serde_json::to_vec(&self).unwrap();
5765
fs::write(data_file(), &data)
5866
}
5967

@@ -134,7 +142,7 @@ impl Game {
134142

135143
/// If there's a tombstone laying in the current location, pick up its items
136144
fn pick_up_tombstone(&mut self) -> bool {
137-
if let Some(mut tombstone) = self.tombstones.remove(&self.location) {
145+
if let Some(mut tombstone) = self.tombstones.remove(&self.location.to_string()) {
138146
tombstone.pick_up(self);
139147
true
140148
} else {
@@ -205,7 +213,7 @@ impl Game {
205213
} else {
206214
// leave hero items in the location
207215
let tombstone = Tombstone::drop(self);
208-
self.tombstones.insert(self.location.clone(), tombstone);
216+
self.tombstones.insert(self.location.to_string(), tombstone);
209217

210218
log::battle_lost(&self.player);
211219
Err(Error::GameOver)

0 commit comments

Comments
 (0)