Skip to content

Commit c376dbe

Browse files
committed
feat: add asset operations via reflect reference
1 parent da5e7d3 commit c376dbe

File tree

10 files changed

+258
-4
lines changed

10 files changed

+258
-4
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
function on_test()
2+
local asset_data = create_test_asset(42, "TestAssetName")
3+
local test_handle = asset_data[1]
4+
local asset_type_reg = asset_data[2]
5+
6+
assert(test_handle ~= nil, "Test asset handle should not be nil")
7+
assert(asset_type_reg ~= nil, "TestAsset type registration should exist")
8+
9+
-- Check asset exists and retrieve it
10+
assert(world.has_asset(test_handle) == true, "has_asset should return true")
11+
12+
local retrieved_asset = world.get_asset(test_handle, asset_type_reg)
13+
assert(retrieved_asset ~= nil, "Should be able to retrieve the test asset")
14+
assert(retrieved_asset.value == 42, "Asset value should be 42")
15+
assert(retrieved_asset.name == "TestAssetName", "Asset name should be 'TestAssetName'")
16+
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
fn on_test() {
2+
let asset_data = create_test_asset(42, "TestAssetName");
3+
let test_handle = asset_data[0];
4+
let asset_type_reg = asset_data[1];
5+
6+
assert(test_handle != (), "Test asset handle should not be nil");
7+
assert(asset_type_reg != (), "TestAsset type registration should exist");
8+
9+
// Check asset exists and retrieve it
10+
assert(world.has_asset.call(test_handle) == true, "has_asset should return true");
11+
12+
let retrieved_asset = world.get_asset.call(test_handle, asset_type_reg);
13+
assert(retrieved_asset != (), "Should be able to retrieve the test asset");
14+
assert(retrieved_asset.value == 42, "Asset value should be 42");
15+
assert(retrieved_asset.name == "TestAssetName", "Asset name should be 'TestAssetName'");
16+
}

crates/bevy_mod_scripting_bindings/src/access_map.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ impl ReflectAccessId {
223223
ReflectBase::Resource(id) => Self::for_component_id(id),
224224
ReflectBase::Component(_, id) => Self::for_component_id(id),
225225
ReflectBase::Owned(id) => Self::for_allocation(id),
226+
ReflectBase::Asset(_, assets_resource_id) => Self::for_component_id(assets_resource_id),
226227
}
227228
}
228229
}

crates/bevy_mod_scripting_bindings/src/reference.rs

Lines changed: 143 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::{
1010
error::InteropError, reflection_extensions::PartialReflectExt, with_access_read,
1111
with_access_write,
1212
};
13+
use bevy_asset::{ReflectAsset, UntypedHandle};
1314
use bevy_ecs::{component::Component, ptr::Ptr, resource::Resource};
1415
use bevy_mod_scripting_derive::DebugWithTypeInfo;
1516
use bevy_mod_scripting_display::{
@@ -252,6 +253,61 @@ impl ReflectReference {
252253
}
253254
}
254255

