Skip to content
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions assets/tests/iter/hashmap.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
local res_type = world.get_type_by_name("TestResourceWithVariousFields")
local res = world.get_resource(res_type)

local map = res.string_map

local count = 0
local found_keys = {}

--- Iterate over PartialReflect refs using pairs
for key, value in pairs(map) do
count = count + 1
found_keys[key] = value
end

assert(count == 2, "Expected 2 entries, got " .. count)
assert(found_keys["foo"] == "bar", "Expected foo=>bar")
assert(found_keys["zoo"] == "zed", "Expected zoo=>zed")
31 changes: 31 additions & 0 deletions assets/tests/iter/hashmap.rhai
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
let res_type = world.get_type_by_name.call("TestResourceWithVariousFields");
let res = world.get_resource.call(res_type);

let map = res.string_map;

let iterator = map.iter();
let count = 0;
let found_keys = #{};

loop {
let result = iterator.next();

if result == () {
break;
}

let key = result[0];
let value = result[1];
count += 1;
found_keys[key] = value;
}

if count != 2 {
throw `Expected 2 entries, got ${count}`;
}
if found_keys["foo"] != "bar" {
throw "Expected foo=>bar";
}
if found_keys["zoo"] != "zed" {
throw "Expected zoo=>zed";
}
21 changes: 21 additions & 0 deletions assets/tests/iter_clone/hashmap.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
local res_type = world.get_type_by_name("TestResourceWithVariousFields")
local res = world.get_resource(res_type)

local map = res.string_map

local iterator = map:iter_clone()
local count = 0
local found_keys = {}

local result = iterator()
while result ~= nil do
local key = result[1]
local value = result[2]
count = count + 1
found_keys[key] = value
result = iterator()
end

assert(count == 2, "Expected 2 entries, got " .. count)
assert(found_keys["foo"] == "bar", "Expected foo=>bar")
assert(found_keys["zoo"] == "zed", "Expected zoo=>zed")
31 changes: 31 additions & 0 deletions assets/tests/iter_clone/hashmap.rhai
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
let res_type = world.get_type_by_name.call("TestResourceWithVariousFields");
let res = world.get_resource.call(res_type);

let map = res.string_map;

let iterator = map.iter_clone();
let count = 0;
let found_keys = #{};

loop {
let result = iterator.next();

if result == () {
break;
}

let key = result[0];
let value = result[1];
count += 1;
found_keys[key] = value;
}

if count != 2 {
throw `Expected 2 entries, got ${count}`;
}
if found_keys["foo"] != "bar" {
throw "Expected foo=>bar";
}
if found_keys["zoo"] != "zed" {
throw "Expected zoo=>zed";
}
27 changes: 27 additions & 0 deletions assets/tests/iter_clone/hashmap_ipairs.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
local res_type = world.get_type_by_name("TestResourceWithVariousFields")
local res = world.get_resource(res_type)

local map = res.string_map

local count = 0
local found_keys = {}

-- ipairs_clone on a map returns (index, [key, value]) where value is a list
for i, entry in map:ipairs_clone() do
assert(i == count + 1, "Index should be sequential: expected " .. (count + 1) .. ", got " .. i)

-- entry should be a list with [key, value]
assert(entry ~= nil, "Entry should not be nil")
assert(entry[1] ~= nil, "Key should not be nil")
assert(entry[2] ~= nil, "Value should not be nil")

local key = entry[1]
local value = entry[2]

count = count + 1
found_keys[key] = value
end

assert(count == 2, "Expected 2 entries, got " .. count)
assert(found_keys["foo"] == "bar", "Expected foo=>bar, got " .. tostring(found_keys["foo"]))
assert(found_keys["zoo"] == "zed", "Expected zoo=>zed, got " .. tostring(found_keys["zoo"]))
23 changes: 23 additions & 0 deletions assets/tests/iter_clone/hashmap_pairs.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
local res_type = world.get_type_by_name("TestResourceWithVariousFields")
local res = world.get_resource(res_type)

local map = res.string_map

local count = 0
local found_keys = {}

-- Use pairs_clone to loop over Reflect values
for key, value in map:pairs_clone() do
count = count + 1
found_keys[key] = value
end

if count ~= 2 then
error(string.format("Expected 2 entries, got %d", count))
end
if found_keys["foo"] ~= "bar" then
error("Expected foo=>bar")
end
if found_keys["zoo"] ~= "zed" then
error("Expected zoo=>zed")
end
17 changes: 17 additions & 0 deletions assets/tests/iter_clone/vec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
local res_type = world.get_type_by_name("TestResourceWithVariousFields")
local res = world.get_resource(res_type)

