//! Defines the algorithm for computing a roughly-minimal patch set given an //! existing instance tree and an instance snapshot. use std::{ collections::{HashMap, HashSet}, mem::take, }; use rbx_dom_weak::types::{Ref, Variant}; use crate::{RojoRef, REF_POINTER_ATTRIBUTE_PREFIX}; use super::{ patch::{PatchAdd, PatchSet, PatchUpdate}, InstanceSnapshot, InstanceWithMeta, RojoTree, }; #[profiling::function] pub fn compute_patch_set(snapshot: Option, tree: &RojoTree, id: Ref) -> PatchSet { let mut patch_set = PatchSet::new(); if let Some(snapshot) = snapshot { let mut context = ComputePatchContext::default(); compute_patch_set_internal(&mut context, snapshot, tree, id, &mut patch_set); // Rewrite Ref properties to refer to instance IDs instead of snapshot IDs // for all of the IDs that we know about so far. rewrite_refs_in_updates(&context, &mut patch_set.updated_instances); rewrite_refs_in_additions(&context, &mut patch_set.added_instances); } else if id != tree.get_root_id() { patch_set.removed_instances.push(id); } patch_set } #[derive(Default)] struct ComputePatchContext { snapshot_id_to_instance_id: HashMap, } fn rewrite_refs_in_updates(context: &ComputePatchContext, updates: &mut [PatchUpdate]) { for update in updates { for property_value in update.changed_properties.values_mut() { if let Some(Variant::Ref(referent)) = property_value { if let Some(&instance_ref) = context.snapshot_id_to_instance_id.get(referent) { *property_value = Some(Variant::Ref(instance_ref)); } } } } } fn rewrite_refs_in_additions(context: &ComputePatchContext, additions: &mut [PatchAdd]) { for addition in additions { rewrite_refs_in_snapshot(context, &mut addition.instance); } } fn rewrite_refs_in_snapshot(context: &ComputePatchContext, snapshot: &mut InstanceSnapshot) { for property_value in snapshot.properties.values_mut() { if let Variant::Ref(referent) = property_value { if let Some(&instance_referent) = context.snapshot_id_to_instance_id.get(referent) { *property_value = Variant::Ref(instance_referent); } } } for child in &mut snapshot.children { rewrite_refs_in_snapshot(context, child); } } fn compute_patch_set_internal( context: &mut ComputePatchContext, mut snapshot: InstanceSnapshot, tree: &RojoTree, id: Ref, patch_set: &mut PatchSet, ) { if snapshot.snapshot_id.is_some() { context .snapshot_id_to_instance_id .insert(snapshot.snapshot_id, id); } let instance = tree .get_instance(id) .expect("Instance did not exist in tree"); compute_property_patches(&mut snapshot, &instance, patch_set, tree); compute_children_patches(context, &mut snapshot, tree, id, patch_set); } fn compute_property_patches( snapshot: &mut InstanceSnapshot, instance: &InstanceWithMeta, patch_set: &mut PatchSet, tree: &RojoTree, ) { let mut visited_properties = HashSet::new(); let mut changed_properties = HashMap::new(); let attribute_ref_properties = compute_ref_properties(snapshot, tree); let changed_name = if snapshot.name == instance.name() { None } else { Some(take(&mut snapshot.name).into_owned()) }; let changed_class_name = if snapshot.class_name == instance.class_name() { None } else { Some(take(&mut snapshot.class_name).into_owned()) }; let changed_metadata = if &snapshot.metadata == instance.metadata() { None } else { Some(take(&mut snapshot.metadata)) }; for (name, snapshot_value) in take(&mut snapshot.properties) { visited_properties.insert(name.clone()); match instance.properties().get(&name) { Some(instance_value) => { if &snapshot_value != instance_value { changed_properties.insert(name, Some(snapshot_value)); } } None => { changed_properties.insert(name, Some(snapshot_value)); } } } for name in instance.properties().keys() { if visited_properties.contains(name.as_str()) { continue; } changed_properties.insert(name.clone(), None); } for (name, ref_value) in attribute_ref_properties { match (&ref_value, instance.properties().get(&name)) { (Some(referent), Some(instance_value)) => { if referent != instance_value { changed_properties.insert(name, ref_value); } else { changed_properties.remove(&name); } } (Some(_), None) | (None, Some(_)) => { changed_properties.insert(name, ref_value); } (None, None) => { changed_properties.remove(&name); } } } if changed_properties.is_empty() && changed_name.is_none() && changed_class_name.is_none() && changed_metadata.is_none() { return; } patch_set.updated_instances.push(PatchUpdate { id: instance.id(), changed_name, changed_class_name, changed_properties, changed_metadata, }); } fn compute_children_patches( context: &mut ComputePatchContext, snapshot: &mut InstanceSnapshot, tree: &RojoTree, id: Ref, patch_set: &mut PatchSet, ) { let instance = tree .get_instance(id) .expect("Instance did not exist in tree"); let instance_children = instance.children(); let mut paired_instances = vec![false; instance_children.len()]; for snapshot_child in take(&mut snapshot.children) { let matching_instance = instance_children .iter() .enumerate() .find(|(instance_index, instance_child_id)| { if paired_instances[*instance_index] { return false; } let instance_child = tree .get_instance(**instance_child_id) .expect("Instance did not exist in tree"); if snapshot_child.name == instance_child.name() && snapshot_child.class_name == instance_child.class_name() { paired_instances[*instance_index] = true; return true; } false }); match matching_instance { Some((_, instance_child_id)) => { compute_patch_set_internal( context, snapshot_child, tree, *instance_child_id, patch_set, ); } None => { patch_set.added_instances.push(PatchAdd { parent_id: id, instance: snapshot_child, }); } } } for (instance_index, instance_child_id) in instance_children.iter().enumerate() { if paired_instances[instance_index] { continue; } patch_set.removed_instances.push(*instance_child_id); } } fn compute_ref_properties( snapshot: &InstanceSnapshot, tree: &RojoTree, ) -> HashMap> { let mut map = HashMap::new(); let attributes = match snapshot.properties.get("Attributes") { Some(Variant::Attributes(attrs)) => attrs, _ => return map, }; for (attr_name, attr_value) in attributes.iter() { let prop_name = match attr_name.strip_prefix(REF_POINTER_ATTRIBUTE_PREFIX) { Some(str) => str, None => continue, }; let rojo_ref = match attr_value { Variant::String(str) => RojoRef::new(str.clone()), Variant::BinaryString(bytes) => { if let Ok(str) = std::str::from_utf8(bytes.as_ref()) { RojoRef::new(str.to_string()) } else { log::warn!( "IDs specified by referent property attributes must be valid UTF-8 strings" ); continue; } } _ => { log::warn!( "Attribute {attr_name} is of type {:?} when it was \ expected to be a String", attr_value.ty() ); continue; } }; if let Some(target_id) = tree.get_specified_id(&rojo_ref) { map.insert(prop_name.to_string(), Some(Variant::Ref(target_id))); } else { map.insert(prop_name.to_string(), None); } } map } #[cfg(test)] mod test { use super::*; use std::borrow::Cow; /// This test makes sure that rewriting refs in instance update patches to /// instances that already exists works. We should be able to correlate the /// snapshot ID and instance ID during patch computation and replace the /// value before returning from compute_patch_set. #[test] fn rewrite_ref_existing_instance_update() { let tree = RojoTree::new(InstanceSnapshot::new().name("foo").class_name("foo")); let root_id = tree.get_root_id(); // This snapshot should be identical to the existing tree except for the // addition of a prop named Self, which is a self-referential Ref. let snapshot_id = Ref::new(); let snapshot = InstanceSnapshot { snapshot_id, properties: [("Self".to_owned(), Variant::Ref(snapshot_id))].into(), metadata: Default::default(), name: Cow::Borrowed("foo"), class_name: Cow::Borrowed("foo"), children: Vec::new(), }; let patch_set = compute_patch_set(Some(snapshot), &tree, root_id); let expected_patch_set = PatchSet { updated_instances: vec![PatchUpdate { id: root_id, changed_name: None, changed_class_name: None, changed_properties: [("Self".to_owned(), Some(Variant::Ref(root_id)))].into(), changed_metadata: None, }], added_instances: Vec::new(), removed_instances: Vec::new(), }; assert_eq!(patch_set, expected_patch_set); } /// The same as rewrite_ref_existing_instance_update, except that the /// property is added in a new instance instead of modifying an existing /// one. #[test] fn rewrite_ref_existing_instance_addition() { let tree = RojoTree::new(InstanceSnapshot::new().name("foo").class_name("foo")); let root_id = tree.get_root_id(); // This patch describes the existing instance with a new child added. let snapshot_id = Ref::new(); let snapshot = InstanceSnapshot { snapshot_id, children: vec![InstanceSnapshot { properties: [("Self".to_owned(), Variant::Ref(snapshot_id))].into(), snapshot_id: Ref::none(), metadata: Default::default(), name: Cow::Borrowed("child"), class_name: Cow::Borrowed("child"), children: Vec::new(), }], metadata: Default::default(), properties: HashMap::new(), name: Cow::Borrowed("foo"), class_name: Cow::Borrowed("foo"), }; let patch_set = compute_patch_set(Some(snapshot), &tree, root_id); let expected_patch_set = PatchSet { added_instances: vec![PatchAdd { parent_id: root_id, instance: InstanceSnapshot { snapshot_id: Ref::none(), metadata: Default::default(), properties: [("Self".to_owned(), Variant::Ref(root_id))].into(), name: Cow::Borrowed("child"), class_name: Cow::Borrowed("child"), children: Vec::new(), }, }], updated_instances: Vec::new(), removed_instances: Vec::new(), }; assert_eq!(patch_set, expected_patch_set); } }