Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,15 @@ serde = { version = "1.0", features = ["derive"] }
test-components = { path = "./test-components" }

[features]
default = ["brp", "presets"]
default = ["brp", "presets", "safelist"]
# Serve the Bevy schema via BRP
brp = ["dep:bevy_remote"]
# Enable the use of Default and user preset values when inserting Components in Blender
presets = ["brp"]
# Pre-configure a set of allowable crates and types to limit what can be used in Blender
safelist = ["brp"]
# Write a skein.manifest.json to disk and exit the Bevy application
write_manifest_and_exit = []

# Idiomatic Bevy code often triggers these lints, and the CI workflow treats them as errors.
# In some cases they may still signal poor code quality however, so consider commenting out these lines.
Expand Down
118 changes: 115 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#![doc = include_str!("../README.md")]
use bevy_app::{App, Plugin};
use bevy_app::{App, Plugin, Startup};
use bevy_ecs::{
name::Name,
observer::Trigger,
Expand All @@ -15,7 +15,7 @@ use bevy_gltf::{
use bevy_log::{error, trace};
use bevy_platform::collections::HashMap;
use bevy_reflect::{Reflect, serde::ReflectDeserializer};
use serde::de::DeserializeSeed;
use serde::{Deserialize, Serialize, de::DeserializeSeed};
use serde_json::Value;
use tracing::{instrument, warn};

Expand All @@ -29,6 +29,18 @@ use tracing::{instrument, warn};
#[cfg(feature = "presets")]
pub mod presets;

/// Safelists are used for providing a well-defined
/// Component API to artists using Blender. By
/// enabling this feature, you can limit the available
/// components Blender users will see.
///
/// Doing this in combination with putting all Blender
/// Components in one (or a few) crates, allows users
/// to have a high degree of control over the Blender
/// user experience.
#[cfg(feature = "safelist")]
pub mod safelist;

/// [`SkeinPlugin`] is the main plugin.
///
/// This will add Scene postprocessing which will
Expand All @@ -43,11 +55,35 @@ pub struct SkeinPlugin {
/// up BRP yourself
#[allow(dead_code)]
pub handle_brp: bool,
/// When the `write_manifest_and_exit` feature is enabled,
/// This value controls whether the currently running
/// application should actually write the skein
/// manifest file and exit.
///
/// By default this value is true. Which means when the
/// `write_manifest_and_exit` feature is enabled, the program
/// will write the file and exit by default.
///
/// If you want to be able to write the manifest from a
/// previously compiled build, such as when distributing a
/// test game binary, set this value to `false` and choose
/// your own adventure for how to configure it when you want
/// to. (one potential option is to do your own CLI argument
/// parsing)
pub write_manifest_and_exit: bool,
}

impl Default for SkeinPlugin {
fn default() -> Self {
Self { handle_brp: true }
Self {
handle_brp: true,
// default is true, because the feature is off by default,
// so turning the feature on should cause this to execute.
//
// use false if you want to be able to toggle this in a dev
// build that is being distributed as a binary.
write_manifest_and_exit: true,
}
}
}

Expand All @@ -56,6 +92,9 @@ impl Plugin for SkeinPlugin {
app.init_resource::<SkeinPresetRegistry>()
.add_observer(skein_processing);

#[cfg(feature = "write_manifest_and_exit")]
app.add_systems(Startup, write_manifest_and_exit);

#[cfg(all(
not(target_family = "wasm"),
feature = "brp"
Expand All @@ -73,6 +112,11 @@ impl Plugin for SkeinPlugin {
);
}

remote_plugin = remote_plugin.with_method(
safelist::BRP_REGISTRY_SCHEMA_METHOD,
safelist::export_registry_types,
);

app.add_plugins((
remote_plugin,
bevy_remote::http::RemoteHttpPlugin::default(),
Expand Down Expand Up @@ -275,3 +319,71 @@ fn skein_processing(
}
}
}

/// The format written to disk when using an "offline" registry
#[derive(Serialize, Deserialize)]
struct SkeinManifest {
/// manifest version. Only bumped if there's a breaking change in the data that we need to communicate to blender
version: usize,
/// the safelisted crates whose components will be shown in Blender; empty Vec is no filter.
crate_safelist: Vec<String>,
/// what version of bevy_skein was used to create this data?
created_using_bevy_skein_version: &'static str,
/// available presets/default values for components
presets: Option<serde_json::Value>,
/// type_reflection data
registry: serde_json::Value,
}

impl Default for SkeinManifest {
fn default() -> Self {
Self {
version: 1,
crate_safelist: Default::default(),
created_using_bevy_skein_version: env!(
"CARGO_PKG_VERSION"
),
presets: Default::default(),
registry: Default::default(),
}
}
}

#[cfg(feature = "write_manifest_and_exit")]
fn write_manifest_and_exit(
mut world: &mut bevy_ecs::world::World,
) -> bevy_ecs::error::Result {
use crate::safelist::export_registry_types;
use bevy_app::AppExit;
use bevy_ecs::{
event::EventWriter, system::SystemState,
};
let types_schema = world
.run_system_cached_with(
export_registry_types,
None,
)?
.map_err(|brp_error| format!("{:?}", brp_error))?;
let registry_save_path = std::path::Path::new(
"skein.manifest.registry.json",
);

let manifest = SkeinManifest {
crate_safelist: vec![],
presets: None,
registry: types_schema,
..Default::default()
};

let writer = std::fs::File::create(registry_save_path)?;
serde_json::to_writer_pretty(writer, &manifest)?;

let mut system_state: SystemState<
EventWriter<AppExit>,
> = SystemState::new(&mut world);

let mut exit_event = system_state.get_mut(&mut world);

exit_event.write(AppExit::Success);
Ok(())
}
195 changes: 195 additions & 0 deletions src/safelist.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
use bevy_ecs::{
reflect::AppTypeRegistry, system::In, world::World,
};
use bevy_platform::collections::{HashMap, HashSet};
use bevy_reflect::{TypeInfo, VariantInfo};
use bevy_remote::{
BrpError, BrpResult, error_codes,
schemas::json_schema::JsonSchemaBevyType,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;

/// The method path for a `bevy/registry/schema` request.
pub const BRP_REGISTRY_SCHEMA_METHOD: &str =
"skein/registry/schema";

/// Constraints that can be placed on a query to include or exclude
/// certain definitions.
#[derive(
Debug, Serialize, Deserialize, Clone, Default, PartialEq,
)]
pub struct BrpJsonSchemaQueryFilter {
/// The crate name of the type name of each component that must not be
/// present on the entity for it to be included in the results.
#[serde(
skip_serializing_if = "Vec::is_empty",
default
)]
pub safelist_types: Vec<String>,

