Skip to content

Commit

Permalink
feat: implement initial persisted storage API.
Browse files Browse the repository at this point in the history
There is a lot that should be improved on, but it is
usable for now.
  • Loading branch information
zicklag committed Sep 29, 2023
1 parent b64eb2c commit fc5f692
Show file tree
Hide file tree
Showing 12 changed files with 373 additions and 24 deletions.
59 changes: 55 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions demos/features/assets/locales/en-US/menu.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ sprite-demo = Sprite Demo
atlas-demo = Atlas Demo
tilemap-demo = Tilemap Demo
path2d-demo = Path2D Demo
save = Save
persisted-text-box-content = Persisted text box content
28 changes: 23 additions & 5 deletions demos/features/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,13 @@ struct TileMeta {
idx: u32,
}

#[derive(HasSchema, Default, Clone)]
#[repr(C)]
struct PersistedTextData(String);

fn main() {
assert!(Color::schema()
.type_data
.get::<SchemaDeserialize>()
.is_some());
// Register persistent data's schema so that it can be loaded by the storage loader.
PersistedTextData::schema();

// Create a bones bevy renderer from our bones game
BonesBevyRenderer::new(create_game())
Expand Down Expand Up @@ -141,6 +143,7 @@ fn menu_system(
ctx: Egui,
mut sessions: ResMut<Sessions>,
mut session_options: ResMut<SessionOptions>,
mut storage: ResMut<Storage>,
// Get the localization field from our `GameMeta`
localization: Localization<GameMeta>,
) {
Expand Down Expand Up @@ -208,6 +211,21 @@ fn menu_system(

ui.add_space(20.0);

ui.vertical_centered(|ui| {
ui.set_width(300.0);
{
let data = storage.get_or_insert_default_mut::<PersistedTextData>();
egui::TextEdit::singleline(&mut data.0)
.hint_text(localization.get("persisted-text-box-content"))
.show(ui);
}
if ui.button(localization.get("save")).clicked() {
storage.save()
}
});

ui.add_space(10.0);

// We can use the `widget()` method on the `Egui` to conveniently run bones
// systems that can modify the `egui::Ui` and return an `egui::Response`.
//
Expand Down Expand Up @@ -447,5 +465,5 @@ fn demo_widget(
ui.label("Demo Widget");
// When using a bones image in egui, we have to get it's corresponding egui texture
// from the egui textures resource.
ui.image(egui_textures.get(meta.menu_image), [100., 100.])
ui.image(egui_textures.get(meta.menu_image), [50., 50.])
}
6 changes: 6 additions & 0 deletions framework_crates/bones_bevy_renderer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,14 @@ bones_framework = { version = "0.3", path = "../bones_framework" }
bevy_egui = "0.21"
glam = { version = "0.24", features = ["serde"] }
bevy_prototype_lyon = "0.9"
serde_yaml = "0.9"
serde = "1.0.188"
anyhow = "1.0"

[dependencies.bevy]
default-features = false
features = ["bevy_render", "bevy_core_pipeline", "bevy_sprite", "x11", "bevy_gilrs"]
version = "0.11"

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
directories = "5.0"
142 changes: 141 additions & 1 deletion framework_crates/bones_bevy_renderer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

use std::path::PathBuf;

use anyhow::Context;

Check failure on line 10 in framework_crates/bones_bevy_renderer/src/lib.rs

View workflow job for this annotation

GitHub Actions / 🔧 Clippy correctness checks (wasm32-unknown-unknown, web-target)

unused import: `anyhow::Context`
pub use bevy;

use bevy::{
Expand All @@ -24,8 +25,9 @@ use bevy_egui::EguiContext;
use glam::*;

use bevy_prototype_lyon::prelude as lyon;
use bones_framework::prelude as bones;
use bones_framework::prelude::{self as bones, SchemaBox};
use prelude::convert::{IntoBevy, IntoBones};
use serde::{de::Visitor, Deserialize, Serialize};

/// The prelude
pub mod prelude {
Expand All @@ -47,6 +49,11 @@ pub struct BonesBevyRenderer {
pub game: bones::Game,
/// The version of the game, used for the asset loader.
pub game_version: bones::Version,
/// The (qualifier, organization, application) that will be used to pick a persistent storage
/// location for the game.
///
/// For example: `("org", "fishfolk", "jumpy")`
pub app_namespace: (String, String, String),
/// The path to load assets from.
pub asset_dir: PathBuf,
/// The path to load asset packs from.
Expand Down Expand Up @@ -178,6 +185,7 @@ impl BonesBevyRenderer {
pixel_art: true,
game,
game_version: bones::Version::new(0, 1, 0),
app_namespace: ("org".into(), "fishfolk".into(), "bones_demo_game".into()),
asset_dir: PathBuf::from("assets"),
packs_dir: PathBuf::from("packs"),
}
Expand Down Expand Up @@ -236,6 +244,15 @@ impl BonesBevyRenderer {
);
}

// Configure and load the persitent storage
let mut storage = bones::Storage::with_backend(Box::new(StorageBackend::new(

Check failure on line 248 in framework_crates/bones_bevy_renderer/src/lib.rs

View workflow job for this annotation

GitHub Actions / 🔧 Clippy correctness checks (wasm32-unknown-unknown, web-target)

the trait bound `StorageBackend: bones_framework::prelude::StorageApi` is not satisfied
&self.app_namespace.0,
&self.app_namespace.1,
&self.app_namespace.2,
)));
storage.load();
self.game.insert_shared_resource(storage);

self.game.insert_shared_resource(bones_egui_textures);
app.insert_resource(bones_image_ids);

Expand Down Expand Up @@ -280,6 +297,129 @@ impl BonesBevyRenderer {
}
}

struct StorageBackend {
storage_path: PathBuf,
}
impl StorageBackend {
fn new(qualifier: &str, organization: &str, application: &str) -> Self {
let project_dirs = directories::ProjectDirs::from(qualifier, organization, application)

Check failure on line 305 in framework_crates/bones_bevy_renderer/src/lib.rs

View workflow job for this annotation

GitHub Actions / 🔧 Clippy correctness checks (wasm32-unknown-unknown, web-target)

failed to resolve: use of undeclared crate or module `directories`
.expect("Identify system data dir path");
Self {
storage_path: project_dirs.data_dir().join("storage.yml"),
}
}
}
#[cfg(not(target_arch = "wasm32"))]
impl bones::StorageApi for StorageBackend {
fn save(&mut self, data: Vec<bones::SchemaBox>) {
let file = std::fs::OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(&self.storage_path)
.expect("Failed to open storage file");
let mut serializer = serde_yaml::Serializer::new(file);
LoadedStorage(data)
.serialize(&mut serializer)
.expect("Failed to serialize to storage file.");
}

fn load(&mut self) -> Vec<bones::SchemaBox> {
if self.storage_path.exists() {
let result: anyhow::Result<LoadedStorage> = (|| {
let file = std::fs::OpenOptions::new()
.read(true)
.open(&self.storage_path)
.context("Failed to open storage file")?;
let loaded: LoadedStorage =
serde_yaml::from_reader(file).context("Failed to deserialize storage file")?;

anyhow::Result::Ok(loaded)
})();
match result {
Ok(loaded) => loaded.0,
Err(e) => {
error!(
"Error deserializing storage file, ignoring file, \
data will be overwritten when saved: {e:?}"
);
default()
}
}
} else {
std::fs::create_dir_all(self.storage_path.parent().unwrap()).unwrap();
default()
}
}
}
struct LoadedStorage(Vec<SchemaBox>);
impl Serialize for LoadedStorage {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let data: HashMap<String, bones::SchemaRef> = self
.0
.iter()
.map(|x| (x.schema().full_name.to_string(), x.as_ref()))
.collect();

use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(Some(data.len()))?;

for (key, value) in data {
map.serialize_key(&key)?;
map.serialize_value(&bones::SchemaSerializer(value))?;
}

map.end()
}
}
impl<'de> Deserialize<'de> for LoadedStorage {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_map(LoadedStorageVisitor).map(Self)
}
}
struct LoadedStorageVisitor;
impl<'de> Visitor<'de> for LoadedStorageVisitor {
type Value = Vec<SchemaBox>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "Mapping of string type names to type data.")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut data = Vec::new();
while let Some(type_name) = map.next_key::<String>()? {
let reg = bones::SCHEMA_REGISTRY.borrow();
let Some(schema) = reg
.schemas
.iter()
.map(|(_id, schema)| schema)
.find(|schema| schema.full_name.as_ref() == type_name)
else {
error!(
"\n\nCannot find schema registration for `{}` while loading persisted \
storage. This means you that you need to call \
`{}::schema()` to register your persisted storage type before \
creating the `BonesBevyRenderer` or that there is data from an old \
version of the app inside of the persistent storage file.\n\n",
type_name, type_name,
);
continue;
};

data.push(map.next_value_seed(bones::SchemaDeserializer(schema))?);
}

Ok(data)
}
}

/// Startup system to load egui fonts and textures.
fn setup_egui(world: &mut World) {
world.resource_scope(|world: &mut World, mut bones_data: Mut<BonesData>| {
Expand Down
Loading

0 comments on commit fc5f692

Please sign in to comment.