256+
/// Create a new reference to an asset by untyped handle.
257+
/// If the type id is incorrect, you will get runtime errors when trying to access the value.
258+
pub fn new_asset_ref(
259+
handle: UntypedHandle,
260+
asset_type_id: TypeId,
261+
world: WorldGuard,
262+
) -> Result<Self, InteropError> {
263+
Ok(Self {
264+
base: ReflectBaseType::new_asset_base(handle, asset_type_id, world)?,
265+
reflect_path: ParsedPath(Vec::default()),
266+
})
267+
}
268+
269+
/// Tries get an untyped asset handle from this reference.
270+
pub fn try_untyped_asset_handle(
271+
&self,
272+
world: WorldGuard,
273+
) -> Result<UntypedHandle, InteropError> {
274+
let handle_type_id = self.tail_type_id(world.clone())?.ok_or_else(|| {
275+
InteropError::invariant("Cannot determine handle type ID from reflection")
276+
.with_context("Asset handle reflection failed - handle may be invalid or corrupted")
277+
})?;
278+
279+
let type_registry = world.type_registry();
280+
let type_registry = type_registry.read();
281+
let reflect_handle = type_registry
282+
.get_type_data::<bevy_asset::ReflectHandle>(handle_type_id)
283+
.ok_or_else(|| {
284+
InteropError::missing_type_data(
285+
handle_type_id,
286+
"ReflectHandle".to_string(),
287+
)
288+
.with_context("Handle type is not registered for asset operations - ensure the asset type is properly registered with ReflectHandle type data")
289+
})?;
290+
291+
let untyped_handle = self.with_reflect(world.clone(), |reflect| {
292+
let reflect_any = reflect.try_as_reflect().ok_or_else(|| {
293+
InteropError::type_mismatch(
294+
std::any::TypeId::of::<dyn bevy_reflect::Reflect>(),
295+
Some(handle_type_id),
296+
)
297+
.with_context("Handle must implement Reflect trait for asset operations")
298+
})?;
299+
300+
reflect_handle
301+
.downcast_handle_untyped(reflect_any.as_any())
302+
.ok_or_else(|| {
303+
InteropError::invariant("Failed to get UntypedHandle")
304+
.with_context("Handle downcast failed - handle may be of wrong type or corrupted")
305+
})
306+
})??;
307+
308+
Ok(untyped_handle)
309+
}
310+
255311
/// Indexes into the reflect path inside this reference.
256312
/// You can use [`Self::reflect`] and [`Self::reflect_mut`] to get the actual value.
257313
pub fn index_path<T: Into<ParsedPath>>(&mut self, index: T) {
@@ -399,6 +455,11 @@ impl ReflectReference {
399455
return self.walk_path(unsafe { &*arc.get_ptr() });
400456
}
401457

458+
if let ReflectBase::Asset(handle, _) = &self.base.base_id {
459+
let asset = unsafe { self.load_asset_mut(handle, world.clone())? };
460+
return self.walk_path(asset.as_partial_reflect());
461+
}
462+
402463
let type_registry = world.type_registry();
403464
let type_registry = type_registry.read();
404465

@@ -454,6 +515,11 @@ impl ReflectReference {
454515
return self.walk_path_mut(unsafe { &mut *arc.get_ptr() });
455516
};
456517

518+
if let ReflectBase::Asset(handle, _) = &self.base.base_id {
519+
let asset = unsafe { self.load_asset_mut(handle, world.clone())? };
520+
return self.walk_path_mut(asset.as_partial_reflect_mut());
521+
};
522+
457523
let type_registry = world.type_registry();
458524
let type_registry = type_registry.read();
459525

@@ -486,6 +552,32 @@ impl ReflectReference {
486552
self.walk_path_mut(base.as_partial_reflect_mut())
487553
}
488554

555+
/// Get asset from world and return a mutable reference to it
556+
unsafe fn load_asset_mut<'w>(
557+
&self,
558+
handle: &UntypedHandle,
559+
world: WorldGuard<'w>,
560+
) -> Result<&'w mut dyn Reflect, InteropError> {
561+
let type_registry = world.type_registry();
562+
let type_registry = type_registry.read();
563+
564+
let reflect_asset: &ReflectAsset = type_registry
565+
.get_type_data(self.base.type_id)
566+
.ok_or_else(|| InteropError::unregistered_base(self.base.clone()))?;
567+
568+
let world_cell = world.as_unsafe_world_cell()?;
569+
let asset = unsafe { reflect_asset.get_unchecked_mut(world_cell, handle.clone()) }
570+
.ok_or_else(|| {
571+
InteropError::unsupported_operation(
572+
Some(self.base.type_id),
573+
None,
574+
"Asset not loaded or handle is invalid",
575+
)
576+
})?;
577+
578+
Ok(asset)
579+
}
580+
489581
fn walk_path<'a>(
490582
&self,
491583
root: &'a dyn PartialReflect,
@@ -589,6 +681,47 @@ impl ReflectBaseType {
589681
)),
590682
}
591683
}
684+
685+
/// Create a new reflection base pointing to an asset with untyped handle
686+
pub fn new_asset_base(
687+
handle: UntypedHandle,
688+
asset_type_id: TypeId,
689+
world: WorldGuard,
690+
) -> Result<Self, InteropError> {
691+
// We need to get the Assets<T> resource ComponentId by type registry lookup
692+
let type_registry = world.type_registry();
693+
let type_registry = type_registry.read();
694+
695+
// Get the ReflectAsset data to find the Assets<T> resource type ID
696+
let reflect_asset: &ReflectAsset =
697+
type_registry.get_type_data(asset_type_id).ok_or_else(|| {
698+
InteropError::unsupported_operation(
699+
Some(asset_type_id),
700+
None,
701+
"Asset type is not registered with ReflectAsset type data",
702+
)
703+
})?;
704+
705+
let assets_resource_type_id = reflect_asset.assets_resource_type_id();
706+
707+
// Convert the TypeId to ComponentId via unsafe world cell
708+
let world_cell = world.as_unsafe_world_cell()?;
709+
let components = world_cell.components();
710+
let assets_resource_id = components
711+
.get_resource_id(assets_resource_type_id)
712+
.ok_or_else(|| {
713+
InteropError::unsupported_operation(
714+
Some(assets_resource_type_id),
715+
None,
716+
"Assets<T> resource is not registered in the world",
717+
)
718+
})?;
719+
720+
Ok(Self {
721+
type_id: asset_type_id,
722+
base_id: ReflectBase::Asset(handle, assets_resource_id),
723+
})
724+
}
592725
}
593726

