diff --git a/src/core/helpers/syncback.rs b/src/core/helpers/syncback.rs index af9a8ae..846fe78 100644 --- a/src/core/helpers/syncback.rs +++ b/src/core/helpers/syncback.rs @@ -1,14 +1,20 @@ use colored::Colorize; -use rbx_dom_weak::{ustr, HashMapExt, UstrMap}; +use rbx_dom_weak::{ + types::{Ref, Variant}, + ustr, HashMapExt, Ustr, UstrMap, +}; use std::path::{Path, PathBuf}; use uuid::Uuid; use crate::{ argon_error, argon_warn, config::Config, - core::meta::{Meta, SyncbackFilter}, + core::{ + meta::{Meta, SyncbackFilter}, + tree::Tree, + }, ext::PathExt, - resolution::UnresolvedValue, + resolution::{is_ref_property, UnresolvedValue}, vfs::Vfs, Properties, }; @@ -181,6 +187,72 @@ pub fn validate_properties(properties: Properties, filter: &SyncbackFilter) -> P } } +pub fn resolve_ref_properties(properties: &mut Properties, class: &str, anchor_dir: &Path, tree: &Tree) { + let ref_properties: Vec = properties + .keys() + .copied() + .filter(|property| is_ref_property(class, property)) + .collect(); + + for property in ref_properties { + let target = match properties.get(&property) { + Some(Variant::Ref(target)) => *target, + _ => continue, + }; + + if target == Ref::none() { + properties.remove(&property); + continue; + } + + match tree.get_meta(target).and_then(|meta| meta.source.anchor_dir()) { + Some(target_dir) => { + let relative = relative_path(anchor_dir, target_dir); + properties.insert(property, Variant::String(path_to_ref_string(&relative))); + } + None => { + argon_warn!( + "Failed to serialize Ref property {}.{}: target instance is outside of the synced tree", + class.bold(), + property.bold() + ); + + properties.remove(&property); + } + } + } +} + +fn relative_path(from: &Path, to: &Path) -> PathBuf { + let from: Vec<_> = from.components().collect(); + let to: Vec<_> = to.components().collect(); + + let common = from.iter().zip(to.iter()).take_while(|(a, b)| a == b).count(); + + let mut result = PathBuf::new(); + + for _ in common..from.len() { + result.push(".."); + } + + for component in &to[common..] { + result.push(component.as_os_str()); + } + + if result.as_os_str().is_empty() { + result.push("."); + } + + result +} + +fn path_to_ref_string(path: &Path) -> String { + path.components() + .map(|component| component.as_os_str().to_string_lossy()) + .collect::>() + .join("/") +} + pub fn serialize_properties(class: &str, properties: Properties) -> UstrMap { properties .iter() @@ -200,3 +272,101 @@ pub fn rename_path(path: &Path, from: &str, to: &str) -> PathBuf { path.get_name().strip_prefix(from).unwrap_or_default() )) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::{ + meta::{Meta, Source}, + snapshot::Snapshot, + tree::Tree, + }; + + #[test] + fn resolve_ref_properties_computes_relative_path() { + let root_path = Path::new("/project/src/Model"); + let part_a_path = root_path.join("PartA"); + let part_b_path = root_path.join("PartB"); + + let mut root_meta = Meta::new(); + root_meta.set_source(Source::directory(root_path)); + + let mut part_a_meta = Meta::new(); + part_a_meta.set_source(Source::directory(&part_a_path)); + + let mut part_b_meta = Meta::new(); + part_b_meta.set_source(Source::directory(&part_b_path)); + + let snapshot = Snapshot::new() + .with_class("Model") + .with_name("Model") + .with_meta(root_meta) + .with_children(vec![ + Snapshot::new() + .with_class("Weld") + .with_name("PartA") + .with_meta(part_a_meta), + Snapshot::new() + .with_class("Folder") + .with_name("PartB") + .with_meta(part_b_meta), + ]); + + let tree = Tree::new(snapshot); + + let part_b_id = tree.root().children()[1]; + + let mut properties = Properties::default(); + properties.insert(ustr("Part0"), Variant::Ref(part_b_id)); + + resolve_ref_properties(&mut properties, "Weld", &part_a_path, &tree); + + assert_eq!( + properties.get(&ustr("Part0")), + Some(&Variant::String("../PartB".to_owned())) + ); + } + + #[test] + fn resolve_ref_properties_removes_unset_ref() { + let mut properties = Properties::default(); + properties.insert(ustr("Part0"), Variant::Ref(Ref::none())); + + let snapshot = Snapshot::new().with_class("Model").with_name("Model"); + let tree = Tree::new(snapshot); + + resolve_ref_properties(&mut properties, "Weld", Path::new("/project/src/Model"), &tree); + + assert!(!properties.contains_key(&ustr("Part0"))); + } + + #[test] + fn relative_path_siblings() { + let from = Path::new("/project/src/Hitbox"); + let to = Path::new("/project/src/Welds"); + + assert_eq!(relative_path(from, to), Path::new("../Welds")); + } + + #[test] + fn relative_path_descendant() { + let from = Path::new("/project/src/Model"); + let to = Path::new("/project/src/Model/Hitbox"); + + assert_eq!(relative_path(from, to), Path::new("Hitbox")); + } + + #[test] + fn relative_path_same_dir() { + let path = Path::new("/project/src/Model"); + + assert_eq!(relative_path(path, path), Path::new(".")); + } + + #[test] + fn path_to_ref_string_uses_forward_slashes() { + let relative = Path::new("..").join("Welds").join("Weld"); + + assert_eq!(path_to_ref_string(&relative), "../Welds/Weld"); + } +} diff --git a/src/core/meta.rs b/src/core/meta.rs index 9d5df43..19ff539 100644 --- a/src/core/meta.rs +++ b/src/core/meta.rs @@ -1,5 +1,7 @@ +use rbx_dom_weak::Ustr; use serde::{Deserialize, Serialize}; use std::{ + collections::HashMap, fmt::Display, path::{Path, PathBuf}, }; @@ -208,6 +210,18 @@ impl Source { pub fn paths(&self) -> Vec<&Path> { self.relevant.iter().map(|entry| entry.path()).collect() } + + pub fn anchor_dir(&self) -> Option<&Path> { + if let Some(SourceEntry::Folder(path)) = self + .relevant + .iter() + .find(|entry| matches!(entry, SourceEntry::Folder(_))) + { + return Some(path); + } + + self.inner.path().and_then(Path::parent) + } } impl Default for Source { @@ -488,6 +502,9 @@ pub struct Meta { pub original_name: Option, /// Custom Mesh Part source path pub mesh_source: Option, + /// `Ref` properties pending resolution to relative paths -> `Variant::Ref` + #[serde(skip)] + pub pending_refs: HashMap, } impl Meta { @@ -500,6 +517,7 @@ impl Meta { keep_unknowns: false, original_name: None, mesh_source: None, + pending_refs: HashMap::new(), } } diff --git a/src/core/mod.rs b/src/core/mod.rs index e781bbb..89daabb 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -56,7 +56,11 @@ impl Core { trace!("Building Tree and Queue"); let vfs = Arc::new(vfs); - let tree = Arc::new(Mutex::new(Tree::new(snapshot))); + + let mut tree = Tree::new(snapshot); + tree.resolve_refs(); + + let tree = Arc::new(Mutex::new(tree)); let queue = Arc::new(Queue::new()); trace!("Starting Processor"); diff --git a/src/core/processor/mod.rs b/src/core/processor/mod.rs index 8e12b3e..8809110 100644 --- a/src/core/processor/mod.rs +++ b/src/core/processor/mod.rs @@ -123,6 +123,8 @@ impl Handler { } } + tree.resolve_refs(); + changes }; diff --git a/src/core/processor/read.rs b/src/core/processor/read.rs index b1630d9..8965269 100644 --- a/src/core/processor/read.rs +++ b/src/core/processor/read.rs @@ -1,7 +1,7 @@ -use std::path::Path; +use std::{collections::HashMap, path::Path}; use log::{error, trace}; -use rbx_dom_weak::types::Ref; +use rbx_dom_weak::types::{Ref, Variant}; use crate::{ core::{ @@ -156,11 +156,45 @@ fn process_child_changes(id: Ref, mut snapshot: Snapshot, changes: &mut Changes, } fn insert_children(snapshot: &mut Snapshot, parent: Ref, tree: &mut Tree) { + let mut ref_map = HashMap::new(); + + insert_children_recursive(snapshot, parent, tree, &mut ref_map); + + if !ref_map.is_empty() { + remap_inserted_refs(snapshot, &ref_map, tree); + } +} + +fn insert_children_recursive(snapshot: &mut Snapshot, parent: Ref, tree: &mut Tree, ref_map: &mut HashMap) { + let placeholder = snapshot.ref_id; + let id = tree.insert_instance(snapshot.clone(), parent); snapshot.set_id(id); + if placeholder.is_some() { + ref_map.insert(placeholder, id); + } + + for child in snapshot.children.iter_mut() { + insert_children_recursive(child, id, tree, ref_map); + } +} + +fn remap_inserted_refs(snapshot: &mut Snapshot, ref_map: &HashMap, tree: &mut Tree) { + for value in snapshot.properties.values_mut() { + if let Variant::Ref(reference) = value { + if let Some(&mapped) = ref_map.get(reference) { + *value = Variant::Ref(mapped); + } + } + } + + if let Some(instance) = tree.get_instance_mut(snapshot.id) { + instance.properties.clone_from(&snapshot.properties); + } + for child in snapshot.children.iter_mut() { - insert_children(child, id, tree); + remap_inserted_refs(child, ref_map, tree); } } diff --git a/src/core/processor/write.rs b/src/core/processor/write.rs index d491dd9..48ee324 100644 --- a/src/core/processor/write.rs +++ b/src/core/processor/write.rs @@ -7,7 +7,9 @@ use std::path::{Path, PathBuf}; use crate::{ config::Config, core::{ - helpers::syncback::{rename_path, serialize_properties, validate_properties, verify_name, verify_path}, + helpers::syncback::{ + rename_path, resolve_ref_properties, serialize_properties, validate_properties, verify_name, verify_path, + }, meta::{Meta, NodePath, Source, SourceEntry, SourceKind}, snapshot::{AddedSnapshot, Snapshot, UpdatedSnapshot}, tree::Tree, @@ -72,6 +74,7 @@ pub fn apply_addition(snapshot: AddedSnapshot, tree: &mut Tree, vfs: &Vfs) -> Re path: &mut PathBuf, snapshot: &mut Snapshot, parent_meta: &Meta, + tree: &Tree, vfs: &Vfs, ) -> Result> { let mut meta = snapshot.meta.clone().with_context(&parent_meta.context); @@ -119,6 +122,10 @@ pub fn apply_addition(snapshot: AddedSnapshot, tree: &mut Tree, vfs: &Vfs) -> Re return Ok(None); } + if let Some(anchor_dir) = meta.source.anchor_dir() { + resolve_ref_properties(&mut properties, &snapshot.class, anchor_dir, tree); + } + let properties = middleware.write(properties, &file_path, vfs)?; let data_path = locate_instance_data(has_children, path, snapshot, parent_meta)?; @@ -142,6 +149,10 @@ pub fn apply_addition(snapshot: AddedSnapshot, tree: &mut Tree, vfs: &Vfs) -> Re meta.set_source(Source::directory(path)); + if let Some(anchor_dir) = meta.source.anchor_dir() { + resolve_ref_properties(&mut properties, &snapshot.class, anchor_dir, tree); + } + let data_path = locate_instance_data(true, path, snapshot, parent_meta)?; if filter.matches_path(&data_path) { @@ -233,12 +244,12 @@ pub fn apply_addition(snapshot: AddedSnapshot, tree: &mut Tree, vfs: &Vfs) -> Re let mut path = parent_path.join(&snapshot.name); if snapshot.children.is_empty() { - if let Some(meta) = write_instance(false, &mut path, &mut snapshot, parent_meta, vfs)? { + if let Some(meta) = write_instance(false, &mut path, &mut snapshot, parent_meta, tree, vfs)? { let snapshot = snapshot.with_meta(meta); tree.insert_instance_with_ref(snapshot, parent_id); } - } else if let Some(mut meta) = write_instance(true, &mut path, &mut snapshot, parent_meta, vfs)? { + } else if let Some(mut meta) = write_instance(true, &mut path, &mut snapshot, parent_meta, tree, vfs)? { let snapshot = snapshot.with_meta(meta.clone()); tree.insert_instance_with_ref(snapshot.clone(), parent_id); @@ -261,9 +272,15 @@ pub fn apply_addition(snapshot: AddedSnapshot, tree: &mut Tree, vfs: &Vfs) -> Re parent_meta: &Meta, tree: &mut Tree, ) { + let mut properties = snapshot.properties.clone(); + + if let Some(anchor_dir) = path.parent() { + resolve_ref_properties(&mut properties, &snapshot.class, anchor_dir, tree); + } + let mut node = ProjectNode { class_name: Some(snapshot.class), - properties: serialize_properties(&snapshot.class, snapshot.properties.clone()), + properties: serialize_properties(&snapshot.class, properties), ..ProjectNode::default() }; @@ -335,7 +352,7 @@ pub fn apply_addition(snapshot: AddedSnapshot, tree: &mut Tree, vfs: &Vfs) -> Re pub fn apply_update(snapshot: UpdatedSnapshot, tree: &mut Tree, vfs: &Vfs) -> Result<()> { trace!("Updating {:?}", snapshot.id); - if let Some(instance) = tree.get_instance(snapshot.id) { + let class = if let Some(instance) = tree.get_instance(snapshot.id) { let filter = tree.get_meta(snapshot.id).unwrap().context.syncback_filter(); if filter.matches_name(&instance.name) || filter.matches_class(&instance.class) { @@ -352,12 +369,21 @@ pub fn apply_update(snapshot: UpdatedSnapshot, tree: &mut Tree, vfs: &Vfs) -> Re filter_warn!(snapshot.id); return Ok(()); } + + instance.class } else { warn!("Attempted to update instance that doesn't exist: {:?}", snapshot.id); return Ok(()); - } + }; let mut meta = tree.get_meta(snapshot.id).unwrap().clone(); + let mut snapshot = snapshot; + + if let Some(properties) = snapshot.properties.as_mut() { + if let Some(anchor_dir) = meta.source.anchor_dir() { + resolve_ref_properties(properties, &class, anchor_dir, tree); + } + } let instance = tree.get_instance_mut(snapshot.id).unwrap(); fn locate_instance_data(name: &str, path: &Path, meta: &Meta, vfs: &Vfs) -> Option { diff --git a/src/core/snapshot.rs b/src/core/snapshot.rs index f64ea54..febfca6 100644 --- a/src/core/snapshot.rs +++ b/src/core/snapshot.rs @@ -11,6 +11,8 @@ use crate::{middleware::data::DataSnapshot, Properties}; #[derive(Clone, Serialize, Deserialize)] pub struct Snapshot { pub id: Ref, + #[serde(skip, default = "Ref::none")] + pub ref_id: Ref, pub meta: Meta, // Roblox related @@ -26,6 +28,7 @@ impl Snapshot { pub fn new() -> Self { Self { id: Ref::none(), + ref_id: Ref::none(), meta: Meta::new(), name: String::new(), class: Ustr::from("Folder"), @@ -115,6 +118,7 @@ impl Snapshot { } self.extend_properties(data.properties); + self.meta.pending_refs.extend(data.ref_properties); self.meta.source.add_data(&data.path); } @@ -203,6 +207,7 @@ impl From for Snapshot { fn from(snapshot: AddedSnapshot) -> Self { Self { id: snapshot.id, + ref_id: Ref::none(), meta: snapshot.meta, name: snapshot.name, class: snapshot.class, diff --git a/src/core/tree.rs b/src/core/tree.rs index ba78491..e631120 100644 --- a/src/core/tree.rs +++ b/src/core/tree.rs @@ -1,9 +1,12 @@ -use log::error; +use log::{error, warn}; use multimap::MultiMap; -use rbx_dom_weak::{types::Ref, Instance, InstanceBuilder, WeakDom}; +use rbx_dom_weak::{ + types::{Ref, Variant}, + Instance, InstanceBuilder, Ustr, WeakDom, +}; use std::{ collections::HashMap, - path::{Path, PathBuf}, + path::{Component, Path, PathBuf}, }; use super::{meta::Meta, snapshot::Snapshot}; @@ -50,22 +53,64 @@ impl Tree { id } - pub fn insert_instance_recursive(&mut self, snapshot: Snapshot, parent: Ref) -> Ref { - let builder = InstanceBuilder::new(snapshot.class) + pub fn insert_instance_recursive(&mut self, mut snapshot: Snapshot, parent: Ref) -> Ref { + let mut ref_map = HashMap::new(); + + let id = self.insert_instance_recursive_inner(&mut snapshot, parent, &mut ref_map); + + if !ref_map.is_empty() { + self.remap_inserted_refs(&snapshot, &ref_map); + } + + id + } + + fn insert_instance_recursive_inner( + &mut self, + snapshot: &mut Snapshot, + parent: Ref, + ref_map: &mut HashMap, + ) -> Ref { + let mut builder = InstanceBuilder::new(snapshot.class) .with_name(snapshot.meta.original_name.as_ref().unwrap_or(&snapshot.name)) - .with_properties(snapshot.properties); + .with_properties(snapshot.properties.clone()); + + if snapshot.id != Ref::none() { + builder = builder.with_referent(snapshot.id); + } let id = self.dom.insert(parent, builder); - self.insert_meta(id, snapshot.meta); + self.insert_meta(id, snapshot.meta.clone()); + snapshot.set_id(id); - for child in snapshot.children { - self.insert_instance_recursive(child, id); + if snapshot.ref_id.is_some() { + ref_map.insert(snapshot.ref_id, id); + } + + for child in snapshot.children.iter_mut() { + self.insert_instance_recursive_inner(child, id, ref_map); } id } + fn remap_inserted_refs(&mut self, snapshot: &Snapshot, ref_map: &HashMap) { + if let Some(instance) = self.dom.get_by_ref_mut(snapshot.id) { + for value in instance.properties.values_mut() { + if let Variant::Ref(reference) = value { + if let Some(&mapped) = ref_map.get(reference) { + *value = Variant::Ref(mapped); + } + } + } + } + + for child in &snapshot.children { + self.remap_inserted_refs(child, ref_map); + } + } + pub fn insert_instance_with_ref(&mut self, snapshot: Snapshot, parent: Ref) { let builder = InstanceBuilder::new(snapshot.class) .with_name(snapshot.meta.original_name.as_ref().unwrap_or(&snapshot.name)) @@ -175,6 +220,51 @@ impl Tree { self.path_to_ids.get_vec(path) } + pub fn resolve_refs(&mut self) { + let pending: Vec<(Ref, Ustr, String)> = self + .id_to_meta + .iter() + .flat_map(|(&id, meta)| { + meta.pending_refs + .iter() + .map(move |(&property, path)| (id, property, path.clone())) + }) + .collect(); + + for (id, property, relative_path) in pending { + let Some(meta) = self.id_to_meta.get(&id) else { + continue; + }; + + let Some(anchor) = meta.source.anchor_dir() else { + warn!("Failed to resolve Ref property {property}: instance has no anchor directory"); + continue; + }; + + let target_path = normalize_path(&anchor.join(&relative_path)); + + let target_id = self + .path_to_ids + .get_vec(&target_path) + .and_then(|ids| ids.first()) + .copied(); + + match target_id { + Some(target_id) => { + if let Some(instance) = self.dom.get_by_ref_mut(id) { + instance.properties.insert(property, Variant::Ref(target_id)); + } + } + None => { + warn!( + "Failed to resolve Ref property {property}: target '{relative_path}' not found relative to {}", + anchor.display() + ); + } + } + } + } + pub fn exists(&self, id: Ref) -> bool { self.dom.get_by_ref(id).is_some() } @@ -203,3 +293,63 @@ impl Tree { self.dom.root().children() } } + +fn normalize_path(path: &Path) -> PathBuf { + let mut components = Vec::new(); + + for component in path.components() { + match component { + Component::CurDir => {} + Component::ParentDir => { + if matches!(components.last(), Some(Component::Normal(_))) { + components.pop(); + } else { + components.push(component); + } + } + other => components.push(other), + } + } + + components.iter().collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::meta::Source; + + #[test] + fn resolve_refs_resolves_pending_ref() { + let root_path = Path::new("/project/src/Model"); + let hitbox_path = Path::new("/project/src/Model/Hitbox"); + + let mut root_meta = Meta::new(); + root_meta.set_source(Source::directory(root_path)); + root_meta + .pending_refs + .insert(Ustr::from("PrimaryPart"), "Hitbox".to_owned()); + + let mut hitbox_meta = Meta::new(); + hitbox_meta.set_source(Source::directory(hitbox_path)); + + let snapshot = Snapshot::new() + .with_class("Model") + .with_name("Model") + .with_meta(root_meta) + .with_children(vec![Snapshot::new() + .with_class("Folder") + .with_name("Hitbox") + .with_meta(hitbox_meta)]); + + let mut tree = Tree::new(snapshot); + tree.resolve_refs(); + + let hitbox_id = tree.root().children()[0]; + + assert_eq!( + tree.root().properties.get(&Ustr::from("PrimaryPart")), + Some(&Variant::Ref(hitbox_id)) + ); + } +} diff --git a/src/middleware/data.rs b/src/middleware/data.rs index 4b19461..eaa97af 100644 --- a/src/middleware/data.rs +++ b/src/middleware/data.rs @@ -11,7 +11,7 @@ use crate::{ core::meta::Meta, ext::PathExt, middleware::helpers, - resolution::UnresolvedValue, + resolution::{is_ref_property, UnresolvedValue}, util::{self, serialize_json}, vfs::Vfs, Properties, @@ -39,6 +39,7 @@ pub struct DataSnapshot { pub path: PathBuf, pub class: Option, pub properties: Properties, + pub ref_properties: HashMap, pub keep_unknowns: Option, pub original_name: Option, pub mesh_source: Option, @@ -55,6 +56,7 @@ pub fn read_data(path: &Path, class: Option<&str>, vfs: &Vfs) -> Result, vfs: &Vfs) -> Result {} + Some(path) => { + ref_properties.insert(property, path.to_owned()); + } + None => { + error!( + "Failed to parse property: {} at {} - Ref properties must be a relative path string", + property, + path.display() + ); + } + } + + continue; + } + match value.resolve(&class, &property) { Ok(value) => { properties.insert(property, value); @@ -113,6 +133,7 @@ pub fn read_data(path: &Path, class: Option<&str>, vfs: &Vfs) -> Result Snapshot { let (_, mut raw_dom) = dom.into_raw(); - fn walk(id: Ref, raw_dom: &mut AHashMap) -> Snapshot { + let mut instances: AHashMap = AHashMap::new(); + let mut ref_map: AHashMap = AHashMap::new(); + + fn collect( + id: Ref, + raw_dom: &mut AHashMap, + instances: &mut AHashMap, + ref_map: &mut AHashMap, + ) { let instance = raw_dom .remove(&id) .expect("Provided ID does not exist in the current DOM"); + ref_map.insert(id, Ref::new()); + + for &child_id in instance.children() { + collect(child_id, raw_dom, instances, ref_map); + } + + instances.insert(id, instance); + } + + collect(id, &mut raw_dom, &mut instances, &mut ref_map); + + fn build(id: Ref, instances: &mut AHashMap, ref_map: &AHashMap) -> Snapshot { + let mut instance = instances + .remove(&id) + .expect("Provided ID does not exist in the current DOM"); + + for value in instance.properties.values_mut() { + if let Variant::Ref(reference) = value { + *value = Variant::Ref(ref_map.get(reference).copied().unwrap_or_else(Ref::none)); + } + } + let children = instance .children() .iter() - .map(|&child_id| walk(child_id, raw_dom)) + .map(|&child_id| build(child_id, instances, ref_map)) .collect(); let mut meta = Meta::new(); @@ -23,13 +56,61 @@ pub fn snapshot_from_dom(dom: WeakDom, id: Ref) -> Snapshot { meta.set_mesh_source(super::save_mesh(&instance.properties)); } - Snapshot::new() + let mut snapshot = Snapshot::new() .with_meta(meta) .with_name(&instance.name) .with_class(&instance.class) .with_properties(instance.properties) - .with_children(children) + .with_children(children); + + snapshot.ref_id = ref_map[&id]; + snapshot } - walk(id, &mut raw_dom) + build(id, &mut instances, &ref_map) +} + +#[cfg(test)] +mod tests { + use rbx_dom_weak::{InstanceBuilder, Ustr}; + + use super::*; + + #[test] + fn remaps_internal_refs() { + let hitbox = InstanceBuilder::new("Part").with_name("Hitbox"); + let hitbox_ref = hitbox.referent(); + + let weld = InstanceBuilder::new("Weld") + .with_name("Weld") + .with_property("Part0", Variant::Ref(hitbox_ref)); + + let model = InstanceBuilder::new("Model") + .with_name("Model") + .with_property("PrimaryPart", Variant::Ref(hitbox_ref)) + .with_child(hitbox) + .with_child(weld); + + let model_ref = model.referent(); + + let dom = WeakDom::new(model); + + let snapshot = snapshot_from_dom(dom, model_ref); + + let hitbox_snapshot = snapshot.children.iter().find(|child| child.name == "Hitbox").unwrap(); + let weld_snapshot = snapshot.children.iter().find(|child| child.name == "Weld").unwrap(); + + assert_eq!( + snapshot.properties.get(&Ustr::from("PrimaryPart")), + Some(&Variant::Ref(hitbox_snapshot.ref_id)) + ); + assert_eq!( + weld_snapshot.properties.get(&Ustr::from("Part0")), + Some(&Variant::Ref(hitbox_snapshot.ref_id)) + ); + + assert_ne!(hitbox_snapshot.ref_id, hitbox_ref); + assert_ne!(snapshot.ref_id, model_ref); + assert_eq!(snapshot.id, Ref::none()); + } } diff --git a/src/middleware/json_model.rs b/src/middleware/json_model.rs index 300fe7b..ab693c8 100644 --- a/src/middleware/json_model.rs +++ b/src/middleware/json_model.rs @@ -5,7 +5,11 @@ use serde::Deserialize; use std::path::Path; use super::helpers; -use crate::{core::snapshot::Snapshot, resolution::UnresolvedValue, vfs::Vfs}; +use crate::{ + core::snapshot::Snapshot, + resolution::{is_ref_property, UnresolvedValue}, + vfs::Vfs, +}; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] @@ -56,6 +60,24 @@ fn walk(model: JsonModel, path: &Path) -> Result { // Resolve properties if let Some(model_properties) = model.properties { for (property, value) in model_properties { + if is_ref_property(&class, &property) { + match value.as_str() { + Some("") => {} + Some(ref_path) => { + snapshot.meta.pending_refs.insert(property, ref_path.to_owned()); + } + None => { + error!( + "Failed to parse property: {} at {} - Ref properties must be a relative path string", + property, + path.display() + ); + } + } + + continue; + } + match value.resolve(&class, &property) { Ok(value) => { properties.insert(property, value); diff --git a/src/resolution.rs b/src/resolution.rs index 2abe599..ec0a3bb 100644 --- a/src/resolution.rs +++ b/src/resolution.rs @@ -207,15 +207,15 @@ impl UnresolvedValue { [ray.direction.x as f64, ray.direction.y as f64, ray.direction.z as f64], ]), + Variant::Ref(_) => AmbiguousValue::String(String::new()), + Variant::Rect(rect) => AmbiguousValue::Array4([ rect.min.x as f64, rect.min.y as f64, rect.max.x as f64, rect.max.y as f64, ]), - // TODO: Implement Ref - // Variant::Ref(reference) => AmbiguousValue:: - // + Variant::Region3(region) => AmbiguousValue::Array3Array2([ [region.min.x as f64, region.min.y as f64, region.min.z as f64], [region.max.x as f64, region.max.y as f64, region.max.z as f64], @@ -477,9 +477,11 @@ impl AmbiguousValue { Vector2::new(rect[2] as f32, rect[3] as f32), ) .into()), - // TODO: Implement Ref - // (VariantType::Ref, AmbiguousValue::String(path)) => Ok(), - // + + (VariantType::Ref, _) => { + bail!("Ref properties must be a relative path string and are resolved separately") + } + (VariantType::Region3, AmbiguousValue::Array3Array2(region)) => Ok(Region3::new( Vector3::new(region[0][0] as f32, region[0][1] as f32, region[0][2] as f32), Vector3::new(region[1][0] as f32, region[1][1] as f32, region[1][2] as f32), @@ -564,6 +566,13 @@ impl AmbiguousValue { } } +pub fn is_ref_property(class: &str, property: &str) -> bool { + matches!( + find_descriptor(class, property).map(|descriptor| &descriptor.data_type), + Some(DataType::Value(VariantType::Ref)) + ) +} + fn find_descriptor(class: &str, property: &str) -> Option<&'static PropertyDescriptor<'static>> { let database = get_reflection_database(); let mut current_class = class;