Skip to content

Commit

Permalink
Initial Implementation of temp:// Asset Source
Browse files Browse the repository at this point in the history
  • Loading branch information
bushrat011899 committed May 20, 2024
1 parent 2aed777 commit 6ead01d
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 0 deletions.
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1413,6 +1413,17 @@ description = "How to configure the texture to repeat instead of the default cla
category = "Assets"
wasm = true

[[example]]
name = "temp_asset"
path = "examples/asset/temp_asset.rs"
doc-scrape-examples = true

[package.metadata.example.temp_asset]
name = "Temporary assets"
description = "How to use the temporary asset source"
category = "Assets"
wasm = false

# Async Tasks
[[example]]
name = "async_compute"
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_asset/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ js-sys = "0.3"

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
notify-debouncer-full = { version = "0.3.1", optional = true }
tempfile = "3.10.1"

[dev-dependencies]
bevy_core = { path = "../bevy_core", version = "0.14.0-dev" }
Expand Down
24 changes: 24 additions & 0 deletions crates/bevy_asset/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ mod path;
mod reflect;
mod server;

#[cfg(not(target_arch = "wasm32"))]
mod temp;

pub use assets::*;
pub use bevy_asset_macros::Asset;
pub use direct_access_ext::DirectAssetAccessExt;
Expand Down Expand Up @@ -91,6 +94,9 @@ pub struct AssetPlugin {
pub mode: AssetMode,
/// How/If asset meta files should be checked.
pub meta_check: AssetMetaCheck,
/// The path to use for temporary assets (relative to the project root).
/// If not provided, a platform specific folder will be created and deleted upon exit.
pub temporary_file_path: Option<String>,
}

#[derive(Debug)]
Expand Down Expand Up @@ -138,6 +144,7 @@ impl Default for AssetPlugin {
processed_file_path: Self::DEFAULT_PROCESSED_FILE_PATH.to_string(),
watch_for_changes_override: None,
meta_check: AssetMetaCheck::default(),
temporary_file_path: None,
}
}
}
Expand All @@ -163,6 +170,23 @@ impl Plugin for AssetPlugin {
);
embedded.register_source(&mut sources);
}

#[cfg(not(target_arch = "wasm32"))]
{
match temp::get_temp_source(app.world_mut(), self.temporary_file_path.clone()) {
Ok(source) => {
let mut sources = app
.world_mut()
.get_resource_or_insert_with::<AssetSourceBuilders>(Default::default);

sources.insert("temp", source);
}
Err(error) => {
error!("Could not setup temp:// AssetSource due to an IO Error: {error}");
}
};
}

