Skip to main content

librojo/snapshot/
tree.rs

1use std::{
2    collections::{HashMap, VecDeque},
3    path::{Path, PathBuf},
4};
5
6use rbx_dom_weak::{
7    types::{Ref, Variant},
8    ustr, Instance, InstanceBuilder, Ustr, UstrMap, WeakDom,
9};
10
11use crate::{multimap::MultiMap, RojoRef};
12
13use super::{InstanceMetadata, InstanceSnapshot};
14
15/// An expanded variant of rbx_dom_weak's `WeakDom` that tracks additional
16/// metadata per instance that's Rojo-specific.
17///
18/// This tree is also optimized for doing fast incremental updates and patches.
19#[derive(Debug)]
20pub struct RojoTree {
21    /// Contains the instances without their Rojo-specific metadata.
22    inner: WeakDom,
23
24    /// Metadata associated with each instance that is kept up-to-date with the
25    /// set of actual instances.
26    metadata_map: HashMap<Ref, InstanceMetadata>,
27
28    /// A multimap from source paths to all of the root instances that were
29    /// constructed from that path.
30    ///
31    /// Descendants of those instances should not be contained in the set, the
32    /// value portion of the map is also a set in order to support the same path
33    /// appearing multiple times in the same Rojo project. This is sometimes
34    /// called "path aliasing" in various Rojo documentation.
35    path_to_ids: MultiMap<PathBuf, Ref>,
36
37    /// A map of specified RojoRefs to underlying Refs they represent.
38    /// This field is a MultiMap to allow for the possibility of the user specifying
39    /// the same RojoRef for multiple different instances. An entry containing
40    /// multiple elements is an error condition that should be raised to the user.
41    specified_id_to_refs: MultiMap<RojoRef, Ref>,
42}
43
44impl RojoTree {
45    pub fn new(snapshot: InstanceSnapshot) -> RojoTree {
46        let root_builder = InstanceBuilder::new(snapshot.class_name)
47            .with_name(snapshot.name)
48            .with_properties(snapshot.properties);
49
50        let mut tree = RojoTree {
51            inner: WeakDom::new(root_builder),
52            metadata_map: HashMap::new(),
53            path_to_ids: MultiMap::new(),
54            specified_id_to_refs: MultiMap::new(),
55        };
56
57        let root_ref = tree.inner.root_ref();
58
59        tree.insert_metadata(root_ref, snapshot.metadata);
60
61        for child in snapshot.children {
62            tree.insert_instance(root_ref, child);
63        }
64
65        tree
66    }
67
68    pub fn inner(&self) -> &WeakDom {
69        &self.inner
70    }
71
72    pub fn get_root_id(&self) -> Ref {
73        self.inner.root_ref()
74    }
75
76    /// Returns the root Instance of this tree.
77    #[inline]
78    pub fn root(&self) -> InstanceWithMeta<'_> {
79        self.get_instance(self.get_root_id())
80            .expect("RojoTrees should have a root")
81    }
82
83    pub fn get_instance(&self, id: Ref) -> Option<InstanceWithMeta<'_>> {
84        if let Some(instance) = self.inner.get_by_ref(id) {
85            let metadata = self.metadata_map.get(&id).unwrap();
86
87            Some(InstanceWithMeta { instance, metadata })
88        } else {
89            None
90        }
91    }
92
93    pub fn get_instance_mut(&mut self, id: Ref) -> Option<InstanceWithMetaMut<'_>> {
94        if let Some(instance) = self.inner.get_by_ref_mut(id) {
95            let metadata = self.metadata_map.get_mut(&id).unwrap();
96
97            Some(InstanceWithMetaMut { instance, metadata })
98        } else {
99            None
100        }
101    }
102
103    pub fn insert_instance(&mut self, parent_ref: Ref, snapshot: InstanceSnapshot) -> Ref {
104        // !!!!!!!!!! UGLY HACK !!!!!!!!!!
105        // ! If you are going to change this, go change it in patch_compute/compute_property_patches too
106        //
107        // This is a set of special cases working around a more general problem upstream
108        // in rbx-dom that causes pivots to not build to file correctly, described in
109        // github.com/rojo-rbx/rojo/issues/628 and github.com/rojo-rbx/rbx-dom/issues/385
110        //
111        // We need to insert the NeedsPivotMigration property with a value of false on
112        // every instance that inherits from Model for pivots to build correctly.
113        let hack_needs_pivot_migration = match snapshot.class_name.as_ref() {
114            // This is not a future proof way to do this but the last time a
115            // descendant of Model was added was in 2020 so it's probably fine.
116            "Model" | "Actor" | "Tool" | "HopperBin" | "Flag" | "WorldModel" | "Workspace"
117            | "Status"
118                if !snapshot
119                    .properties
120                    .contains_key(&ustr("NeedsPivotMigration")) =>
121            {
122                vec![("NeedsPivotMigration", Variant::Bool(false))]
123            }
124            _ => Vec::new(),
125        };
126
127        let builder = InstanceBuilder::empty()
128            .with_class(snapshot.class_name)
129            .with_name(snapshot.name.into_owned())
130            .with_properties(hack_needs_pivot_migration)
131            .with_properties(snapshot.properties);
132
133        let referent = self.inner.insert(parent_ref, builder);
134        self.insert_metadata(referent, snapshot.metadata);
135
136        for child in snapshot.children {
137            self.insert_instance(referent, child);
138        }
139
140        referent
141    }
142
143    pub fn remove(&mut self, id: Ref) {
144        let mut to_move = VecDeque::new();
145        to_move.push_back(id);
146
147        while let Some(id) = to_move.pop_front() {
148            self.remove_metadata(id);
149
150            if let Some(instance) = self.inner.get_by_ref(id) {
151                to_move.extend(instance.children().iter().copied());
152            }
153        }
154
155        self.inner.destroy(id);
156    }
157
158    /// Replaces the metadata associated with the given instance ID.
159    pub fn update_metadata(&mut self, id: Ref, metadata: InstanceMetadata) {
160        use std::collections::hash_map::Entry;
161
162        match self.metadata_map.entry(id) {
163            Entry::Occupied(mut entry) => {
164                let existing_metadata = entry.get();
165
166                // If this instance's source path changed, we need to update our
167                // path associations so that file changes will trigger updates
168                // to this instance correctly.
169                if existing_metadata.relevant_paths != metadata.relevant_paths {
170                    for existing_path in &existing_metadata.relevant_paths {
171                        self.path_to_ids.remove(existing_path, id);
172                    }
173
174                    for new_path in &metadata.relevant_paths {
175                        self.path_to_ids.insert(new_path.clone(), id);
176                    }
177                }
178                if existing_metadata.specified_id != metadata.specified_id {
179                    // We need to uphold the invariant that each ID can only map
180                    // to one referent.
181                    if let Some(new) = &metadata.specified_id {
182                        if !self.specified_id_to_refs.get(new).is_empty() {
183                            log::error!("Duplicate user-specified referent '{new}'");
184                        }
185
186                        self.specified_id_to_refs.insert(new.clone(), id);
187                    }
188                    if let Some(old) = &existing_metadata.specified_id {
189                        self.specified_id_to_refs.remove(old, id);
190                    }
191                }
192
193                entry.insert(metadata);
194            }
195            Entry::Vacant(entry) => {
196                entry.insert(metadata);
197            }
198        }
199    }
200
201    pub fn descendants(&self, id: Ref) -> RojoDescendants<'_> {
202        let mut queue = VecDeque::new();
203        queue.push_back(id);
204
205        RojoDescendants { queue, tree: self }
206    }
207
208    pub fn get_ids_at_path(&self, path: &Path) -> &[Ref] {
209        self.path_to_ids.get(path)
210    }
211
212    pub fn get_metadata(&self, id: Ref) -> Option<&InstanceMetadata> {
213        self.metadata_map.get(&id)
214    }
215
216    /// Get the backing Ref of the given RojoRef. If the RojoRef maps to exactly
217    /// one Ref, this method returns Some. Otherwise, it returns None.
218    pub fn get_specified_id(&self, specified: &RojoRef) -> Option<Ref> {
219        match self.specified_id_to_refs.get(specified)[..] {
220            [referent] => Some(referent),
221            _ => None,
222        }
223    }
224
225    pub fn set_specified_id(&mut self, id: Ref, specified: RojoRef) {
226        if let Some(metadata) = self.metadata_map.get_mut(&id) {
227            if let Some(old) = metadata.specified_id.replace(specified.clone()) {
228                self.specified_id_to_refs.remove(&old, id);
229            }
230        }
231        self.specified_id_to_refs.insert(specified, id);
232    }
233
234    fn insert_metadata(&mut self, id: Ref, metadata: InstanceMetadata) {
235        for path in &metadata.relevant_paths {
236            self.path_to_ids.insert(path.clone(), id);
237        }
238
239        if let Some(specified_id) = &metadata.specified_id {
240            if !self.specified_id_to_refs.get(specified_id).is_empty() {
241                log::error!("Duplicate user-specified referent '{specified_id}'");
242            }
243
244            self.set_specified_id(id, specified_id.clone());
245        }
246
247        self.metadata_map.insert(id, metadata);
248    }
249
250    /// Moves the Rojo metadata from the instance with the given ID from this
251    /// tree into some loose maps.
252    fn remove_metadata(&mut self, id: Ref) {
253        let metadata = self.metadata_map.remove(&id).unwrap();
254
255        if let Some(specified) = metadata.specified_id {
256            self.specified_id_to_refs.remove(&specified, id);
257        }
258
259        for path in &metadata.relevant_paths {
260            self.path_to_ids.remove(path, id);
261        }
262    }
263}
264
265pub struct RojoDescendants<'a> {
266    queue: VecDeque<Ref>,
267    tree: &'a RojoTree,
268}
269
270impl<'a> Iterator for RojoDescendants<'a> {
271    type Item = InstanceWithMeta<'a>;
272
273    fn next(&mut self) -> Option<Self::Item> {
274        let id = self.queue.pop_front()?;
275
276        let instance = self
277            .tree
278            .inner
279            .get_by_ref(id)
280            .expect("Instance did not exist");
281
282        let metadata = self
283            .tree
284            .get_metadata(instance.referent())
285            .expect("Metadata did not exist for instance");
286
287        self.queue.extend(instance.children().iter().copied());
288
289        Some(InstanceWithMeta { instance, metadata })
290    }
291}
292
293/// RojoTree's equivalent of `&'a Instance`.
294///
295/// This has to be a value type for RojoTree because the instance and metadata
296/// are stored in different places. The mutable equivalent is
297/// `InstanceWithMetaMut`.
298#[derive(Debug, Clone, Copy)]
299pub struct InstanceWithMeta<'a> {
300    instance: &'a Instance,
301    metadata: &'a InstanceMetadata,
302}
303
304impl<'a> InstanceWithMeta<'a> {
305    pub fn id(&self) -> Ref {
306        self.instance.referent()
307    }
308
309    pub fn parent(&self) -> Ref {
310        self.instance.parent()
311    }
312
313    pub fn name(&self) -> &'a str {
314        &self.instance.name
315    }
316
317    pub fn class_name(&self) -> Ustr {
318        self.instance.class
319    }
320
321    pub fn properties(&self) -> &'a UstrMap<Variant> {
322        &self.instance.properties
323    }
324
325    pub fn children(&self) -> &'a [Ref] {
326        self.instance.children()
327    }
328
329    pub fn metadata(&self) -> &'a InstanceMetadata {
330        self.metadata
331    }
332
333    pub fn inner(&self) -> &Instance {
334        self.instance
335    }
336}
337
338/// RojoTree's equivalent of `&'a mut Instance`.
339///
340/// This has to be a value type for RojoTree because the instance and metadata
341/// are stored in different places. The immutable equivalent is
342/// `InstanceWithMeta`.
343#[derive(Debug)]
344pub struct InstanceWithMetaMut<'a> {
345    instance: &'a mut Instance,
346    metadata: &'a mut InstanceMetadata,
347}
348
349impl InstanceWithMetaMut<'_> {
350    pub fn id(&self) -> Ref {
351        self.instance.referent()
352    }
353
354    pub fn name(&self) -> &str {
355        &self.instance.name
356    }
357
358    pub fn name_mut(&mut self) -> &mut String {
359        &mut self.instance.name
360    }
361
362    pub fn class_name(&self) -> &str {
363        &self.instance.class
364    }
365
366    pub fn set_class_name<'a, S: Into<&'a str>>(&mut self, new_class: S) {
367        self.instance.class = ustr(new_class.into());
368    }
369
370    pub fn properties(&self) -> &UstrMap<Variant> {
371        &self.instance.properties
372    }
373
374    pub fn properties_mut(&mut self) -> &mut UstrMap<Variant> {
375        &mut self.instance.properties
376    }
377
378    pub fn children(&self) -> &[Ref] {
379        self.instance.children()
380    }
381
382    pub fn metadata(&self) -> &InstanceMetadata {
383        self.metadata
384    }
385
386    pub fn inner(&self) -> &Instance {
387        self.instance
388    }
389
390    pub fn inner_mut(&mut self) -> &mut Instance {
391        self.instance
392    }
393}
394
395#[cfg(test)]
396mod test {
397    use crate::{
398        snapshot::{InstanceMetadata, InstanceSnapshot},
399        RojoRef,
400    };
401
402    use super::RojoTree;
403
404    #[test]
405    fn swap_duped_specified_ids() {
406        let custom_ref = RojoRef::new("MyCoolRef".into());
407        let snapshot = InstanceSnapshot::new()
408            .metadata(InstanceMetadata::new().specified_id(Some(custom_ref.clone())));
409        let mut tree = RojoTree::new(InstanceSnapshot::new());
410
411        let original = tree.insert_instance(tree.get_root_id(), snapshot.clone());
412        assert_eq!(tree.get_specified_id(&custom_ref.clone()), Some(original));
413
414        let duped = tree.insert_instance(tree.get_root_id(), snapshot.clone());
415        assert_eq!(tree.get_specified_id(&custom_ref.clone()), None);
416
417        tree.remove(original);
418        assert_eq!(tree.get_specified_id(&custom_ref.clone()), Some(duped));
419    }
420}