Skip to content

Commit

Permalink
feat: embedded assets
Browse files Browse the repository at this point in the history
  • Loading branch information
eerii committed Jul 25, 2024
1 parent c76545b commit 3c4748a
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 11 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ name: release

on:
push:
branches: [main, dev]
branches: [main]
tags: [ "*[0-9]+.[0-9]+" ]
workflow_dispatch:
inputs:
Expand Down
20 changes: 20 additions & 0 deletions Cargo.lock

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

11 changes: 9 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ authors = ["Eri <[email protected]>"]
license = "Apache-2.0 OR MIT"
version = "0.14.1"
edition = "2021"
# exclude = ["assets", "wasm", ".data"]
exclude = ["assets", "wasm", ".data"]

[features]
# Feature sets
default = ["dev"]
dev = [
"common",
Expand All @@ -17,12 +18,18 @@ dev = [
"bevy/embedded_watcher",
"bevy/file_watcher",
]
release = ["common"]
release = ["common", "embedded"]
common = []
# Individual features
embedded = ["include_dir"]

[dependencies]
# Bevy and plugins
bevy = { version = "0.14", features = ["serialize", "wayland"] }
leafwing-input-manager = { git = "https://github.com/Leafwing-Studios/leafwing-input-manager" }

# Other dependencies
include_dir = { version = "0.7.4", optional = true }
log = { version = "*", features = [
"max_level_debug",
"release_max_level_warn",
Expand Down
9 changes: 8 additions & 1 deletion src/assets.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use bevy::{prelude::*, utils::HashMap};

#[cfg(feature = "embedded")]
pub(crate) mod embedded;
mod fonts;
mod meta;
mod music;
Expand All @@ -21,7 +23,7 @@ pub mod prelude {
}

/// Represent a handle to any asset type
pub trait AssetKey: Sized {
pub trait AssetKey: Sized + Eq + std::hash::Hash {
type Asset: Asset;
}

Expand All @@ -44,4 +46,9 @@ impl<K: AssetKey> AssetMap<K> {
self.values()
.all(|x| asset_server.is_loaded_with_dependencies(x))
}

/// Returns a weak clone of the asset handle
pub fn get(&self, key: &K) -> Handle<K::Asset> {
self[key].clone_weak()
}
}
168 changes: 168 additions & 0 deletions src/assets/embedded.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
//! Simplified version of https://github.com/vleue/bevy_embedded_assets

use std::{
io::Read,
path::{Path, PathBuf},
pin::Pin,
task::Poll,
};

use bevy::{
asset::io::{
AssetReader,
AssetReaderError,
AssetSource,
AssetSourceId,
ErasedAssetReader,
PathStream,
Reader,
},
prelude::*,
tasks::futures_lite::{AsyncRead, AsyncSeek, Stream},
utils::HashMap,
};
use include_dir::{include_dir, Dir};

const ASSET_DIR: Dir = include_dir!("assets");

pub(crate) fn plugin(app: &mut App) {
if app.is_plugin_added::<AssetPlugin>() {
error!("The embedded asset plugin must come before bevy's AssetPlugin");
}
app.register_asset_source(
AssetSourceId::Default,
AssetSource::build().with_reader(move || {
Box::new(EmbeddedAssetReader::new(AssetSource::get_default_reader(
ASSET_DIR.path().to_str().unwrap_or("assets").into(),
)))
}),
);
}

/// A wrapper around the raw bytes of an asset
#[derive(Default, Debug, Clone, Copy)]
pub struct DataReader(pub &'static [u8]);

impl AsyncRead for DataReader {
fn poll_read(
self: Pin<&mut Self>,
_: &mut std::task::Context<'_>,
buf: &mut [u8],
) -> Poll<std::io::Result<usize>> {
let read = self.get_mut().0.read(buf);
Poll::Ready(read)
}
}

impl AsyncSeek for DataReader {
fn poll_seek(
self: Pin<&mut Self>,
_: &mut std::task::Context<'_>,
_pos: std::io::SeekFrom,
) -> Poll<std::io::Result<u64>> {
Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Seek not supported",
)))
}
}

/// A wrapper around directories to read them
struct DirReader(Vec<PathBuf>);

impl Stream for DirReader {
type Item = PathBuf;

fn poll_next(
self: Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
Poll::Ready(this.0.pop())
}
}

/// A custom asset reader to search embedded assets first (and fall back to the
/// asset folder otherwise)
struct EmbeddedAssetReader {
loaded: HashMap<&'static Path, &'static [u8]>,
fallback: Box<dyn ErasedAssetReader>,
}

impl EmbeddedAssetReader {
fn new(
mut fallback: impl FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync + 'static,
) -> Self {
let mut reader = Self {
loaded: HashMap::default(),
fallback: fallback(),
};
// Preload all files in the asset directory
load_assets_rec(&ASSET_DIR, &mut reader);
reader
}
}

fn load_assets_rec(dir: &'static Dir, reader: &mut EmbeddedAssetReader) {
for file in dir.files() {
debug!("Embedding asset: '{}'", file.path().display());
reader.loaded.insert(file.path(), file.contents());
}
for dir in dir.dirs() {
load_assets_rec(dir, reader);
}
}

// Here we implement the `AssetReader` trait from bevy, which lets us switch the
// default reader for our own, automating the handling of the embedded://
// namespace and allowing us to use the same code regardless of where the method
impl AssetReader for EmbeddedAssetReader {
async fn read<'a>(&'a self, path: &'a Path) -> Result<Box<Reader<'a>>, AssetReaderError> {
if ASSET_DIR.contains(path) {
return self
.loaded
.get(path)
.map(|b| -> Box<Reader> { Box::new(DataReader(b)) })
.ok_or_else(|| AssetReaderError::NotFound(path.to_path_buf()));
}
warn!("Asset read failed for '{}', using fallback", path.display());
self.fallback.read(path).await
}

async fn read_meta<'a>(&'a self, path: &'a Path) -> Result<Box<Reader<'a>>, AssetReaderError> {
let meta_path = path.to_path_buf().with_added_extension(".meta");
if ASSET_DIR.contains(&meta_path) {
return self
.loaded
.get(&*meta_path)
.map(|b| -> Box<Reader> { Box::new(DataReader(b)) })
.ok_or_else(|| AssetReaderError::NotFound(path.to_path_buf()));
}
self.fallback.read(path).await
}

async fn read_directory<'a>(
&'a self,
path: &'a Path,
) -> Result<Box<PathStream>, AssetReaderError> {
if ASSET_DIR.contains(path) {
let paths: Vec<_> = self
.loaded
.keys()
.filter(|p| p.starts_with(path))
.map(|t| t.to_path_buf())
.collect();
return Ok(Box::new(DirReader(paths)));
}
warn!("Dir read failed for '{}', using fallback", path.display());
self.fallback.read_directory(path).await
}

async fn is_directory<'a>(&'a self, path: &'a Path) -> Result<bool, AssetReaderError> {
let base = path.join("");
Ok(self
.loaded
.keys()
.any(|p| p.starts_with(&base) && p != &path))
}
}
13 changes: 11 additions & 2 deletions src/components/camera.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use bevy::prelude::*;