{
let mut watch = cfg!(feature = "watch");
if let Some(watch_override) = self.watch_for_changes_override {
Expand Down
56 changes: 56 additions & 0 deletions crates/bevy_asset/src/temp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use std::{
io::{Error, ErrorKind},
path::{Path, PathBuf},
};

use bevy_ecs::{system::Resource, world::World};

use crate::io::AssetSourceBuilder;

/// Private resource to store the temporary directory used by `temp://`.
/// Kept private as it should only be removed on application exit.
#[derive(Resource)]
enum TempDirectory {
/// Uses [`TempDir`](tempfile::TempDir)'s drop behaviour to delete the directory.
/// Note that this is not _guaranteed_ to succeed, so it is possible to leak files from this
/// option until the underlying OS cleans temporary directories. For secure files, consider using
/// [`tempfile`](tempfile::tempfile) directly.
Delete(tempfile::TempDir),
/// Will not delete the temporary directory on exit, leaving cleanup the responsibility of
/// the user or their system.
Persist(PathBuf),
}

impl TempDirectory {
fn path(&self) -> &Path {
match self {
TempDirectory::Delete(x) => x.path(),
TempDirectory::Persist(x) => x.as_ref(),
}
}
}

pub(crate) fn get_temp_source(
world: &mut World,
temporary_file_path: Option<String>,
) -> std::io::Result<AssetSourceBuilder> {
let temp_dir = match world.remove_resource::<TempDirectory>() {
Some(resource) => resource,
None => match temporary_file_path {
Some(path) => TempDirectory::Persist(path.into()),
None => TempDirectory::Delete(tempfile::tempdir()?),
},
};

let path = temp_dir
.path()
.as_os_str()
.try_into()
.map_err(|error| Error::new(ErrorKind::InvalidData, error))?;

let source = AssetSourceBuilder::platform_default(path, None);

world.insert_resource(temp_dir);

Ok(source)
}
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ Example | Description
[Extra asset source](../examples/asset/extra_source.rs) | Load an asset from a non-standard asset source
[Hot Reloading of Assets](../examples/asset/hot_asset_reloading.rs) | Demonstrates automatic reloading of assets when modified on disk
[Repeated texture configuration](../examples/asset/repeated_texture.rs) | How to configure the texture to repeat instead of the default clamp to edges
[Temporary assets](../examples/asset/temp_asset.rs) | How to use the temporary asset source

## Async Tasks

Expand Down
183 changes: 183 additions & 0 deletions examples/asset/temp_asset.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
//! This example shows how to use the temporary asset source, `temp://`.
//! First, a [`TextAsset`] is created in-memory, then saved into the temporary asset source.
//! Once the save operation is completed, we load the asset just like any other file, and display its contents!

use bevy::{
asset::{
saver::{AssetSaver, ErasedAssetSaver},
AssetPath, ErasedLoadedAsset, LoadedAsset,
},
prelude::*,
tasks::{block_on, IoTaskPool, Task},
};

use futures_lite::future;
use text_asset::{TextAsset, TextLoader, TextSaver};

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.init_asset::<TextAsset>()
.register_asset_loader(TextLoader)
.add_systems(Startup, (save_temp_asset, setup_ui))
.add_systems(Update, (wait_until_temp_saved, display_text))
.run();
}

/// Attempt to save an asset to the temporary asset source.
fn save_temp_asset(assets: Res<AssetServer>, mut commands: Commands) {
// This is the asset we will attempt to save.
let my_text_asset = TextAsset("Hello World!".to_owned());

// To ensure the `Task` can outlive this function, we must provide owned versions
// of the `AssetServer` and our desired path.
let path = AssetPath::from("temp://message.txt").into_owned();
let server = assets.clone();

let task = IoTaskPool::get().spawn(async move {
save_asset(my_text_asset, path, server, TextSaver)
.await
.unwrap();
});

// To ensure the task completes before we try loading, we will manually poll this task
// so we can react to its completion.
commands.spawn(SavingTask(task));
}

/// Poll the save tasks until completion, and then start loading our temporary text asset.
fn wait_until_temp_saved(
assets: Res<AssetServer>,
mut tasks: Query<(Entity, &mut SavingTask)>,
mut commands: Commands,
) {
for (entity, mut task) in tasks.iter_mut() {
if let Some(()) = block_on(future::poll_once(&mut task.0)) {
commands.insert_resource(MyTempText {
text: assets.load("temp://message.txt"),
});

commands.entity(entity).despawn_recursive();
}
}
}

/// Setup a basic UI to display our [`TextAsset`] once it's loaded.
fn setup_ui(mut commands: Commands) {
commands.spawn(Camera2dBundle::default());

commands.spawn((TextBundle::from_section("Loading...", default())
.with_text_justify(JustifyText::Center)
.with_style(Style {
position_type: PositionType::Absolute,
bottom: Val::Percent(50.),
right: Val::Percent(50.),
..default()
}),));
}

/// Once the [`TextAsset`] is loaded, update our display text to its contents.
fn display_text(
mut query: Query<&mut Text>,
my_text: Option<Res<MyTempText>>,
texts: Res<Assets<TextAsset>>,
) {
let message = my_text
.as_ref()
.and_then(|resource| texts.get(&resource.text))
.map(|text| text.0.as_str())
.unwrap_or("Loading...");

for mut text in query.iter_mut() {
*text = Text::from_section(message, default());
}
}

/// Save an [`Asset`] at the provided path. Returns [`None`] on failure.
async fn save_asset<A: Asset>(
asset: A,
path: AssetPath<'_>,
server: AssetServer,
saver: impl AssetSaver<Asset = A> + ErasedAssetSaver,
) -> Option<()> {
let asset = ErasedLoadedAsset::from(LoadedAsset::from(asset));
let source = server.get_source(path.source()).ok()?;
let writer = source.writer().ok()?;

let mut writer = writer.write(path.path()).await.ok()?;
ErasedAssetSaver::save(&saver, &mut writer, &asset, &())
.await
.ok()?;

Some(())
}

#[derive(Component)]
struct SavingTask(Task<()>);

#[derive(Resource)]
struct MyTempText {
text: Handle<TextAsset>,
}

mod text_asset {
//! Putting the implementation of an asset loader and writer for a text asset in this module to avoid clutter.
//! While this is required for this example to function, it isn't the focus.

use bevy::{
asset::{
io::{Reader, Writer},
saver::{AssetSaver, SavedAsset},
AssetLoader, LoadContext,
},
prelude::*,
};
use futures_lite::{AsyncReadExt, AsyncWriteExt};

#[derive(Asset, TypePath, Debug)]
pub struct TextAsset(pub String);

#[derive(Default)]
pub struct TextLoader;

impl AssetLoader for TextLoader {
type Asset = TextAsset;
type Settings = ();
type Error = std::io::Error;
async fn load<'a>(
&'a self,
reader: &'a mut Reader<'_>,
_settings: &'a Self::Settings,
_load_context: &'a mut LoadContext<'_>,
) -> Result<TextAsset, Self::Error> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let value = String::from_utf8(bytes).unwrap();
Ok(TextAsset(value))
}

fn extensions(&self) -> &[&str] {
&["txt"]
}
}

#[derive(Default)]
pub struct TextSaver;

impl AssetSaver for TextSaver {
type Asset = TextAsset;
type Settings = ();
type OutputLoader = TextLoader;
type Error = std::io::Error;

async fn save<'a>(
&'a self,
writer: &'a mut Writer,
asset: SavedAsset<'a, Self::Asset>,
_settings: &'a Self::Settings,
) -> Result<(), Self::Error> {
writer.write_all(asset.0.as_bytes()).await?;
Ok(())
}
}
}

0 comments on commit 6ead01d

Please sign in to comment.