Skip to content
Merged
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ categories.workspace = true
readme.workspace = true

[workspace.package]
version = "0.16.0"
version = "0.16.1"
edition = "2024"
authors = ["Maksymilian Mozolewski <makspl17@gmail.com>"]
license = "MIT OR Apache-2.0"
Expand Down
10 changes: 10 additions & 0 deletions assets/tests/asset_operations/asset_operations.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
function on_test()
local test_handle = create_test_asset(42, "TestAssetName")

assert(test_handle ~= nil, "Test asset handle should not be nil")
assert(world.has_asset(test_handle) == true, "has_asset should return true")

local retrieved_asset = world.get_asset(test_handle, types.TestAsset)
assert(retrieved_asset.value == 42, "Asset value should be 42")
assert(retrieved_asset.name == "TestAssetName", "Asset name should be 'TestAssetName'")
end
10 changes: 10 additions & 0 deletions assets/tests/asset_operations/asset_operations.rhai
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
fn on_test() {
let test_handle = create_test_asset(42, "TestAssetName");

assert(test_handle != (), "Test asset handle should not be nil");
assert(world.has_asset.call(test_handle) == true, "has_asset should return true");

let retrieved_asset = world.get_asset.call(test_handle, types.TestAsset);
assert(retrieved_asset.value == 42, "Asset value should be 42");
assert(retrieved_asset.name == "TestAssetName", "Asset name should be 'TestAssetName'");
}
1 change: 1 addition & 0 deletions crates/bevy_mod_scripting_bindings/src/access_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ impl ReflectAccessId {
ReflectBase::Resource(id) => Self::for_component_id(id),
ReflectBase::Component(_, id) => Self::for_component_id(id),
ReflectBase::Owned(id) => Self::for_allocation(id),
ReflectBase::Asset(_, assets_resource_id) => Self::for_component_id(assets_resource_id),
}
}
}
Expand Down
146 changes: 145 additions & 1 deletion crates/bevy_mod_scripting_bindings/src/reference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::{
error::InteropError, reflection_extensions::PartialReflectExt, with_access_read,
with_access_write,
};
use bevy_asset::{ReflectAsset, UntypedHandle};
use bevy_ecs::{component::Component, ptr::Ptr, resource::Resource};
use bevy_mod_scripting_derive::DebugWithTypeInfo;
use bevy_mod_scripting_display::{
Expand Down Expand Up @@ -252,6 +253,89 @@ impl ReflectReference {
}
}

/// Create a new reference to an asset by untyped handle.
/// If the type id is incorrect, you will get runtime errors when trying to access the value.
pub fn new_asset_ref(
handle: UntypedHandle,
asset_type_id: TypeId,
world: WorldGuard,
) -> Result<Self, InteropError> {
Ok(Self {
base: ReflectBaseType::new_asset_base(handle, asset_type_id, world)?,
reflect_path: ParsedPath(Vec::default()),
})
}

/// Tries get an untyped asset handle from this reference.
pub fn try_untyped_asset_handle(
&self,
world: WorldGuard,
) -> Result<UntypedHandle, InteropError> {
let handle_type_id = self.tail_type_id(world.clone())?.ok_or_else(|| {
InteropError::invariant("Cannot determine handle type ID from reflection")
.with_context("Asset handle reflection failed - handle may be invalid or corrupted")
})?;

let type_registry = world.type_registry();
let type_registry = type_registry.read();
let reflect_handle = type_registry
.get_type_data::<bevy_asset::ReflectHandle>(handle_type_id)
.ok_or_else(|| {
InteropError::missing_type_data(
handle_type_id,
stringify!(ReflectHandle).into(),
)
.with_context("Handle type is not registered for asset operations - ensure that you registered it with bevy::App::register_asset_reflect::<T>()")
})?;

let untyped_handle = self.with_reflect(world.clone(), |reflect| {
let reflect_any = reflect.try_as_reflect().ok_or_else(|| {
InteropError::type_mismatch(
std::any::TypeId::of::<dyn bevy_reflect::Reflect>(),
Some(handle_type_id),
)
.with_context("Handle must implement Reflect trait for asset operations")
})?;

reflect_handle
.downcast_handle_untyped(reflect_any.as_any())
.ok_or_else(|| {
InteropError::could_not_downcast(self.clone(), handle_type_id)
.with_context("UntypedHandle downcast failed - handle may be of wrong type or corrupted")
})
})??;
Ok(untyped_handle)
}

/// Get asset from world and return a mutable reference to it
unsafe fn load_asset_mut<'w>(
&self,
handle: &UntypedHandle,
world: WorldGuard<'w>,
) -> Result<&'w mut dyn Reflect, InteropError> {
let type_registry = world.type_registry();
let type_registry = type_registry.read();

