Asset Protocol
Asset protocols are units that decode bytes and then does some things with decoded data, usually put components into asset, schedule dependencies to load and relate them with source asset, if there are any dependencies.
Asset protocols are second step in asset progression - they are decoupled from asset fetch engines to make them not care about source of the asset and by that be used no matter where assets come from, which solves the need for asset loaders to implement with all possible asset sources, which is cumbersome to deal with.
use keket::{
database::{path::AssetPath, AssetDatabase},
fetch::file::FileAssetFetch,
protocol::{bundle::BundleAssetProtocol, bytes::BytesAssetProtocol, text::TextAssetProtocol},
};
use serde::Deserialize;
use serde_json::Value;
use std::{error::Error, fs::Metadata, path::PathBuf};
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Person {
age: u8,
home: PersonHome,
friends: Vec<String>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct PersonHome {
country: String,
address: String,
}
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
// Asset protocols tell how to deserialize bytes into assets.
.with_protocol(TextAssetProtocol)
.with_protocol(BytesAssetProtocol)
// Bundle protocol allows for easly making a protocol that takes
// bytes and returns bundle with optional dependencies.
.with_protocol(BundleAssetProtocol::new("json", |bytes: Vec<u8>| {
let asset = serde_json::from_slice::<Value>(&bytes)?;
Ok((asset,).into())
}))
.with_protocol(BundleAssetProtocol::new("person", |bytes: Vec<u8>| {
let asset = serde_json::from_slice::<Person>(&bytes)?;
Ok((asset,).into())
}))
// Asset fetch tells how to get bytes from specific source.
.with_fetch(FileAssetFetch::default().with_root("resources"));
// Ensure method either gives existing asset handle or creates new
// and loads asset if not existing yet in storage.
let lorem = database.ensure("text://lorem.txt")?;
// Accessing component(s) of asset entry.
// Assets can store multiple data associated to them, consider them meta data.
println!("Lorem Ipsum: {}", lorem.access::<&String>(&database));
let json = database.ensure("json://person.json")?;
println!("JSON: {:#}", json.access::<&Value>(&database));
let person = database.ensure("person://person.json")?;
println!("Person: {:#?}", person.access::<&Person>(&database));
let trash = database.ensure("bytes://trash.bin")?;
println!("Bytes: {:?}", trash.access::<&Vec<u8>>(&database));
// We can query storage for asset components to process assets, just like with ECS.
for (asset_path, file_path, metadata) in database
.storage
.query::<true, (&AssetPath, &PathBuf, &Metadata)>()
{
println!(
"Asset: `{}` at location: {:?} has metadata: {:#?}",
asset_path, file_path, metadata
);
}
Ok(())
}
For type to be considered asset protocol, it must implement AssetProtocol
trait
that has methods for processing asset bytes:
use keket::{
database::{path::AssetPathStatic, AssetDatabase},
fetch::file::FileAssetFetch,
protocol::bundle::{
BundleAssetProtocol, BundleWithDependencies, BundleWithDependenciesProcessor,
},
};
use serde::Deserialize;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
// Register custom asset processor.
.with_protocol(BundleAssetProtocol::new("custom", CustomAssetProcessor))
.with_fetch(FileAssetFetch::default().with_root("resources"));
// Ensure custom asset existence.
let handle = database.ensure("custom://part1.json")?;
// Make database process loaded dependencies.
while database.is_busy() {
database.maintain()?;
}
let contents = handle.access::<&CustomAsset>(&database).contents(&database);
println!("Custom chain contents: {:?}", contents);
Ok(())
}
// Custom asset type.
#[derive(Debug, Default, Deserialize)]
struct CustomAsset {
// Main content.
content: String,
// Optional sibling asset (content continuation).
#[serde(default)]
next: Option<AssetPathStatic>,
}
impl CustomAsset {
fn contents(&self, database: &AssetDatabase) -> String {
// Read this and it's siblings content to output.
let mut result = String::new();
let mut current = Some(self);
while let Some(asset) = current {
result.push_str(asset.content.as_str());
current = current
.as_ref()
.and_then(|asset| asset.next.as_ref())
.and_then(|path| path.find(database))
.and_then(|handle| handle.access_checked::<&Self>(database));
if current.is_some() {
result.push(' ');
}
}
result
}
}
struct CustomAssetProcessor;
impl BundleWithDependenciesProcessor for CustomAssetProcessor {
// Bundle of asset components this asset processor produces from processed asset.
type Bundle = (CustomAsset,);
fn process_bytes(
&mut self,
bytes: Vec<u8>,
) -> Result<BundleWithDependencies<Self::Bundle>, Box<dyn Error>> {
let asset = serde_json::from_slice::<CustomAsset>(&bytes)?;
let dependency = asset.next.clone();
// Return bundled components and optional dependency which will be additionally loaded.
Ok(BundleWithDependencies::new((asset,)).maybe_dependency(dependency))
}
}
Optionally type can implement more specific asset processing that doesn't assume you only care about bytes or even only processed asset:
use anput::world::World;
use keket::{
database::{
handle::{AssetDependency, AssetHandle},
path::AssetPathStatic,
AssetDatabase,
},
fetch::{file::FileAssetFetch, AssetAwaitsResolution, AssetBytesAreReadyToProcess},
protocol::AssetProtocol,
};
use serde::Deserialize;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
// Register custom asset protocol.
.with_protocol(CustomAssetProtocol)
.with_fetch(FileAssetFetch::default().with_root("resources"));
let handle = database.ensure("custom://part1.json")?;
while database.is_busy() {
database.maintain()?;
}
let contents = handle.access::<&CustomAsset>(&database).contents(&database);
println!("Custom chain contents: {:?}", contents);
Ok(())
}
// Custom asset type.
#[derive(Debug, Default, Deserialize)]
struct CustomAsset {
content: String,
#[serde(default)]
next: Option<AssetPathStatic>,
}
impl CustomAsset {
fn contents(&self, database: &AssetDatabase) -> String {
let mut result = String::new();
let mut current = Some(self);
while let Some(asset) = current {
result.push_str(asset.content.as_str());
current = current
.as_ref()
.and_then(|asset| asset.next.as_ref())
.and_then(|path| path.find(database))
.and_then(|handle| handle.access_checked::<&Self>(database));
if current.is_some() {
result.push(' ');
}
}
result
}
}
struct CustomAssetProtocol;
impl AssetProtocol for CustomAssetProtocol {
fn name(&self) -> &str {
"custom"
}
fn process_asset(
&mut self,
handle: AssetHandle,
storage: &mut World,
) -> Result<(), Box<dyn Error>> {
// Always remember that in order for asset to be considered done with processing
// bytes, it has to remove AssetBytesAreReadyToProcess component from that asset.
// We are doing that by taking its bytes content first and removing it after.
let bytes = {
let mut bytes =
storage.component_mut::<true, AssetBytesAreReadyToProcess>(handle.entity())?;
std::mem::take(&mut bytes.0)
};
storage.remove::<(AssetBytesAreReadyToProcess,)>(handle.entity())?;
// Once we have asset bytes, we decode them into asset data.
let asset = serde_json::from_slice::<CustomAsset>(&bytes)?;
// We also have to extract dependencies if it has some.
if let Some(path) = asset.next.clone() {
// For every dependency we get, we need to spawn an asset entity with
// AssetAwaitsResolution component to tell asset database to start loading it.
let entity = storage.spawn((path, AssetAwaitsResolution))?;
// We should also relate processed asset with its dependency asset.
// Dependency relations are useful for traversing asset graphs.
storage.relate::<true, _>(AssetDependency, handle.entity(), entity)?;
}
Ok(())
}
}