/// The crate name of the type name of each component that must be present
/// on the entity for it to be included in the results.
#[serde(
skip_serializing_if = "Vec::is_empty",
default
)]
pub safelist_crates: Vec<String>,
}

/// Handles a `bevy/registry/schema` request (list all registry types in form of schema) coming from a client.
pub fn export_registry_types(
In(params): In<Option<Value>>,
world: &World,
) -> BrpResult {
let filter: BrpJsonSchemaQueryFilter = match params {
None => Default::default(),
Some(params) => parse(params)?,
};

let types = world.resource::<AppTypeRegistry>();
let types = types.read();
let mut deps = HashSet::new();
let schemas = types
.iter()
.map(|type_registration| (bevy_remote::schemas::json_schema::export_type(&type_registration), type_registration.type_info()))
.filter(|((_, schema), _)| {
if !filter.safelist_crates.is_empty() {
return schema
.crate_name
.as_ref()
.is_some_and(|crate_name| {
filter
.safelist_crates
.contains(&crate_name)
});
} else {
return true;
}
})
.inspect(|((_, _), type_info)| {
for dep in get_type_dependencies(&type_info) {
deps.insert(dep);
}
})
.map(|(v,_)| v)
.collect::<HashMap<String, JsonSchemaBevyType>>();

dbg!(deps);
dbg!(schemas.get("test_components::TeamMember"));

serde_json::to_value(schemas)
.map_err(BrpError::internal)
}

/// A helper function used to parse a `serde_json::Value`.
fn parse<T: for<'de> Deserialize<'de>>(
value: Value,
) -> Result<T, BrpError> {
serde_json::from_value(value).map_err(|err| BrpError {
code: error_codes::INVALID_PARAMS,
message: err.to_string(),
data: None,
})
}

fn get_type_dependencies(
type_info: &TypeInfo,
) -> Vec<&str> {
match type_info {
TypeInfo::Struct(info) => {
return info
.iter()
.flat_map(|field| match field.type_info() {
Some(next_type_info) => {
get_type_dependencies(
&next_type_info,
)
}
None => vec![],
})
.collect::<Vec<_>>();
}

TypeInfo::Enum(info) => {
let simple = info.iter().all(|variant| {
matches!(variant, VariantInfo::Unit(_))
});
if simple {
return vec![info.type_path()];
} else {
return info
.iter()
.flat_map(|variant| match variant {
VariantInfo::Struct(v) => {
return v
.iter()
.flat_map(|field| match field.type_info() {
Some(next_type_info) => {
get_type_dependencies(
&next_type_info,
)
}
None => vec![],
})
.collect::<Vec<_>>();
}
VariantInfo::Tuple(v) => return v
.iter()
.flat_map(|field| {
match field.type_info() {
Some(next_type_info) => {
get_type_dependencies(
&next_type_info,
)
}
None => vec![],
}
})
.collect::<Vec<_>>(),
VariantInfo::Unit(_) => vec![],
})
.collect::<Vec<_>>();
}
}
TypeInfo::TupleStruct(info) => {
return info
.iter()
.flat_map(|field| match field.type_info() {
Some(next_type_info) => {
get_type_dependencies(
&next_type_info,
)
}
None => vec![],
})
.collect::<Vec<_>>();
}
TypeInfo::List(info) => match info.item_info() {
Some(type_info) => vec![type_info.type_path()],
None => vec![],
},
TypeInfo::Array(info) => match info.item_info() {
Some(type_info) => vec![type_info.type_path()],
None => vec![],
},
TypeInfo::Map(_info) => {
// info.key_type
// info.value_type
return vec![];
}
TypeInfo::Tuple(info) => info
.iter()
.flat_map(|field| match field.type_info() {
Some(next_type_info) => {
get_type_dependencies(&next_type_info)
}
None => vec![],
})
.collect::<Vec<_>>(),
TypeInfo::Set(info) => vec![info.value_ty().path()],
TypeInfo::Opaque(info) => {
vec![info.type_path()]
}
}
}
5 changes: 4 additions & 1 deletion tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ fn test_skein_options() {
App::new()
.add_plugins((
MinimalPlugins,
SkeinPlugin { handle_brp: false },
SkeinPlugin {
handle_brp: false,
..default()
},
))
// immediately exit
.add_systems(
Expand Down