let reflect_asset: &ReflectAsset = type_registry
.get_type_data(self.base.type_id)
.ok_or_else(|| InteropError::unregistered_base(self.base.clone()))?;

let world_cell = world.as_unsafe_world_cell()?;
// Safety: The caller guarantees exclusive access to the asset through the WorldGuard,
// and we've validated that the type_id matches the ReflectAsset type data.
// The UnsafeWorldCell is valid for the lifetime 'w of the WorldGuard.
let asset = unsafe { reflect_asset.get_unchecked_mut(world_cell, handle.clone()) }
.ok_or_else(|| {
InteropError::unsupported_operation(
Some(self.base.type_id),
None,
"Asset not loaded or handle is invalid",
)
})?;

Ok(asset)
}

/// Indexes into the reflect path inside this reference.
/// You can use [`Self::reflect`] and [`Self::reflect_mut`] to get the actual value.
pub fn index_path<T: Into<ParsedPath>>(&mut self, index: T) {
Expand Down Expand Up @@ -399,6 +483,11 @@ impl ReflectReference {
return self.walk_path(unsafe { &*arc.get_ptr() });
}

if let ReflectBase::Asset(handle, _) = &self.base.base_id {
let asset = unsafe { self.load_asset_mut(handle, world.clone())? };
return self.walk_path(asset.as_partial_reflect());
}

let type_registry = world.type_registry();
let type_registry = type_registry.read();

Expand Down Expand Up @@ -454,6 +543,11 @@ impl ReflectReference {
return self.walk_path_mut(unsafe { &mut *arc.get_ptr() });
};

if let ReflectBase::Asset(handle, _) = &self.base.base_id {
let asset = unsafe { self.load_asset_mut(handle, world.clone())? };
return self.walk_path_mut(asset.as_partial_reflect_mut());
};

let type_registry = world.type_registry();
let type_registry = type_registry.read();

Expand Down Expand Up @@ -589,6 +683,47 @@ impl ReflectBaseType {
)),
}
}

/// Create a new reflection base pointing to an asset with untyped handle
pub fn new_asset_base(
handle: UntypedHandle,
asset_type_id: TypeId,
world: WorldGuard,
) -> Result<Self, InteropError> {
// We need to get the Assets<T> resource ComponentId by type registry lookup
let type_registry = world.type_registry();
let type_registry = type_registry.read();

// Get the ReflectAsset data to find the Assets<T> resource type ID
let reflect_asset: &ReflectAsset =
type_registry.get_type_data(asset_type_id).ok_or_else(|| {
InteropError::unsupported_operation(
Some(asset_type_id),
None,
"Asset type is not registered with ReflectAsset type data",
)
})?;

let assets_resource_type_id = reflect_asset.assets_resource_type_id();

// Convert the TypeId to ComponentId via unsafe world cell
let world_cell = world.as_unsafe_world_cell()?;
let components = world_cell.components();
let assets_resource_id = components
.get_resource_id(assets_resource_type_id)
.ok_or_else(|| {
InteropError::unsupported_operation(
Some(assets_resource_type_id),
None,
"Assets<T> resource is not registered in the world",
)
})?;

Ok(Self {
type_id: asset_type_id,
base_id: ReflectBase::Asset(handle, assets_resource_id),
})
}
}

/// The Id of the kind of reflection base being pointed to
Expand All @@ -599,8 +734,10 @@ pub enum ReflectBase {
Component(Entity, ComponentId),
/// A resource
Resource(ComponentId),
/// an allocation
/// An allocation
Owned(ReflectAllocationId),
/// An asset accessed by handle
Asset(UntypedHandle, ComponentId),
}

