use bevy_app::{App, Plugin};
use bevy_asset::{
Asset, AssetApp, AssetLoader, AssetMetaCheck, AssetPlugin, LoadContext,
io::{
AssetReader, AssetReaderError, AssetSource, AssetSourceId, PathStream, Reader, VecReader,
},
};
use bevy_reflect::TypePath;
use futures_lite::stream;
use godot::classes::ResourceLoader;
#[cfg(feature = "experimental-threads")]
use godot::classes::resource_loader::ThreadLoadStatus;
use godot::obj::{Gd, Singleton};
use godot::prelude::Resource as GodotBaseResource;
use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, Mutex};
use thiserror::Error;
use crate::interop::GodotResourceHandle;
#[derive(Default)]
pub struct GodotAssetsPlugin;
impl Plugin for GodotAssetsPlugin {
fn build(&self, app: &mut App) {
app.register_asset_source(
AssetSourceId::Default,
AssetSource::build().with_reader(|| Box::new(GodotAssetReader::new())),
);
app.register_asset_source(
AssetSourceId::from("res"),
AssetSource::build().with_reader(|| Box::new(GodotAssetReader::new())),
);
app.register_asset_source(
AssetSourceId::from("user"),
AssetSource::build().with_reader(|| Box::new(GodotAssetReader::new())),
);
app.register_asset_source(
AssetSourceId::from("uid"),
AssetSource::build().with_reader(|| Box::new(GodotAssetReader::new())),
);
app.add_plugins(AssetPlugin {
meta_check: AssetMetaCheck::Never,
..Default::default()
});
app.init_asset::<GodotResource>()
.init_asset_loader::<GodotResourceAssetLoader>();
}
}
pub struct GodotAssetReader;
impl Default for GodotAssetReader {
fn default() -> Self {
Self::new()
}
}
impl GodotAssetReader {
pub fn new() -> Self {
Self
}
}
impl AssetReader for GodotAssetReader {
async fn read<'a>(&'a self, _path: &'a Path) -> Result<impl Reader + 'a, AssetReaderError> {
Ok(VecReader::new(Vec::<u8>::new()))
}
async fn read_meta<'a>(
&'a self,
_path: &'a Path,
) -> Result<impl Reader + 'a, AssetReaderError> {
Ok(VecReader::new(Vec::<u8>::new()))
}
async fn read_directory<'a>(
&'a self,
_path: &'a Path,
) -> Result<Box<PathStream>, AssetReaderError> {
let empty_iter = std::iter::empty::<std::path::PathBuf>();
let stream = stream::iter(empty_iter);
Ok(Box::new(stream) as Box<PathStream>)
}
async fn is_directory<'a>(&'a self, _path: &'a Path) -> Result<bool, AssetReaderError> {
Ok(false)
}
}
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum GodotAssetLoaderError {
#[error("Failed to load Godot resource: {0}")]
ResourceLoadFailed(String),
}
#[derive(Asset, TypePath, Debug, Clone)]
pub struct GodotResource {
handle: GodotResourceHandle,
}
impl GodotResource {
pub fn get(&mut self) -> Gd<GodotBaseResource> {
self.handle.get()
}
pub fn handle(&self) -> &GodotResourceHandle {
&self.handle
}
pub fn try_cast<T>(&mut self) -> Option<Gd<T>>
where
T: godot::obj::GodotClass + godot::obj::Inherits<GodotBaseResource>,
{
self.get().try_cast().ok()
}
}
#[derive(Debug)]
enum LoadingState {
Requested,
Loading,
Ready,
Failed,
}
static LOADING_TRACKER: once_cell::sync::Lazy<Arc<Mutex<HashMap<String, LoadingState>>>> =
once_cell::sync::Lazy::new(|| Arc::new(Mutex::new(HashMap::new())));
#[derive(Default)]
pub struct GodotResourceAssetLoader;
impl AssetLoader for GodotResourceAssetLoader {
type Asset = GodotResource;
type Settings = ();
type Error = GodotAssetLoaderError;
#[cfg(feature = "experimental-threads")]
async fn load(
&self,
_reader: &mut dyn Reader,
_settings: &(),
load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let godot_path = load_context.asset_path().to_string();
{
let mut resource_loader = ResourceLoader::singleton();
let path_gstring = godot::builtin::GString::from(&godot_path);
resource_loader.load_threaded_request(&path_gstring);
}
{
let mut tracker = LOADING_TRACKER.lock().unwrap();
tracker.insert(godot_path.clone(), LoadingState::Requested);
}
loop {
let status = {
let mut resource_loader = ResourceLoader::singleton();
let path_gstring = godot::builtin::GString::from(&godot_path);
resource_loader.load_threaded_get_status(&path_gstring)
};
match status {
ThreadLoadStatus::LOADED => {
let resource = {
let mut resource_loader = ResourceLoader::singleton();
let path_gstring = godot::builtin::GString::from(&godot_path);
resource_loader.load_threaded_get(&path_gstring)
};
match resource {
Some(resource) => {
{
let mut tracker = LOADING_TRACKER.lock().unwrap();
tracker.insert(godot_path.clone(), LoadingState::Ready);
}
let handle = GodotResourceHandle::new(resource);
return Ok(GodotResource { handle });
}
None => {
{
let mut tracker = LOADING_TRACKER.lock().unwrap();
tracker.insert(godot_path.clone(), LoadingState::Failed);
}
return Err(GodotAssetLoaderError::ResourceLoadFailed(format!(
"Failed to get loaded Godot resource: {godot_path}"
)));
}
}
}
ThreadLoadStatus::FAILED => {
{
let mut tracker = LOADING_TRACKER.lock().unwrap();
tracker.insert(godot_path.clone(), LoadingState::Failed);
}
return Err(GodotAssetLoaderError::ResourceLoadFailed(format!(
"Godot ResourceLoader failed to load: {godot_path}"
)));
}
ThreadLoadStatus::INVALID_RESOURCE => {
{
let mut tracker = LOADING_TRACKER.lock().unwrap();
tracker.insert(godot_path.clone(), LoadingState::Failed);
}
return Err(GodotAssetLoaderError::ResourceLoadFailed(format!(
"Invalid resource path or corrupted resource: {godot_path}"
)));
}
_ => {
{
let mut tracker = LOADING_TRACKER.lock().unwrap();
tracker.insert(godot_path.clone(), LoadingState::Loading);
}
futures_lite::future::yield_now().await;
}
}
}
}
#[cfg(not(feature = "experimental-threads"))]
async fn load(
&self,
_reader: &mut dyn Reader,
_settings: &(),
load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let godot_path = load_context.asset_path().to_string();
let path_gstring = godot::builtin::GString::from(&godot_path);
{
let mut tracker = LOADING_TRACKER.lock().unwrap();
tracker.insert(godot_path.clone(), LoadingState::Requested);
}
let mut resource_loader = ResourceLoader::singleton();
let resource = resource_loader.load(&path_gstring);
match resource {
Some(resource) => {
{
let mut tracker = LOADING_TRACKER.lock().unwrap();
tracker.insert(godot_path.clone(), LoadingState::Ready);
}
let handle = GodotResourceHandle::new(resource);
Ok(GodotResource { handle })
}
None => {
{
let mut tracker = LOADING_TRACKER.lock().unwrap();
tracker.insert(godot_path.clone(), LoadingState::Failed);
}
Err(GodotAssetLoaderError::ResourceLoadFailed(format!(
"Failed to load Godot resource: {godot_path}"
)))
}
}
}
fn extensions(&self) -> &[&str] {
&[
"tscn", "scn", "res", "tres", "jpg", "jpeg", "png", "wav", "mp3", "ogg", "aac", ]
}
}