Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement initial persisted storage API. #205

Merged
merged 1 commit into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
147 changes: 146 additions & 1 deletion framework_crates/bones_bevy_renderer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,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 +48,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 +184,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 +243,18 @@ impl BonesBevyRenderer {
);
}

// Configure and load the persitent storage
#[cfg(not(target_arch = "wasm32"))]
{
let mut storage = bones::Storage::with_backend(Box::new(StorageBackend::new(
&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 +299,132 @@ impl BonesBevyRenderer {
}
}

#[cfg(not(target_arch = "wasm32"))]
struct StorageBackend {
storage_path: PathBuf,
}
#[cfg(not(target_arch = "wasm32"))]
impl StorageBackend {
fn new(qualifier: &str, organization: &str, application: &str) -> Self {
let project_dirs = directories::ProjectDirs::from(qualifier, organization, application)
.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> {
use anyhow::Context;
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
Loading