Fix BRP query failing when specifying missing/invalid components (#18871)

# Objective

- Fixes #18869.

## Solution

The issue was the `?` after a `Result` raising the error, instead of
treating it.
Instead it is handled with `ok`, `and_then`, `map` ...

_Edit: I added the following logic._
On `bevy/query` remote requests, when `strict` is false:
- Unregistered components in `option` and `without` are ignored.
- Unregistered components in `has` are considered absent from the
entity.
- Unregistered components in `components` and `with` result in an empty
response since they specify hard requirements.

I made the `get_component_ids` function return a
`AnyhowResult<(Vec<(TypeId, ComponentId)>, Vec<String>)>` instead of the
previous `AnyhowResult<Vec<(TypeId, ComponentId)>>`; that is I added the
list of unregistered components.

## Testing

I tested changes using the same procedure as in the linked issue:
```sh
cargo run --example server --features="bevy_remote"
```
In another terminal:
```sh
# Not strict:
$ curl -X POST http://localhost:15702 -H "Content-Type: application/json" -d '{ "jsonrpc": "2.0", "method": "bevy/query", "id": 0, "params": { "data": { "components": [ "foo::bar::MyComponent" ] } } }'
{"jsonrpc":"2.0","id":0,"result":[]}

# Strict:
$ curl -X POST http://localhost:15702 -H "Content-Type: application/json" -d '{ "jsonrpc": "2.0", "method": "bevy/query", "id": 0, "params": { "data": { "components": [ "foo::bar::MyComponent" ] }, "strict": true } }'
{"jsonrpc":"2.0","id":0,"error":{"code":-23402,"message":"Component `foo::bar::MyComponent` isn't registered or used in the world"}}
```
This commit is contained in:
Sébastien Job 2025-05-26 17:26:46 +02:00 committed by François Mockers
parent 8ff0c6481a
commit ddc3264794

View File

@ -718,17 +718,27 @@ pub fn process_remote_query_request(In(params): In<Option<Value>>, world: &mut W
let app_type_registry = world.resource::<AppTypeRegistry>().clone();
let type_registry = app_type_registry.read();
let components = get_component_ids(&type_registry, world, components, strict)
let (components, unregistered_in_components) =
get_component_ids(&type_registry, world, components, strict)
.map_err(BrpError::component_error)?;
let option = get_component_ids(&type_registry, world, option, strict)
let (option, _) = get_component_ids(&type_registry, world, option, strict)
.map_err(BrpError::component_error)?;
let has =
let (has, unregistered_in_has) =
get_component_ids(&type_registry, world, has, strict).map_err(BrpError::component_error)?;
let without = get_component_ids(&type_registry, world, without, strict)
let (without, _) = get_component_ids(&type_registry, world, without, strict)
.map_err(BrpError::component_error)?;
let with = get_component_ids(&type_registry, world, with, strict)
let (with, unregistered_in_with) = get_component_ids(&type_registry, world, with, strict)
.map_err(BrpError::component_error)?;
// When "strict" is false:
// - Unregistered components in "option" and "without" are ignored.
// - Unregistered components in "has" are considered absent from the entity.
// - Unregistered components in "components" and "with" result in an empty
// response since they specify hard requirements.
if !unregistered_in_components.is_empty() || !unregistered_in_with.is_empty() {
return serde_json::to_value(BrpQueryResponse::default()).map_err(BrpError::internal);
}
let mut query = QueryBuilder::<FilteredEntityRef>::new(world);
for (_, component) in &components {
query.ref_id(*component);
@ -784,6 +794,7 @@ pub fn process_remote_query_request(In(params): In<Option<Value>>, world: &mut W
let has_map = build_has_map(
row.clone(),
has_paths_and_reflect_components.iter().copied(),
&unregistered_in_has,
);
response.push(BrpQueryRow {
entity: row.id(),
@ -1024,12 +1035,19 @@ pub fn process_remote_remove_request(
let type_registry = app_type_registry.read();
let component_ids = get_component_ids(&type_registry, world, components, true)
.and_then(|(registered, unregistered)| {
if unregistered.is_empty() {
Ok(registered)
} else {
Err(anyhow!("Unregistered component types: {:?}", unregistered))
}
})
.map_err(BrpError::component_error)?;
// Remove the components.
let mut entity_world_mut = get_entity_mut(world, entity)?;
for (_, component_id) in component_ids {
entity_world_mut.remove_by_id(component_id);
for (_, component_id) in component_ids.iter() {
entity_world_mut.remove_by_id(*component_id);
}
Ok(Value::Null)
@ -1264,8 +1282,9 @@ fn get_entity_mut(world: &mut World, entity: Entity) -> Result<EntityWorldMut<'_
.map_err(|_| BrpError::entity_not_found(entity))
}
/// Returns the [`TypeId`] and [`ComponentId`] of the components with the given
/// full path names.
/// Given components full path, returns a tuple that contains
/// - A list of corresponding [`TypeId`] and [`ComponentId`] for registered components.
/// - A list of unregistered component paths.
///
/// Note that the supplied path names must be *full* path names: e.g.
/// `bevy_transform::components::transform::Transform` instead of `Transform`.
@ -1274,25 +1293,33 @@ fn get_component_ids(
world: &World,
component_paths: Vec<String>,
strict: bool,
) -> AnyhowResult<Vec<(TypeId, ComponentId)>> {
) -> AnyhowResult<(Vec<(TypeId, ComponentId)>, Vec<String>)> {
let mut component_ids = vec![];
let mut unregistered_components = vec![];
for component_path in component_paths {
let type_id = get_component_type_registration(type_registry, &component_path)?.type_id();
let Some(component_id) = world.components().get_id(type_id) else {
if strict {
let maybe_component_tuple = get_component_type_registration(type_registry, &component_path)
.ok()
.and_then(|type_registration| {
let type_id = type_registration.type_id();
world
.components()
.get_id(type_id)
.map(|component_id| (type_id, component_id))
});
if let Some((type_id, component_id)) = maybe_component_tuple {
component_ids.push((type_id, component_id));
} else if strict {
return Err(anyhow!(
"Component `{}` isn't used in the world",
"Component `{}` isn't registered or used in the world",
component_path
));
} else {
unregistered_components.push(component_path);
}
continue;
};
component_ids.push((type_id, component_id));
}
Ok(component_ids)
Ok((component_ids, unregistered_components))
}
/// Given an entity (`entity_ref`) and a list of reflected component information
@ -1325,12 +1352,16 @@ fn build_components_map<'a>(
Ok(serialized_components_map)
}
/// Given an entity (`entity_ref`) and list of reflected component information
/// (`paths_and_reflect_components`), return a map which associates each component to
/// a boolean value indicating whether or not that component is present on the entity.
/// Given an entity (`entity_ref`),
/// a list of reflected component information (`paths_and_reflect_components`)
/// and a list of unregistered components,
/// return a map which associates each component to a boolean value indicating
/// whether or not that component is present on the entity.
/// Unregistered components are considered absent from the entity.
fn build_has_map<'a>(
entity_ref: FilteredEntityRef,
paths_and_reflect_components: impl Iterator<Item = (&'a str, &'a ReflectComponent)>,
unregistered_components: &[String],
) -> HashMap<String, Value> {
let mut has_map = <HashMap<_, _>>::default();
@ -1338,6 +1369,9 @@ fn build_has_map<'a>(
let has = reflect_component.contains(entity_ref.clone());
has_map.insert(type_path.to_owned(), Value::Bool(has));
}
unregistered_components.iter().for_each(|component| {
has_map.insert(component.to_owned(), Value::Bool(false));
});
has_map
}