local iterated_vals = {}
local iterator = res.vec_usize:iter_clone()
local result = iterator()
while result ~= nil do
iterated_vals[#iterated_vals + 1] = result
result = iterator()
end

assert(#iterated_vals == 5, "Length is not 5")
assert(iterated_vals[1] == 1, "First value is not 1")
assert(iterated_vals[2] == 2, "Second value is not 2")
assert(iterated_vals[3] == 3, "Third value is not 3")
assert(iterated_vals[4] == 4, "Fourth value is not 4")
assert(iterated_vals[5] == 5, "Fifth value is not 5")
22 changes: 22 additions & 0 deletions assets/tests/iter_clone/vec.rhai
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
let res_type = world.get_type_by_name.call("TestResourceWithVariousFields");
let res = world.get_resource.call(res_type);

let iterated_vals = [];
let iterator = res.vec_usize.iter_clone();

loop {
let result = iterator.next();

if result == () {
break;
}

iterated_vals.push(result);
}

assert(iterated_vals.len == 5, "Length is not 5");
assert(iterated_vals[0] == 1, "First value is not 1");
assert(iterated_vals[1] == 2, "Second value is not 2");
assert(iterated_vals[2] == 3, "Third value is not 3");
assert(iterated_vals[3] == 4, "Fourth value is not 4");
assert(iterated_vals[4] == 5, "Fifth value is not 5");
15 changes: 15 additions & 0 deletions assets/tests/iter_clone/vec_ipairs.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
local res_type = world.get_type_by_name("TestResourceWithVariousFields")
local res = world.get_resource(res_type)

local iterated_vals = {}
for i, v in res.vec_usize:ipairs_clone() do
assert(i == #iterated_vals + 1, "Index mismatch: expected " .. (#iterated_vals + 1) .. ", got " .. i)
iterated_vals[#iterated_vals + 1] = v
end

assert(#iterated_vals == 5, "Length is not 5")
assert(iterated_vals[1] == 1, "First value is not 1")
assert(iterated_vals[2] == 2, "Second value is not 2")
assert(iterated_vals[3] == 3, "Third value is not 3")
assert(iterated_vals[4] == 4, "Fourth value is not 4")
assert(iterated_vals[5] == 5, "Fifth value is not 5")
14 changes: 14 additions & 0 deletions assets/tests/iter_clone/vec_pairs.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
local res_type = world.get_type_by_name("TestResourceWithVariousFields")
local res = world.get_resource(res_type)

local iterated_vals = {}
for v in res.vec_usize:pairs_clone() do
iterated_vals[#iterated_vals + 1] = v
end

assert(#iterated_vals == 5, "Length is not 5")
assert(iterated_vals[1] == 1, "First value is not 1")
assert(iterated_vals[2] == 2, "Second value is not 2")
assert(iterated_vals[3] == 3, "Third value is not 3")
assert(iterated_vals[4] == 4, "Fourth value is not 4")
assert(iterated_vals[5] == 5, "Fifth value is not 5")
143 changes: 143 additions & 0 deletions crates/bevy_mod_scripting_bindings/src/reference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,15 @@ impl ReflectReference {
ReflectRefIter::new_indexed(self)
}

/// Creates a new iterator for Maps specifically.
/// Unlike `into_iter_infinite`, this iterator is finite and will return None when exhausted.
pub fn into_map_iter(self) -> ReflectMapRefIter {
ReflectMapRefIter {
base: self,
index: 0,
}
}

/// If this is a reference to something with a length accessible via reflection, returns that length.
pub fn len(&self, world: WorldGuard) -> Result<Option<usize>, InteropError> {
self.with_reflect(world, |r| match r.reflect_ref() {
Expand Down Expand Up @@ -924,11 +933,145 @@ impl ReflectRefIter {
};
(next, index)
}

/// Returns the next element as a cloned value using `from_reflect`.
/// Returns a fully reflected value (Box<dyn Reflect>) instead of a reference.
/// Returns Ok(None) when the path is invalid (end of iteration).
pub fn next_cloned(&mut self, world: WorldGuard) -> Result<Option<ReflectReference>, InteropError> {
let index = match &mut self.index {
IterationKey::Index(i) => {
let idx = *i;
*i += 1;
idx
}
};

let element = self.base.with_reflect(world.clone(), |base_reflect| {
match base_reflect.reflect_ref() {
bevy_reflect::ReflectRef::List(list) => {
list.get(index).map(|item| {
<dyn PartialReflect>::from_reflect(item, world.clone())
})
}
bevy_reflect::ReflectRef::Array(array) => {
array.get(index).map(|item| {
<dyn PartialReflect>::from_reflect(item, world.clone())
})
}
_ => None,
}
})?;

match element {
Some(result) => {
let owned_value = result?;
let allocator = world.allocator();
let mut allocator_guard = allocator.write();
let value_ref = ReflectReference::new_allocated_boxed(owned_value, &mut *allocator_guard);
Ok(Some(value_ref))
}
None => Ok(None),
}
}
}

const fn list_index_access(index: usize) -> Access<'static> {
Access::ListIndex(index)
}

/// Iterator specifically for Maps that doesn't use the path system.
/// This bypasses Bevy's path resolution which rejects ListIndex on Maps,
/// and instead directly uses Map::get_at() to iterate over map entries.
pub struct ReflectMapRefIter {
pub(crate) base: ReflectReference,
pub(crate) index: usize,
}

#[profiling::all_functions]
impl ReflectMapRefIter {
/// Returns the next map entry as a (key, value) tuple.
/// Returns Ok(None) when there are no more entries.
pub fn next_ref(&mut self, world: WorldGuard) -> Result<Option<(ReflectReference, ReflectReference)>, InteropError> {
let idx = self.index;
self.index += 1;

// Access the map and get the entry at index
self.base.with_reflect(world.clone(), |reflect| {
match reflect.reflect_ref() {
ReflectRef::Map(map) => {
if let Some((key, value)) = map.get_at(idx) {
let allocator = world.allocator();
let mut allocator_guard = allocator.write();

let key_ref = ReflectReference::new_allocated_boxed_parial_reflect(
key.to_dynamic(),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not convinced about using to_dynamic, all of of the current API surface avoids non Reflect values for a reason, any downcasts (i.e. into_script_ref calls) will fail on dynamic structs etc, this will work for primitives but more complex opaque values which don't have a reflect_clone (I think, i.e. opaque values without an explicit reflect(clone), or even anything with a reflect(ignore) field) will cause issues.

So while this might work individually, but will introduce problems with some bindings.

For example Ref<T> uses try_downcast_ref::<T>, which will fail if the value came from to_dynamic, as DynamicStruct etc are not concrete Reflect types. This is why need calls to FromReflect for getting owned variants of values.

Imo the decision to get a dynamic vs non dynamic value should not be made by the script user, as it's a bit of a leaky abstraction. Ideally the scripter should not need to know anything about the internals here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I agree — I just made a few APIs to discuss the idea. So, just to clarify, we want to keep only the variant that I currently named with the _clone postfix, right?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes exactly

&mut *allocator_guard
)?;

let value_ref = ReflectReference::new_allocated_boxed_parial_reflect(
value.to_dynamic(),
&mut *allocator_guard
)?;

drop(allocator_guard);
Ok(Some((key_ref, value_ref)))
} else {
Ok(None)
}
}
_ => Err(InteropError::unsupported_operation(
reflect.get_represented_type_info().map(|ti| ti.type_id()),
None,
"map iteration on non-map type".to_owned(),
))
}
})?
}

/// Returns the next map entry as a (key, value) tuple, cloning the values.
/// Returns Ok(None) when there are no more entries.
/// This uses `from_reflect` to clone the actual values instead of creating dynamic references.
/// Returns fully reflected values (Box<dyn Reflect>) like `map_get_clone` does.
pub fn next_cloned(&mut self, world: WorldGuard) -> Result<Option<(ReflectReference, ReflectReference)>, InteropError> {
let idx = self.index;
self.index += 1;

// Access the map and get the entry at index
self.base.with_reflect(world.clone(), |reflect| {
match reflect.reflect_ref() {
ReflectRef::Map(map) => {
if let Some((key, value)) = map.get_at(idx) {
let allocator = world.allocator();
let mut allocator_guard = allocator.write();

let owned_key = <dyn PartialReflect>::from_reflect(key, world.clone())?;
let key_ref = ReflectReference::new_allocated_boxed(
owned_key,
&mut *allocator_guard
);

let owned_value = <dyn PartialReflect>::from_reflect(value, world.clone())?;
let value_ref = ReflectReference::new_allocated_boxed(
owned_value,
&mut *allocator_guard
);

drop(allocator_guard);
Ok(Some((key_ref, value_ref)))
} else {
Ok(None)
}
}
_ => Err(InteropError::unsupported_operation(
reflect.get_represented_type_info().map(|ti| ti.type_id()),
None,
"map iteration on non-map type".to_owned(),
))
}
})?
}
}

#[profiling::all_functions]
impl Iterator for ReflectRefIter {
type Item = Result<ReflectReference, InteropError>;
Expand Down
Loading
Loading