use crate::base::prelude::*;
use crate::{
base::prelude::*,
prelude::{AssetMap, MetaAssetKey},
};

pub(super) fn plugin(app: &mut App) {
app.add_systems(OnEnter(GameState::Startup), init);
Expand All @@ -17,7 +20,13 @@ pub struct GameCamera;
pub struct FinalCamera;

/// Spawn the main cameras
fn init(mut cmd: Commands) {
fn init(mut cmd: Commands, meta_assets: Res<AssetMap<MetaAssetKey>>) {
let camera_bundle = Camera2dBundle::default();
cmd.spawn((camera_bundle, GameCamera, FinalCamera));

// Test logo, delete
cmd.spawn(SpriteBundle {
texture: meta_assets.get(&MetaAssetKey::BevyLogo),
..default()
});
}
2 changes: 1 addition & 1 deletion src/components/music.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub(super) fn plugin(app: &mut App) {
fn init(mut cmd: Commands, music_assets: Res<AssetMap<MusicAssetKey>>) {
cmd.spawn((
AudioBundle {
source: music_assets[&MusicAssetKey::Ambient].clone_weak(),
source: music_assets.get(&MusicAssetKey::Ambient),
settings: PlaybackSettings {
mode: PlaybackMode::Loop,
paused: true,
Expand Down
9 changes: 5 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#![feature(path_add_extension)]
#![allow(clippy::too_many_arguments)]
#![allow(clippy::type_complexity)]
// #![warn(missing_docs)]
Expand Down Expand Up @@ -33,10 +34,11 @@ pub struct GamePlugin;

impl Plugin for GamePlugin {
fn build(&self, app: &mut App) {
// Default bevy plugins
// The embedded plugin, if enabled, must come before bevy's `AssetPlugin`
#[cfg(feature = "embedded")]
app.add_plugins(assets::embedded::plugin);

// The window plugin specifies the main window properties like its size,
// if it is resizable and its title
// Default bevy plugins
let window_plugin = WindowPlugin {
primary_window: Some(Window {
title: "Hello Bevy".into(),
Expand All @@ -48,7 +50,6 @@ impl Plugin for GamePlugin {
}),
..default()
};

app.add_plugins(DefaultPlugins.set(window_plugin));

// Game plugins
Expand Down

0 comments on commit 3c4748a

Please sign in to comment.