594727
/// The Id of the kind of reflection base being pointed to
@@ -599,8 +732,10 @@ pub enum ReflectBase {
599732
Component(Entity, ComponentId),
600733
/// A resource
601734
Resource(ComponentId),
602-
/// an allocation
735+
/// An allocation
603736
Owned(ReflectAllocationId),
737+
/// An asset accessed by handle
738+
Asset(UntypedHandle, ComponentId),
604739
}
605740

606741
impl DisplayWithTypeInfo for ReflectBase {
@@ -655,6 +790,13 @@ impl DisplayWithTypeInfo for ReflectBase {
655790
WithTypeInfo::new_with_opt_info(id, type_info_provider)
656791
.display_with_type_info(f, type_info_provider)
657792
}
793+
ReflectBase::Asset(handle, assets_resource_id) => {
794+
f.write_str("asset with handle: ")?;
795+
write!(f, "{:?}", handle)?;
796+
f.write_str(", in Assets resource: ")?;
797+
WithTypeInfo::new_with_opt_info(assets_resource_id, type_info_provider)
798+
.display_with_type_info(f, type_info_provider)
799+
}
658800
}
659801
}
660802
}

crates/bevy_mod_scripting_display/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ readme.workspace = true
1313

1414
[dependencies]
1515
bevy_reflect = { workspace = true }
16+
bevy_asset = { workspace = true }
1617
bevy_ecs = { workspace = true, features = ["bevy_reflect"] }
1718
bevy_platform = { workspace = true }
1819
parking_lot = { workspace = true }
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
impl crate::DebugWithTypeInfo for bevy_asset::UntypedHandle {
2+
fn to_string_with_type_info(
3+
&self,
4+
f: &mut std::fmt::Formatter<'_>,
5+
_type_info_provider: Option<&dyn crate::GetTypeInfo>,
6+
) -> std::fmt::Result {
7+
write!(f, "{:?}", self)
8+
}
9+
}

crates/bevy_mod_scripting_display/src/impls/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
mod bevy_asset;
12
mod bevy_ecs;
23
mod bevy_platform;
34
mod bevy_reflect;

crates/bevy_mod_scripting_functions/src/core.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,33 @@ impl World {
512512
let world = ctxt.world()?;
513513
world.register_script_component(name).map(Val)
514514
}
515+
516+
/// Retrieves an asset by its handle and asset type registration.
517+
///
518+
/// Arguments:
519+
/// * `ctxt`: The function call context.
520+
/// * `handle_reference`: The handle to the asset (as a reflect reference).
521+
/// * `registration`: The type registration of the asset type.
522+
/// Returns:
523+
/// * `asset`: The asset reference, if the asset is loaded.
524+
fn get_asset(
525+
ctxt: FunctionCallContext,
526+
handle_reference: ReflectReference,
527+
registration: Val<ScriptTypeRegistration>,
528+
) -> Result<Option<ReflectReference>, InteropError> {
529+
profiling::function_scope!("get_asset");
530+
let untyped_handle = handle_reference.try_untyped_asset_handle(ctxt.world()?)?;
531+
Ok(Some(ReflectReference::new_asset_ref(untyped_handle, registration.type_id(), ctxt.world()?)?))
532+
}
533+
534+
/// Checks if can get asset handle
535+
fn has_asset(
536+
ctxt: FunctionCallContext,
537+
handle_reference: ReflectReference,
538+
) -> Result<bool, InteropError> {
539+
profiling::function_scope!("has_asset");
540+
Ok(handle_reference.try_untyped_asset_handle(ctxt.world()?).is_ok())
541+
}
515542
}
516543