impl DisplayWithTypeInfo for ReflectBase {
Expand Down Expand Up @@ -655,6 +792,13 @@ impl DisplayWithTypeInfo for ReflectBase {
WithTypeInfo::new_with_opt_info(id, type_info_provider)
.display_with_type_info(f, type_info_provider)
}
ReflectBase::Asset(handle, assets_resource_id) => {
f.write_str("asset with handle: ")?;
write!(f, "{:?}", handle)?;
f.write_str(", in Assets resource: ")?;
WithTypeInfo::new_with_opt_info(assets_resource_id, type_info_provider)
.display_with_type_info(f, type_info_provider)
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_mod_scripting_display/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ readme.workspace = true

[dependencies]
bevy_reflect = { workspace = true }
bevy_asset = { workspace = true }
bevy_ecs = { workspace = true, features = ["bevy_reflect"] }
bevy_platform = { workspace = true }
parking_lot = { workspace = true }
Expand Down
9 changes: 9 additions & 0 deletions crates/bevy_mod_scripting_display/src/impls/bevy_asset.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
impl crate::DebugWithTypeInfo for bevy_asset::UntypedHandle {
fn to_string_with_type_info(
&self,
f: &mut std::fmt::Formatter<'_>,
_type_info_provider: Option<&dyn crate::GetTypeInfo>,
) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
1 change: 1 addition & 0 deletions crates/bevy_mod_scripting_display/src/impls/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod bevy_asset;
mod bevy_ecs;
mod bevy_platform;
mod bevy_reflect;
Expand Down
27 changes: 27 additions & 0 deletions crates/bevy_mod_scripting_functions/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,33 @@ impl World {
let world = ctxt.world()?;
world.register_script_component(name).map(Val)
}

/// Retrieves an asset by its handle and asset type registration.
///
/// Arguments:
/// * `ctxt`: The function call context.
/// * `handle_reference`: The handle to the asset (as a reflect reference).
/// * `registration`: The type registration of the asset type.
/// Returns:
/// * `asset`: The asset reference, if the asset is loaded.
fn get_asset(
ctxt: FunctionCallContext,
handle_reference: ReflectReference,
registration: Val<ScriptTypeRegistration>,
) -> Result<Option<ReflectReference>, InteropError> {
profiling::function_scope!("get_asset");
let untyped_handle = handle_reference.try_untyped_asset_handle(ctxt.world()?)?;
Ok(Some(ReflectReference::new_asset_ref(untyped_handle, registration.type_id(), ctxt.world()?)?))
}

/// Checks if can get asset handle
fn has_asset(
ctxt: FunctionCallContext,
handle_reference: ReflectReference,
) -> Result<bool, InteropError> {
profiling::function_scope!("has_asset");
Ok(handle_reference.try_untyped_asset_handle(ctxt.world()?).is_ok())
}
}

#[script_bindings(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use std::{

use ::{
bevy_app::App,
bevy_ecs::{component::ComponentId, entity::Entity, world::World},
bevy_asset::Assets,
bevy_ecs::{change_detection::Mut, component::ComponentId, entity::Entity, world::World},
bevy_reflect::{Reflect, TypeRegistration},
};
use bevy_mod_scripting_asset::Language;
Expand All @@ -20,7 +21,7 @@ use bevy_mod_scripting_bindings::{
};
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha12Rng;
use test_utils::test_data::EnumerateTestComponents;
use test_utils::test_data::{EnumerateTestComponents, TestAsset};

// lazy lock rng state
pub static RNG: std::sync::LazyLock<Mutex<ChaCha12Rng>> = std::sync::LazyLock::new(|| {
Expand Down Expand Up @@ -146,5 +147,18 @@ pub fn register_test_functions(world: &mut App) {
reason.unwrap_or_default()
)
},
)
.register(
"create_test_asset",
|s: FunctionCallContext, value: i32, name: String| {
let world = s.world()?;
let test_asset = TestAsset::new(value, name);
let handle = world.with_resource_mut(|mut assets: Mut<Assets<TestAsset>>| {
assets.add(test_asset)
})?;
let allocator = world.allocator();
let mut allocator = allocator.write();
Ok(ReflectReference::new_allocated(handle, &mut allocator))
},
);
}
19 changes: 18 additions & 1 deletion crates/testing_crates/test_utils/src/test_data.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use std::{alloc::Layout, collections::HashMap};


use bevy_app::{App, ScheduleRunnerPlugin, TaskPoolPlugin};
use bevy_diagnostic::FrameCountPlugin;
use bevy_log::LogPlugin;
use bevy_time::TimePlugin;

use ::{
bevy_asset::AssetPlugin,
bevy_asset::{Asset, AssetApp, AssetPlugin},
bevy_diagnostic::DiagnosticsPlugin,
bevy_ecs::{component::*, prelude::*, world::World},
bevy_reflect::{prelude::*, *},
Expand All @@ -27,6 +28,18 @@ impl TestComponent {
}
}

#[derive(Asset, Reflect, PartialEq, Debug, Clone)]
pub struct TestAsset {
pub value: i32,
pub name: String,
}

impl TestAsset {
pub fn new(value: i32, name: String) -> Self {
Self { value, name }
}
}

#[derive(Component, Reflect, PartialEq, Eq, Debug, Default)]
#[reflect(Component)]
pub struct GenericComponent<T: Default> {
Expand Down Expand Up @@ -361,6 +374,10 @@ pub fn setup_integration_test<F: FnOnce(&mut World, &mut TypeRegistry)>(init: F)
..Default::default()
},
));

app.init_asset::<TestAsset>();
app.register_asset_reflect::<TestAsset>();

app
}

Expand Down
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
- [Constructing Arbitrary Types](./ScriptingReference/constructing-arbitrary-types.md)
- [Core Bindings](./ladfiles/bindings.lad.json)
- [Core Callbacks](./ScriptingReference/core-callbacks.md)
- [Asset Operations](./ScriptingReference/asset-operations.md)

# Developing BMS

Expand Down
Loading
Loading