diff --git a/Cargo.lock b/Cargo.lock index e16ddc59..1954a567 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -197,6 +197,7 @@ version = "0.1.0" dependencies = [ "bincode", "cimvr_derive_macros", + "kobble", "log", "once_cell", "serde", @@ -452,6 +453,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "dyn_edit_perms" +version = "0.1.0" +dependencies = [ + "cimvr_common", + "cimvr_engine_interface", + "serde", +] + [[package]] name = "ecs" version = "0.1.0" @@ -807,6 +817,15 @@ dependencies = [ "serde", ] +[[package]] +name = "kobble" +version = "0.1.0" +source = "git+https://github.com/ChatImproVR/Kobble.git#51c168dac16348f000c56b985cdcfe281c3074b8" +dependencies = [ + "once_cell", + "serde", +] + [[package]] name = "kqueue" version = "1.0.7" @@ -956,9 +975,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.16.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "particle-life" @@ -1014,9 +1033,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.52" +version = "1.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224" +checksum = "ba466839c78239c09faf015484e5cc04860f88242cff4d03eb038f04b4699b73" dependencies = [ "unicode-ident", ] @@ -1043,9 +1062,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.23" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "556d0f47a940e895261e77dc200d5eadfc6ef644c179c6f5edfc105e3a2292c8" dependencies = [ "proc-macro2", ] @@ -1279,9 +1298,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.109" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "09ee3a69cd2c7e06684677e5629b3878b253af05e4714964204279c6bc02cf0b" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 43ca74e8..9174a11d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ members = [ "example_plugins/gamepad", "example_plugins/keyboard", "example_plugins/chat", + "example_plugins/dyn_edit_perms", ] # Causes a build issue with OpenXR if included in workspace diff --git a/client/Cargo.lock b/client/Cargo.lock index 6d2a9eb8..d0000d33 100644 --- a/client/Cargo.lock +++ b/client/Cargo.lock @@ -275,6 +275,7 @@ version = "0.1.0" dependencies = [ "bincode", "cimvr_derive_macros", + "kobble", "log", "once_cell", "serde", @@ -1389,6 +1390,15 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "kobble" +version = "0.1.0" +source = "git+https://github.com/ChatImproVR/Kobble.git#51c168dac16348f000c56b985cdcfe281c3074b8" +dependencies = [ + "once_cell", + "serde", +] + [[package]] name = "kqueue" version = "1.0.7" @@ -1739,9 +1749,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "openxr" @@ -1886,9 +1896,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.52" +version = "1.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224" +checksum = "ba466839c78239c09faf015484e5cc04860f88242cff4d03eb038f04b4699b73" dependencies = [ "unicode-ident", ] diff --git a/client/src/component_ui.rs b/client/src/component_ui.rs new file mode 100644 index 00000000..3d6e2ac9 --- /dev/null +++ b/client/src/component_ui.rs @@ -0,0 +1,293 @@ +use cimvr_engine::{ + dyn_edit::extract_dyn, + interface::{ + component_id, + dyn_edit::{DynamicEditCommand, DynamicEditRequest}, + kobble::{DynamicValue, Schema, SchemaDeserializer}, + prelude::{Access, Component, ComponentId, EntityId, QueryComponent, Synchronized}, + serial::{deserialize, serialize, serialize_into}, + ComponentSchema, + }, + Engine, +}; +use egui::{Context, DragValue, ScrollArea, Ui, WidgetText}; +use std::collections::{HashMap, HashSet}; + +pub struct ComponentUi { + schema: HashMap, + selected: HashSet, + display: Vec, +} + +impl ComponentUi { + pub fn new(engine: &mut Engine) -> Self { + engine.subscribe::(); + Self { + schema: Default::default(), + selected: Default::default(), + display: Default::default(), + } + } + + pub fn run(&mut self, ctx: &Context, engine: &mut Engine) { + egui::SidePanel::left("ComponentUi").show(ctx, |ui| { + // Component selector + let mut needs_update = false; + ui.label("Components:"); + let mut sorted_keys: Vec = self.schema.keys().cloned().collect(); + sorted_keys.sort_by(|a, b| a.id.cmp(&b.id)); + for id in &sorted_keys { + let has_id = self.selected.contains(id); + let marker = if has_id { "[-] " } else { "" }; + let button = ui.button(format!("{}{}", marker, id.id)); + + if button.clicked() { + if has_id { + self.selected.remove(id); + } else { + self.selected.insert(id.clone()); + } + needs_update = true; + } + } + + if ui.button("Clear").clicked() { + self.selected.clear(); + needs_update = true; + } + ui.separator(); + + // Update displayed entities + // TODO: Actually update each frame? Just sort the ids. + // Might get a bit jittery with lots of plugins adding/removing entities... + if needs_update { + let query: Vec = self + .selected + .iter() + .map(|id| QueryComponent { + component: id.clone(), + access: Access::Write, + }) + .collect(); + + self.display = engine.ecs().query(&query).into_iter().collect(); + } + + // Component editor + let mut sorted_components: Vec = self.selected.iter().cloned().collect(); + sorted_components.sort_by(|a, b| a.id.cmp(&b.id)); + + ScrollArea::vertical().show(ui, |ui| { + for &entity in &self.display { + let EntityId(id_number) = entity; + + if ui.button(format!("Entity {:X}", id_number)).clicked() { + // Set the selected components equal to those on the given Entity, + // and which have useable schema + self.selected = engine + .ecs() + .all_components(entity) + .map(|(c, _)| c.clone()) + .filter(|c| self.schema.contains_key(c)) + .collect(); + // Display only the selected entity + self.display = vec![entity]; + return; + } + + for component in &sorted_components { + let schema = self.schema[component].clone(); + let Some(data) = engine.ecs().get_raw(entity, component) else { continue }; + + SchemaDeserializer::set_schema(schema); + if let Ok(SchemaDeserializer(mut dynamic)) = + deserialize(std::io::Cursor::new(data)) + { + ui.label(component_text_fmt(&component.id)); + + if editor(&mut dynamic, ui) { + // Create a dynamic edit + let mut edit = extract_dyn(engine.ecs(), entity); + + // Write new data into it + let Some(data) = edit.components.get_mut(component) else { continue }; + data.fill(0); + serialize_into(std::io::Cursor::new(data), &dynamic).unwrap(); + + // Request a dynamic edit locally + engine.send(DynamicEditCommand(edit.clone())); + + // If the synchronized component was attached, request a remote + // edit too + if edit.components.contains_key(&component_id::()) { + engine.send(DynamicEditRequest(edit.clone())); + } + } + } else { + ui.label(format!("Failed to deserialize {}", component.id)); + } + } + ui.separator(); + } + }) + }); + } + + pub fn update(&mut self, engine: &mut Engine) { + for msg in engine.inbox::() { + let ComponentSchema { id, schema } = msg; + self.schema.insert(id, schema); + } + } +} + +fn editor(value: &mut DynamicValue, ui: &mut Ui) -> bool { + match value { + DynamicValue::Unit => false, + DynamicValue::Bool(b) => ui.checkbox(b, "").clicked(), + DynamicValue::String(s) | DynamicValue::UnitStruct(s) => { + ui.label(s.clone()); + false + } + DynamicValue::NewtypeStruct(name, field) => { + if name == "GenericHandle" { + return false; + } + + if name.ends_with("Id") { + fn shorten(n: N, name: &str) -> String { + format!("{} ({})", name, &format!("{:06X}", n)[..6]) + } + match field.as_ref() { + DynamicValue::U8(v) => ui.label(shorten(v, name)), + DynamicValue::U16(v) => ui.label(shorten(v, name)), + DynamicValue::U32(v) => ui.label(shorten(v, name)), + DynamicValue::U64(v) => ui.label(shorten(v, name)), + DynamicValue::U128(v) => ui.label(shorten(v, name)), + _ => ui.label(name.clone()), + }; + return false; + } + + ui.horizontal(|ui| { + ui.label(name.clone()); + editor(field, ui) + }) + .inner + } + DynamicValue::UniformSequence(fields) | DynamicValue::Tuple(fields) => { + let mut changed = false; + for field_val in fields { + ui.horizontal(|ui| { + changed |= editor(field_val, ui); + }); + } + changed + } + DynamicValue::TupleStruct(name, fields) => { + if name == "Mat4" { + return edit_matrix(value, ui); + } + + if matches!(name.as_str(), "Vec3" | "Vec4" | "Quat") { + return edit_vector(value, ui); + } + + ui.label(name.clone()); + let mut changed = false; + for field_val in fields { + ui.horizontal(|ui| { + changed |= editor(field_val, ui); + }); + } + changed + } + DynamicValue::Struct { name, fields } => { + ui.label(name.clone()); + let mut changed = false; + for (name, field_val) in fields { + ui.horizontal(|ui| { + ui.label(name.clone()); + changed |= editor(field_val, ui); + }); + } + changed + } + DynamicValue::Enum(schema, sel_idx) => { + let mut changed = false; + ui.horizontal(|ui| { + for (idx, variant) in schema.variants.iter().enumerate() { + let clicked = ui.radio(idx == *sel_idx as usize, variant).clicked(); + changed |= clicked; + if clicked { + *sel_idx = idx as u32; + } + } + }); + changed + } + DynamicValue::I8(v) => ui.add(DragValue::new(v).speed(0.1)).changed(), + DynamicValue::U8(v) => ui.add(DragValue::new(v).speed(0.1)).changed(), + DynamicValue::I16(v) => ui.add(DragValue::new(v).speed(0.1)).changed(), + DynamicValue::U16(v) => ui.add(DragValue::new(v).speed(0.1)).changed(), + DynamicValue::I32(v) => ui.add(DragValue::new(v).speed(0.1)).changed(), + DynamicValue::U32(v) => ui.add(DragValue::new(v).speed(0.1)).changed(), + DynamicValue::I64(v) => ui.add(DragValue::new(v).speed(0.1)).changed(), + DynamicValue::U64(v) => ui.add(DragValue::new(v).speed(0.1)).changed(), + DynamicValue::I128(v) => { + ui.label(format!("{}", v)); + false + } + DynamicValue::U128(v) => { + ui.label(format!("{}", v)); + false + } + DynamicValue::Char(c) => { + ui.label(format!("{}", c)); + false + } + DynamicValue::F32(v) => ui.add(DragValue::new(v).speed(0.1)).changed(), + DynamicValue::F64(v) => ui.add(DragValue::new(v).speed(0.1)).changed(), + } +} + +fn edit_matrix(value: &mut DynamicValue, ui: &mut Ui) -> bool { + if let DynamicValue::TupleStruct(_, fields) = value { + let mut changed = false; + ui.vertical(|ui| { + for row in fields.chunks_exact_mut(4) { + ui.horizontal(|ui| { + for col in row { + if let DynamicValue::F32(v) = col { + changed |= ui.add(DragValue::new(v).speed(0.1)).changed(); + } + } + }); + } + }); + changed + } else { + false + } +} + +fn edit_vector(value: &mut DynamicValue, ui: &mut Ui) -> bool { + if let DynamicValue::TupleStruct(_, fields) = value { + let mut changed = false; + ui.horizontal(|ui| { + let names = ["x: ", "y: ", "z: ", "w: "]; + for (col, name) in fields.iter_mut().zip(names) { + if let DynamicValue::F32(v) = col { + changed |= ui.add(DragValue::new(v).prefix(name).speed(0.1)).changed(); + } + } + }); + changed + } else { + false + } +} + +fn component_text_fmt(name: &str) -> WidgetText { + WidgetText::from(name).strong() +} diff --git a/client/src/main.rs b/client/src/main.rs index a261330e..549b45f6 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -29,6 +29,8 @@ mod desktop_input; mod gamepad; mod render; mod ui; +mod component_ui; +mod plugin_ui; use structopt::StructOpt; diff --git a/client/src/plugin_ui.rs b/client/src/plugin_ui.rs new file mode 100644 index 00000000..1ad2a4e3 --- /dev/null +++ b/client/src/plugin_ui.rs @@ -0,0 +1,144 @@ +use std::collections::HashMap; + +use cimvr_common::ui::*; +use cimvr_engine::Engine; +use egui::{color_picker::color_edit_button_rgb, Context, DragValue, ScrollArea, TextEdit, Ui}; + +pub struct PluginUi { + elements: HashMap, +} + +struct Element { + name: String, + schema: Vec, + state: Vec, +} + +impl PluginUi { + pub fn new(engine: &mut Engine) -> Self { + engine.subscribe::(); + Self { + elements: HashMap::new(), + } + } + + pub fn run(&mut self, ctx: &Context, engine: &mut Engine) { + if self.elements.is_empty() { + return; + } + + egui::SidePanel::right("PluginUi").show(ctx, |ui| { + ScrollArea::vertical().show(ui, |ui| { + for (id, elem) in self.elements.iter_mut() { + ui.label(&elem.name); + if elem.show(ui) { + engine.send(UiUpdate { + id: *id, + state: elem.state.clone(), + }); + } + ui.add_space(10.); + } + }); + }); + } + + pub fn update(&mut self, engine: &mut Engine) { + // Process requests + for req in engine.inbox::() { + self.process_request(req); + } + + // Handle button declicks + for (id, elem) in &mut self.elements { + let mut any = false; + for state in &mut elem.state { + if let State::Button { clicked } = state { + if *clicked { + *clicked = false; + any = true; + } + } + } + + if any { + engine.send(UiUpdate { + id: *id, + state: elem.state.clone(), + }); + } + } + } + + fn process_request(&mut self, req: UiRequest) { + match req.op { + UiOperation::Create { + name, + schema, + init_state, + } => { + let elem = Element { + name, + schema, + state: init_state, + }; + if self.elements.insert(req.id, elem).is_some() { + log::trace!("Replaced Ui element {:?}", req.id) + } + } + UiOperation::Update(state) => { + if let Some(elem) = self.elements.get_mut(&req.id) { + elem.state = state; + } else { + log::error!("Failed to update invalid Ui element {:?}", req.id) + } + } + UiOperation::Delete => { + if self.elements.remove(&req.id).is_none() { + log::error!("Failed to remove invalid Ui element {:?}", req.id) + } + } + } + } +} + +impl Element { + /// Returns `true` if the given state updated + pub fn show(&mut self, ui: &mut Ui) -> bool { + let mut needs_update = false; + for (schema, state) in self.schema.iter().zip(&mut self.state) { + needs_update |= show(ui, schema, state); + } + needs_update + } +} + +fn show(ui: &mut Ui, schema: &Schema, state: &mut State) -> bool { + match (schema, state) { + (Schema::Label, State::Label { text }) => ui.label(text.to_owned()).changed(), + (Schema::TextInput, State::TextInput { text }) => { + ui.add(TextEdit::singleline(text)).changed() + } + (Schema::Button { text }, State::Button { clicked }) => { + *clicked = ui.button(text).clicked(); + *clicked + } + (Schema::DragValue { min, max }, State::DragValue { value }) => { + let range = min.unwrap_or(f32::MIN)..=max.unwrap_or(f32::MAX); + let dv = DragValue::new(value).clamp_range(range); + ui.add(dv).changed() + } + (Schema::ColorPicker, State::ColorPicker { rgb }) => { + color_edit_button_rgb(ui, rgb).changed() + } + (schema, state) => { + log::error!( + "Invalid UI schema and state combo: {:?} {:?}", + schema, + state + ); + false + } + } +} + diff --git a/client/src/ui.rs b/client/src/ui.rs index 94a4e08e..57b69217 100644 --- a/client/src/ui.rs +++ b/client/src/ui.rs @@ -4,140 +4,28 @@ use cimvr_common::ui::*; use cimvr_engine::Engine; use egui::{color_picker::color_edit_button_rgb, Context, DragValue, ScrollArea, TextEdit, Ui}; -pub struct OverlayUi { - elements: HashMap, -} +use crate::{component_ui::ComponentUi, plugin_ui::PluginUi}; -struct Element { - name: String, - schema: Vec, - state: Vec, +pub struct OverlayUi { + plugin_ui: PluginUi, + component_ui: ComponentUi, } impl OverlayUi { pub fn new(engine: &mut Engine) -> Self { - engine.subscribe::(); Self { - elements: HashMap::new(), + plugin_ui: PluginUi::new(engine), + component_ui: ComponentUi::new(engine), } } pub fn run(&mut self, ctx: &Context, engine: &mut Engine) { - if self.elements.is_empty() { - return; - } - - egui::SidePanel::left("my_side_panel").show(ctx, |ui| { - ScrollArea::vertical().show(ui, |ui| { - for (id, elem) in self.elements.iter_mut() { - ui.label(&elem.name); - if elem.show(ui) { - engine.send(UiUpdate { - id: *id, - state: elem.state.clone(), - }); - } - ui.add_space(10.); - } - }); - }); + self.plugin_ui.run(ctx, engine); + self.component_ui.run(ctx, engine); } pub fn update(&mut self, engine: &mut Engine) { - // Process requests - for req in engine.inbox::() { - self.process_request(req); - } - - // Handle button declicks - for (id, elem) in &mut self.elements { - let mut any = false; - for state in &mut elem.state { - if let State::Button { clicked } = state { - if *clicked { - *clicked = false; - any = true; - } - } - } - - if any { - engine.send(UiUpdate { - id: *id, - state: elem.state.clone(), - }); - } - } - } - - fn process_request(&mut self, req: UiRequest) { - match req.op { - UiOperation::Create { - name, - schema, - init_state, - } => { - let elem = Element { - name, - schema, - state: init_state, - }; - if self.elements.insert(req.id, elem).is_some() { - log::trace!("Replaced Ui element {:?}", req.id) - } - } - UiOperation::Update(state) => { - if let Some(elem) = self.elements.get_mut(&req.id) { - elem.state = state; - } else { - log::error!("Failed to update invalid Ui element {:?}", req.id) - } - } - UiOperation::Delete => { - if self.elements.remove(&req.id).is_none() { - log::error!("Failed to remove invalid Ui element {:?}", req.id) - } - } - } - } -} - -impl Element { - /// Returns `true` if the given state updated - pub fn show(&mut self, ui: &mut Ui) -> bool { - let mut needs_update = false; - for (schema, state) in self.schema.iter().zip(&mut self.state) { - needs_update |= show(ui, schema, state); - } - needs_update - } -} - -fn show(ui: &mut Ui, schema: &Schema, state: &mut State) -> bool { - match (schema, state) { - (Schema::Label, State::Label { text }) => ui.label(text.to_owned()).changed(), - (Schema::TextInput, State::TextInput { text }) => { - ui.add(TextEdit::singleline(text)).changed() - } - (Schema::Button { text }, State::Button { clicked }) => { - *clicked = ui.button(text).clicked(); - *clicked - } - (Schema::DragValue { min, max }, State::DragValue { value }) => { - let range = min.unwrap_or(f32::MIN)..=max.unwrap_or(f32::MAX); - let dv = DragValue::new(value).clamp_range(range); - ui.add(dv).changed() - } - (Schema::ColorPicker, State::ColorPicker { rgb }) => { - color_edit_button_rgb(ui, rgb).changed() - } - (schema, state) => { - log::error!( - "Invalid UI schema and state combo: {:?} {:?}", - schema, - state - ); - false - } + self.plugin_ui.update(engine); + self.component_ui.update(engine); } } diff --git a/engine/src/dyn_edit.rs b/engine/src/dyn_edit.rs new file mode 100644 index 00000000..013439a7 --- /dev/null +++ b/engine/src/dyn_edit.rs @@ -0,0 +1,45 @@ +use crate::{ecs::Ecs, Engine}; +use cimvr_engine_interface::{ + dyn_edit::{DynamicEdit, DynamicEditCommand}, + prelude::EntityId, +}; + +/// Extract a dynamic value from the ECS +pub fn extract_dyn(ecs: &Ecs, entity: EntityId) -> DynamicEdit { + DynamicEdit { + entity, + components: ecs + .all_components(entity) + .map(|(c, d)| (c.clone(), d.to_vec())) + .collect(), + } +} + +/// Insert a dynamic value into the ECS +pub fn insert_dyn(ecs: &mut Ecs, dynamic: &DynamicEdit) { + // Delete existing state + ecs.remove_entity(dynamic.entity); + ecs.import_entity(dynamic.entity); + + // Import components + for (comp_id, data) in &dynamic.components { + ecs.add_component_raw(dynamic.entity, comp_id, data); + } +} + +/// Dynamic edit command follower +pub struct DynamicEditor; + +impl DynamicEditor { + pub fn sub(engine: &mut Engine) -> Self { + engine.subscribe::(); + Self + } + + /// Receive update events and apply them to the engine + pub fn update(engine: &mut Engine) { + for DynamicEditCommand(edit) in engine.inbox().collect::>() { + insert_dyn(engine.ecs(), &edit); + } + } +} diff --git a/engine/src/ecs.rs b/engine/src/ecs.rs index dba89e74..41cfeae6 100644 --- a/engine/src/ecs.rs +++ b/engine/src/ecs.rs @@ -237,6 +237,13 @@ impl Ecs { } */ + /// Get each component and its associated data on an entity + pub fn all_components(&self, entity: EntityId) -> impl Iterator { + self.map + .iter() + .filter_map(move |(comp, entities)| Some((comp, entities.get(&entity)?.as_slice()))) + } + pub fn export(&mut self, query: &[QueryComponent]) -> EcsMap { let entities: Vec = self.query(query).into_iter().collect(); diff --git a/engine/src/lib.rs b/engine/src/lib.rs index 241d93bb..93e1f976 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -1,8 +1,10 @@ +pub mod dyn_edit; pub mod ecs; pub mod hotload; pub mod network; pub mod plugin; pub mod timing; +use dyn_edit::DynamicEditor; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, path::PathBuf}; use timing::Timing; @@ -43,6 +45,8 @@ pub struct Engine { cfg: Config, /// Manages FrameTime time: Timing, + /// Dynamic value synchronization + dyn_edit: DynamicEditor, } /// Plugin management structure @@ -98,7 +102,8 @@ impl Engine { let ecs = Ecs::new(); - Ok(Self { + let mut inst = Self { + dyn_edit: DynamicEditor, time, wasm, indices: HashMap::new(), @@ -107,7 +112,11 @@ impl Engine { external_inbox: HashMap::new(), network_inbox: vec![], cfg, - }) + }; + + DynamicEditor::sub(&mut inst); + + Ok(inst) } /// Initialize plugin code. Must be called at least once! @@ -181,6 +190,11 @@ impl Engine { self.dispatch_plugin(stage, i)?; } + // TODO: ditto responsibility of something else + // OOP sucks why do I use it + // Synchronize dynamic edits + DynamicEditor::update(self); + // Distribute messages self.propagate(); diff --git a/engine_interface/Cargo.toml b/engine_interface/Cargo.toml index 6e42d058..f865a60e 100644 --- a/engine_interface/Cargo.toml +++ b/engine_interface/Cargo.toml @@ -10,5 +10,7 @@ serverside = [] cimvr_derive_macros = { path = "../engine_derive_macros" } bincode = "1.3.3" serde = { version = "1", features = ["derive"] } -once_cell = "1.16.0" +once_cell = "1.17.0" log = "0.4.17" +kobble = { git = "https://github.com/ChatImproVR/Kobble.git" } +#kobble = { path = "../../kobble" } diff --git a/engine_interface/src/dyn_edit.rs b/engine_interface/src/dyn_edit.rs new file mode 100644 index 00000000..c98dc7b9 --- /dev/null +++ b/engine_interface/src/dyn_edit.rs @@ -0,0 +1,30 @@ +use std::collections::HashMap; + +use crate::pkg_namespace; +use crate::prelude::*; +use serde::{Deserialize, Serialize}; + +// TODO: Add metadata for edit requests, so that servers can intelligently filter... + +/// A dynamic edit request sent to the server +/// Emitted by e.g. the component GUI to change entities containing the Synchronized component. +#[derive(Message, Serialize, Deserialize, Clone)] +#[locality("Remote")] +pub struct DynamicEditRequest(pub DynamicEdit); + +/// A dynamic edit command sent to the engine +/// Emitted by e.g. the component GUI to locally change entities +#[derive(Message, Serialize, Deserialize, Clone)] +#[locality("Local")] +pub struct DynamicEditCommand(pub DynamicEdit); + +/// A dynamic edit operation +#[derive(Serialize, Deserialize, Clone)] +pub struct DynamicEdit { + /// Target entity + pub entity: EntityId, + // TODO: This is a potentially harmful thing to expose. + // We should provide a wrapper around it! + /// Full component data state of this entity + pub components: HashMap>, +} diff --git a/engine_interface/src/lib.rs b/engine_interface/src/lib.rs index 2043d521..a4c718d1 100644 --- a/engine_interface/src/lib.rs +++ b/engine_interface/src/lib.rs @@ -7,6 +7,7 @@ /// Code specific to WASM plugins pub mod plugin; +pub use kobble; use std::{cell::RefCell, collections::HashMap}; mod component_validate_error; pub mod component_validation; @@ -33,6 +34,9 @@ pub mod network; /// PCG algorithm for generating random universally-unique entity IDs pub mod pcg; +/// Dynamic editing feedback to server +pub mod dyn_edit; + /// Convenience imports for the lazy // #[macro_use] pub mod prelude { @@ -104,34 +108,23 @@ fn validate_component(c: &C) { } } -/// Component size cache -pub(crate) struct SizeCache(HashMap<&'static str, usize>); - -thread_local! { - /// Thread local component size cache - static SIZE_CACHE: RefCell> = RefCell::new(Lazy::new(|| SizeCache::new())); -} - -impl SizeCache { - pub fn new() -> Self { - SizeCache(HashMap::new()) - } - - /// Get the size in bytse of the given component - #[track_caller] - pub fn size(&mut self) -> usize { - let SizeCache(map) = self; - *map.entry(C::ID) - .or_insert_with(|| max_component_size::()) - } -} - /// Get the size of a component #[track_caller] pub fn component_size_cached() -> usize { - SIZE_CACHE.with(|cache| cache.borrow_mut().size::()) + thread_local! { + /// Thread local component size cache + static SIZE_CACHE: RefCell>> + = RefCell::new(Lazy::new(|| HashMap::new())); + } + SIZE_CACHE.with(|cache| { + *cache + .borrow_mut() + .entry(C::ID) + .or_insert_with(|| max_component_size::()) + }) } +// TODO: This should be a method of Component! /// Get the ComponentId of a Component pub fn component_id() -> ComponentId { let size = component_size_cached::() @@ -142,3 +135,17 @@ pub fn component_id() -> ComponentId { size, } } + +/// Component schema information +#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct ComponentSchema { + pub id: ComponentId, + pub schema: kobble::Schema, +} + +impl Message for ComponentSchema { + const CHANNEL: ChannelIdStatic = ChannelIdStatic { + id: pkg_namespace!("ComponentSchema"), + locality: Locality::Local, + }; +} diff --git a/engine_interface/src/plugin.rs b/engine_interface/src/plugin.rs index b0f68676..d1c0ea8e 100644 --- a/engine_interface/src/plugin.rs +++ b/engine_interface/src/plugin.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use crate::{ component_id, pcg::Pcg, @@ -5,6 +7,7 @@ use crate::{ serial::{ deserialize, serialize, serialize_into, serialized_size, EcsData, ReceiveBuf, SendBuf, }, + ComponentSchema, }; pub use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; @@ -110,6 +113,8 @@ pub struct EngineIo { pub(crate) outbox: Vec, /// Inbox pub(crate) inbox: Inbox, + /// Schema which have been sent already + pub(crate) schema_set: HashSet, } /// Scheduling of systems @@ -293,6 +298,7 @@ impl EngineIo { pcg: Pcg::new(), outbox: vec![], inbox, + schema_set: Default::default(), } } @@ -315,8 +321,24 @@ impl EngineIo { pub fn add_component(&mut self, entity: EntityId, data: C) { let data = serialize(&data).expect("Failed to serialize component data"); + let id = component_id::(); + + // Send schema information to host on first use + if !self.schema_set.contains(&id) { + match kobble::record_schema::() { + Ok(schema) => { + self.send(&ComponentSchema { + id: id.clone(), + schema, + }); + self.schema_set.insert(id.clone()); + } + Err(e) => crate::println!("Error generating schema for {}; {:?}", id.id, e), + } + } + self.commands - .push(EcsCommand::AddComponent(entity, component_id::(), data)); + .push(EcsCommand::AddComponent(entity, id, data)); } /// Delete an entity and all of it's components diff --git a/example_plugins/cube/src/lib.rs b/example_plugins/cube/src/lib.rs index 491edec9..8690e015 100644 --- a/example_plugins/cube/src/lib.rs +++ b/example_plugins/cube/src/lib.rs @@ -1,5 +1,5 @@ use cimvr_common::{ - render::{Mesh, MeshHandle, Primitive, Render, UploadMesh, Vertex}, + render::{Mesh, MeshHandle, Primitive, Render, RenderExtra, UploadMesh, Vertex}, Transform, }; use cimvr_engine_interface::{make_app_state, pkg_namespace, prelude::*}; @@ -31,16 +31,14 @@ impl UserState for ClientState { impl UserState for ServerState { fn new(io: &mut EngineIo, _sched: &mut EngineSchedule) -> Self { // Create an entity - let _cube_ent = io - .create_entity() + io.create_entity() // Attach a Transform component (which defaults to the origin) .add_component(Transform::default()) // Attach the Render component, which details how the object should be drawn // Note that we use CUBE_HANDLE here, to tell the rendering engine to draw the cube - .add_component(Render::new(CUBE_HANDLE).primitive(Primitive::Triangles)) + .add_component(Render::new(CUBE_HANDLE)) // Attach the Synchronized component, which will copy the object to clients .add_component(Synchronized) - // And get the entity ID .build(); Self diff --git a/example_plugins/dyn_edit_perms/.cargo/config.toml b/example_plugins/dyn_edit_perms/.cargo/config.toml new file mode 100644 index 00000000..fd1f2bee --- /dev/null +++ b/example_plugins/dyn_edit_perms/.cargo/config.toml @@ -0,0 +1,5 @@ +[build] +target = "wasm32-unknown-unknown" + +[alias] +test_pc = "test --target=x86_64-unknown-linux-gnu" diff --git a/example_plugins/dyn_edit_perms/.gitignore b/example_plugins/dyn_edit_perms/.gitignore new file mode 100644 index 00000000..4fffb2f8 --- /dev/null +++ b/example_plugins/dyn_edit_perms/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/example_plugins/dyn_edit_perms/Cargo.toml b/example_plugins/dyn_edit_perms/Cargo.toml new file mode 100644 index 00000000..10442107 --- /dev/null +++ b/example_plugins/dyn_edit_perms/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "dyn_edit_perms" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +cimvr_common = { path = "../../common" } +cimvr_engine_interface = { path = "../../engine_interface" } +serde = { version = "1", features = ["derive"] } diff --git a/example_plugins/dyn_edit_perms/src/lib.rs b/example_plugins/dyn_edit_perms/src/lib.rs new file mode 100644 index 00000000..6e5ba782 --- /dev/null +++ b/example_plugins/dyn_edit_perms/src/lib.rs @@ -0,0 +1,67 @@ +use cimvr_engine_interface::{ + dbg, + dyn_edit::{DynamicEditCommand, DynamicEditRequest}, + make_app_state, pkg_namespace, + prelude::*, + println, ComponentSchema, +}; +use serde::{Deserialize, Serialize}; + +struct ServerState; + +impl UserState for ServerState { + fn new(_io: &mut EngineIo, sched: &mut EngineSchedule) -> Self { + sched + .add_system(Self::update) + .subscribe::() + .subscribe::() + .build(); + Self + } +} + +impl ServerState { + fn update(&mut self, io: &mut EngineIo, _query: &mut QueryResult) { + // Automatically forward all edit requests into edit commands. + // Here you might filter by username, permissions, etc. + for DynamicEditRequest(edit) in io.inbox().collect::>() { + io.send(&DynamicEditCommand(edit)); + } + + // The second purpose of this plugin: + // Receive component schema server-side, and forward them + // client-side for display/editing + for component_schema in io.inbox::().collect::>() { + io.send(&ComponentSchemaDownload(component_schema)); + } + } +} + +#[derive(Message, Serialize, Deserialize, Debug)] +#[locality("Remote")] +struct ComponentSchemaDownload(ComponentSchema); + +struct ClientState; + +impl UserState for ClientState { + fn new(_io: &mut EngineIo, sched: &mut EngineSchedule) -> Self { + sched + .add_system(Self::update) + .subscribe::() + .build(); + Self + } +} + +impl ClientState { + fn update(&mut self, io: &mut EngineIo, _query: &mut QueryResult) { + // Download component schema data from server and make it available client-side. + for ComponentSchemaDownload(component_schema) in + io.inbox::().collect::>() + { + io.send(&component_schema); + } + } +} + +make_app_state!(ClientState, ServerState); diff --git a/example_plugins/ui_example/src/lib.rs b/example_plugins/ui_example/src/lib.rs index 7c5bb483..73c96a01 100644 --- a/example_plugins/ui_example/src/lib.rs +++ b/example_plugins/ui_example/src/lib.rs @@ -1,5 +1,3 @@ -//! UI Example -//! This example is intended to be run with other plugins such as `cube` or `fluid_sim` use cimvr_engine_interface::{make_app_state, pkg_namespace, prelude::*}; use serde::{Deserialize, Serialize}; @@ -10,8 +8,14 @@ use server::ServerState; make_app_state!(ClientState, ServerState); -#[derive(Message, Clone, Debug, Serialize, Deserialize)] -#[locality("Remote")] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct ChangeColor { rgb: [f32; 3], } + +impl Message for ChangeColor { + const CHANNEL: ChannelIdStatic = ChannelIdStatic { + id: pkg_namespace!("ChangeColor"), + locality: Locality::Remote, + }; +} diff --git a/example_plugins/ui_example/src/server.rs b/example_plugins/ui_example/src/server.rs index 378439e1..e8f9c624 100644 --- a/example_plugins/ui_example/src/server.rs +++ b/example_plugins/ui_example/src/server.rs @@ -23,13 +23,11 @@ impl UserState for ServerState { impl ServerState { fn update(&mut self, io: &mut EngineIo, query: &mut QueryResult) { if let Some(ChangeColor { rgb }) = io.inbox_first() { - for ent in query.iter() { - // The default shader uses RenderExtra to set the color + for entity in query.iter() { let mut extra = [0.; 4 * 4]; extra[..3].copy_from_slice(&rgb); - // This value must be 1 to get the color to show. See the default vertex shader! extra[3] = 1.; - io.add_component(ent, RenderExtra(extra)); + io.add_component(entity, RenderExtra(extra)); } } }