bevy icon indicating copy to clipboard operation
bevy copied to clipboard

Wrong asset loader is used after extensionless assets change

Open rparrett opened this issue 1 year ago • 3 comments

Bevy version

main, after #10153

What you did

With the following fork/branch of bevy_common_assets: https://github.com/rparrett/bevy_common_assets/tree/bevy13

cargo run --example multiple_formats --features=json,ron

What went wrong

The ron file is parsed as json and throws an error.

2024-01-31T22:52:54.621165Z ERROR bevy_asset::server: Failed to load asset 'trees.level.ron' with asset loader 'bevy_common_assets::json::JsonAssetLoader<multiple_formats::Level>': Could not parse the JSON: expected value at line 1 column 1

rparrett avatar Jan 31 '24 23:01 rparrett

Or here's a smaller repro:

Open
use bevy::utils::thiserror;
use bevy::{
    asset::{io::Reader, AssetLoader, LoadContext},
    prelude::*,
    reflect::TypePath,
    utils::BoxedFuture,
};
use thiserror::Error;

#[non_exhaustive]
#[derive(Debug, Error)]
#[error("error")]
pub struct GenericLoaderError;

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

#[derive(Default)]
pub struct ALoader;

impl AssetLoader for ALoader {
    type Asset = C;
    type Settings = ();
    type Error = GenericLoaderError;
    fn load<'a>(
        &'a self,
        _reader: &'a mut Reader,
        _settings: &'a (),
        _load_context: &'a mut LoadContext,
    ) -> BoxedFuture<'a, Result<Self::Asset, Self::Error>> {
        Box::pin(async move { Ok(C("A".to_string())) })
    }

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

#[derive(Default)]
pub struct BLoader;

impl AssetLoader for BLoader {
    type Asset = C;
    type Settings = ();
    type Error = GenericLoaderError;
    fn load<'a>(
        &'a self,
        _reader: &'a mut Reader,
        _settings: &'a (),
        _load_context: &'a mut LoadContext,
    ) -> BoxedFuture<'a, Result<Self::Asset, Self::Error>> {
        Box::pin(async move { Ok(C("B".to_string())) })
    }

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

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .init_resource::<State>()
        .init_asset::<C>()
        .register_asset_loader(ALoader)
        .register_asset_loader(BLoader)
        .add_systems(Startup, setup)
        .add_systems(Update, print_on_load)
        .run();
}

#[derive(Resource, Default)]
struct State {
    handle_a: Handle<C>,
    handle_b: Handle<C>,
    printed: bool,
}

fn setup(mut state: ResMut<State>, asset_server: Res<AssetServer>) {
    state.handle_a = asset_server.load("data/custom.a");
    state.handle_b = asset_server.load("data/custom.b");
}

fn print_on_load(mut state: ResMut<State>, a_assets: Res<Assets<C>>) {
    if state.printed {
        return;
    }

    let a_asset = a_assets.get(&state.handle_a);
    let b_asset = a_assets.get(&state.handle_b);

    if b_asset.is_none() && b_asset.is_none() {
        return;
    }

    info!("Loaded: {:?} {:?}", a_asset.unwrap(), b_asset.unwrap());

    state.printed = true;
}

Before #10153: Loaded: C("A") C("B"). After: Loaded: C("B") C("B")

rparrett avatar Feb 01 '24 03:02 rparrett

Hi, I believe the issue here is caused by your two different loaders producing the same asset type C. In the extension-less asset PR, asset loader resolution will short-circuit file extensions in favour of asset type resolution when that information is available. This is effectively the inverse problem to having two asset loaders registered for the same file extension.

I believe this can be fixed by amending AssetLoaders to store a list of MaybeAssetLoader's in type_id_to_loader and select 1 (probably last added). Then during asset loader resolution check if there are multiple loaders for that type, and then use the file extension to attempt to break the deadlock.

I'm terribly sorry this popped up for you, I'll make a PR based on my outline here and hopefully get that fixed soon!

bushrat011899 avatar Feb 01 '24 04:02 bushrat011899

I believe I'm hitting a related issue but in my case it's a custom Image loader hitting unreachables. Minimal repro follows:

use bevy::asset::io::Reader;
use bevy::asset::{AssetLoader, LoadContext};
use bevy::prelude::*;
use bevy::utils::BoxedFuture;

#[derive(Default)]
struct UnreachableLoader;

impl AssetLoader for UnreachableLoader {
    type Asset = Image;
    type Error = std::io::Error;
    type Settings = ();

    fn load<'a>(
        &'a self,
        _reader: &'a mut Reader<'_>,
        _settings: &'a Self::Settings,
        _load_context: &'a mut LoadContext<'_>,
    ) -> BoxedFuture<'a, Result<Self::Asset, Self::Error>> {
        Box::pin(async { Ok(Image::default()) })
    }

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

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .init_asset_loader::<UnreachableLoader>()
        .run();
}
bevy-f7ffde730c324c74\afa7b5c\crates\bevy_asset\src\server\mod.rs:164:47:
internal error: entered unreachable code

The above PR also fixes my issue so I'm mainly posting this for posterity.

monax3 avatar Feb 10 '24 04:02 monax3