makspll / bevy_mod_scripting

Bevy Scripting Plugin
Apache License 2.0
390 stars 31 forks source link

How to turn a ReflectedValue into a specfic type? #85

Closed zwazel closed 10 months ago

zwazel commented 10 months ago

Following scenario, i get a resource

let lobby_list_type = world.get_type_by_name("blablabla::LobbyListResource");
if world.has_resource(lobby_list_type) {
    let lobby_list = world.get_resource(lobby_list_type);
    print(`lobby list type: ${type_of(lobby_list)}, lobby list: ${lobby_list}`);
}

This resource looks like this:

#[derive(Resource, Reflect, Default, Clone)]
#[reflect(Resource)]
pub struct LobbyListResource(pub Vec<u64>);

Quite simple. I now want to access the different lobbies from the vec, I figured out that I can just access them by indexing.

print(`lobby list: ${lobby_list[0]}`);

this works. But I'd like to loop through them:

for lobby in lobby_list {
    print(`lobby: ${lobby}`);
}

Which gives me following error: Runtime error in script "script_name.rhai" For loop expects iterable type. So I looked into Rhai and how you've implemented some things. For example, I can use the api.build_type in the attach_api of an APIProvider, and register some functions as well as it being iterable:

#[derive(Resource, Reflect, Default, Clone)]
#[reflect(Resource)]
pub struct LobbyListResource(pub Vec<u64>);

impl IntoIterator for LobbyListResource {
    type Item = u64;

    type IntoIter = std::vec::IntoIter<Self::Item>;

    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

impl CustomType for LobbyListResource {
    fn build(mut builder: rhai::TypeBuilder<Self>) {
        builder
            .with_name("LobbyListResource")
            .with_indexer_get(LobbyListResource::get_field).is_iterable();
    }
}

struct HandleLobbyApiProvider;

impl APIProvider for HandleLobbyApiProvider {
    type APITarget = Engine;
    type ScriptContext = RhaiContext;
    type DocTarget = RhaiDocFragment;

    fn attach_api(
        &mut self,
        api: &mut Self::APITarget,
    ) -> Result<(), bevy_mod_scripting::prelude::ScriptError> {
        api.build_type::<LobbyListResource>();

        Ok(())
    }
}

But that doesn't solve my problem, because the script only gets a ReflectedValue. So i'm wondering if there is a way for me to turn a ReflectedValue into a specific Type?

makspll commented 10 months ago

TL;DR: in your api provider attach_api add:

api.register_vec_functions::<u64>();

then you should be able to simply:

for lobby in lobby_list.0 {
    print(`elem: ${e}`);
}

Hey, you're missing a link between the reflection system and your custom 'proxy'

So the way ReflectedValue works is it looks for registrations of RhaiProxyable traits on the types you're accessing when traversing your type via field accessors, Vec<T> actually already has a custom type implemented for it hooked into via:

RhaiProxyable impl for Vec

impl<T: RhaiVecElem> RhaiProxyable for Vec<T> {
    fn ref_to_rhai(self_: crate::ScriptRef) -> Result<Dynamic, Box<EvalAltResult>> {
        Ok(Dynamic::from(RhaiVec::<T>::new_ref(self_)))
    }
    // ...
}

The logical path from get_resource is as follows (with error paths and other things simplified away for brevity):

zwazel commented 10 months ago

then you should be able to simply:

for lobby in lobby_list.0 {
    print(`elem: ${e}`);
}

I get a Syntax error: Error in loading script lobby_list_container.rhai: Syntax error for script "script_name" Expecting name of a property. So using .0 does not work, it seems. I changed the resource from a tuple to a struct. And that works. But surprisingly, trying to loop through the empty array causes a crash.

if world.has_resource(lobby_list_type) {
    let lobbies = world.get_resource(lobby_list_type).lobbies;
    for lobby in lobbies {
        print(`lobby: ${lobby}`);
    }
}

The Crash error:

thread 'Compute Task Pool (0)' panicked at 'called `Option::unwrap()` on a `None` value', C:\...\.cargo\git\checkouts\bevy_mod_scripting-ff78cea4271e6409\6bafad2\bevy_mod_scripting_core\src\hosts.rs:364:57
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Encountered a panic in system `bevy_mod_scripting_core::systems::script_hot_reload_handler<bevy_mod_scripting_rhai::RhaiScriptHost<pgc::modding::ModdedScriptRuntimeArguments>>`!
Encountered a panic in system `bevy_app::main_schedule::Main::run_main`!

Which surprised me, i expected it to just act like a normal for loop and skip it when it's empty. For now I can workaround by just checking if its empty first:

if world.has_resource(lobby_list_type) {
    let lobbies = world.get_resource(lobby_list_type).lobbies;
    if !lobbies.is_empty() {
        for lobby in lobbies {
            print(`lobby: ${lobby}`);
        }
    }
}

Is this a confirmed bug or intended behaviour? I can open another issue if you want for this.

zwazel commented 10 months ago

I've created a new branch on my local version and will look into it, i already found a place to add on #68. Will create a Pull Request later

makspll commented 10 months ago

Hey, indeed looks like a bug, please open another issue for the bug so it's easier to find for others :) thanks a lot!

As for accessing the tuple struct, it might be we need slightly different syntax try with '[0]' or '._0' I'll need to check back what this was for rhai

makspll commented 10 months ago

I think I need to get around to building a test tool for all off the common usage patterns of the script APIs

zwazel commented 10 months ago

I've opened issue #86 and will close this one.