517544
#[script_bindings(

crates/testing_crates/script_integration_test_harness/src/test_functions.rs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ use std::{
55

66
use ::{
77
bevy_app::App,
8-
bevy_ecs::{component::ComponentId, entity::Entity, world::World},
8+
bevy_asset::Assets,
9+
bevy_ecs::{change_detection::Mut, component::ComponentId, entity::Entity, world::World},
910
bevy_reflect::{Reflect, TypeRegistration},
1011
};
1112
use bevy_mod_scripting_asset::Language;
@@ -20,7 +21,7 @@ use bevy_mod_scripting_bindings::{
2021
};
2122
use rand::{Rng, SeedableRng};
2223
use rand_chacha::ChaCha12Rng;
23-
use test_utils::test_data::EnumerateTestComponents;
24+
use test_utils::test_data::{EnumerateTestComponents, TestAsset};
2425

2526
// lazy lock rng state
2627
pub static RNG: std::sync::LazyLock<Mutex<ChaCha12Rng>> = std::sync::LazyLock::new(|| {
@@ -146,5 +147,28 @@ pub fn register_test_functions(world: &mut App) {
146147
reason.unwrap_or_default()
147148
)
148149
},
150+
)
151+
.register(
152+
"create_test_asset",
153+
|s: FunctionCallContext, value: i32, name: String| {
154+
let world = s.world()?;
155+
let test_asset = TestAsset::new(value, name);
156+
157+
let handle = world.with_resource_mut(|mut assets: Mut<Assets<TestAsset>>| {
158+
assets.add(test_asset)
159+
})?;
160+
let type_registry = world.type_registry();
161+
let type_registry = type_registry.read();
162+
let registration = type_registry.get(std::any::TypeId::of::<TestAsset>())
163+
.ok_or_else(|| InteropError::str("TestAsset type not registered"))?;
164+
let reg = ScriptTypeRegistration::new(Arc::new(registration.clone()));
165+
166+
let allocator = world.allocator();
167+
let mut allocator = allocator.write();
168+
Ok(vec![
169+
ReflectReference::new_allocated(handle, &mut allocator),
170+
ReflectReference::new_allocated(reg, &mut allocator)
171+
])
172+
},
149173
);
150174
}

crates/testing_crates/test_utils/src/test_data.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
use std::{alloc::Layout, collections::HashMap};
22

3+
34
use bevy_app::{App, ScheduleRunnerPlugin, TaskPoolPlugin};
45
use bevy_diagnostic::FrameCountPlugin;
56
use bevy_log::LogPlugin;
67
use bevy_time::TimePlugin;
78

89
use ::{
9-
bevy_asset::AssetPlugin,
10+
bevy_asset::{Asset, AssetApp, AssetPlugin},
1011
bevy_diagnostic::DiagnosticsPlugin,
1112
bevy_ecs::{component::*, prelude::*, world::World},
1213
bevy_reflect::{prelude::*, *},
@@ -27,6 +28,18 @@ impl TestComponent {
2728
}
2829
}
2930

31+
#[derive(Asset, Reflect, PartialEq, Debug, Clone)]
32+
pub struct TestAsset {
33+
pub value: i32,
34+
pub name: String,
35+
}
36+
37+
impl TestAsset {
38+
pub fn new(value: i32, name: String) -> Self {
39+
Self { value, name }
40+
}
41+
}
42+
3043
#[derive(Component, Reflect, PartialEq, Eq, Debug, Default)]
3144
#[reflect(Component)]
3245
pub struct GenericComponent<T: Default> {
@@ -361,6 +374,10 @@ pub fn setup_integration_test<F: FnOnce(&mut World, &mut TypeRegistry)>(init: F)
361374
..Default::default()
362375
},
363376
));
377+
378+
app.init_asset::<TestAsset>();
379+
app.register_asset_reflect::<TestAsset>();
380+
364381
app
365382
}
366383

0 commit comments

Comments
 (0)