use std::{
borrow::Cow,
cmp::Ordering,
collections::{HashMap, HashSet},
fmt,
str,
};
use rbx_dom_weak::{RbxTree, RbxId, RbxInstanceProperties, RbxValue};
use serde::{Serialize, Deserialize};
use crate::{
path_map::PathMap,
rbx_session::MetadataPerInstance,
};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct InstanceChanges {
pub added: HashSet<RbxId>,
pub removed: HashSet<RbxId>,
pub updated: HashSet<RbxId>,
}
impl fmt::Display for InstanceChanges {
fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
writeln!(output, "InstanceChanges {{")?;
if !self.added.is_empty() {
writeln!(output, " Added:")?;
for id in &self.added {
writeln!(output, " {}", id)?;
}
}
if !self.removed.is_empty() {
writeln!(output, " Removed:")?;
for id in &self.removed {
writeln!(output, " {}", id)?;
}
}
if !self.updated.is_empty() {
writeln!(output, " Updated:")?;
for id in &self.updated {
writeln!(output, " {}", id)?;
}
}
writeln!(output, "}}")
}
}
impl InstanceChanges {
pub fn is_empty(&self) -> bool {
self.added.is_empty() && self.removed.is_empty() && self.updated.is_empty()
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct RbxSnapshotInstance<'a> {
pub name: Cow<'a, str>,
pub class_name: Cow<'a, str>,
pub properties: HashMap<String, RbxValue>,
pub children: Vec<RbxSnapshotInstance<'a>>,
pub metadata: MetadataPerInstance,
}
impl<'a> RbxSnapshotInstance<'a> {
pub fn get_owned(&'a self) -> RbxSnapshotInstance<'static> {
let children: Vec<RbxSnapshotInstance<'static>> = self.children.iter()
.map(RbxSnapshotInstance::get_owned)
.collect();
RbxSnapshotInstance {
name: Cow::Owned(self.name.clone().into_owned()),
class_name: Cow::Owned(self.class_name.clone().into_owned()),
properties: self.properties.clone(),
children,
metadata: self.metadata.clone(),
}
}
}
impl<'a> PartialOrd for RbxSnapshotInstance<'a> {
fn partial_cmp(&self, other: &RbxSnapshotInstance) -> Option<Ordering> {
Some(self.name.cmp(&other.name)
.then(self.class_name.cmp(&other.class_name)))
}
}
pub fn snapshot_from_tree(tree: &RbxTree, id: RbxId) -> Option<RbxSnapshotInstance<'static>> {
let instance = tree.get_instance(id)?;
let mut children = Vec::new();
for &child_id in instance.get_children_ids() {
children.push(snapshot_from_tree(tree, child_id)?);
}
Some(RbxSnapshotInstance {
name: Cow::Owned(instance.name.to_owned()),
class_name: Cow::Owned(instance.class_name.to_owned()),
properties: instance.properties.clone(),
children,
metadata: MetadataPerInstance {
source_path: None,
ignore_unknown_instances: false,
project_definition: None,
},
})
}
pub fn reify_root(
snapshot: &RbxSnapshotInstance,
instance_per_path: &mut PathMap<HashSet<RbxId>>,
metadata_per_instance: &mut HashMap<RbxId, MetadataPerInstance>,
changes: &mut InstanceChanges,
) -> RbxTree {
let instance = reify_core(snapshot);
let mut tree = RbxTree::new(instance);
let id = tree.get_root_id();
reify_metadata(snapshot, id, instance_per_path, metadata_per_instance);
changes.added.insert(id);
for child in &snapshot.children {
reify_subtree(child, &mut tree, id, instance_per_path, metadata_per_instance, changes);
}
tree
}
pub fn reify_subtree(
snapshot: &RbxSnapshotInstance,
tree: &mut RbxTree,
parent_id: RbxId,
instance_per_path: &mut PathMap<HashSet<RbxId>>,
metadata_per_instance: &mut HashMap<RbxId, MetadataPerInstance>,
changes: &mut InstanceChanges,
) -> RbxId {
let instance = reify_core(snapshot);
let id = tree.insert_instance(instance, parent_id);
reify_metadata(snapshot, id, instance_per_path, metadata_per_instance);
changes.added.insert(id);
for child in &snapshot.children {
reify_subtree(child, tree, id, instance_per_path, metadata_per_instance, changes);
}
id
}
fn reify_metadata(
snapshot: &RbxSnapshotInstance,
instance_id: RbxId,
instance_per_path: &mut PathMap<HashSet<RbxId>>,
metadata_per_instance: &mut HashMap<RbxId, MetadataPerInstance>,
) {
if let Some(source_path) = &snapshot.metadata.source_path {
let path_metadata = match instance_per_path.get_mut(&source_path) {
Some(v) => v,
None => {
instance_per_path.insert(source_path.clone(), Default::default());
instance_per_path.get_mut(&source_path).unwrap()
},
};
path_metadata.insert(instance_id);
}
metadata_per_instance.insert(instance_id, snapshot.metadata.clone());
}
pub fn reconcile_subtree(
tree: &mut RbxTree,
id: RbxId,
snapshot: &RbxSnapshotInstance,
instance_per_path: &mut PathMap<HashSet<RbxId>>,
metadata_per_instance: &mut HashMap<RbxId, MetadataPerInstance>,
changes: &mut InstanceChanges,
) {
reify_metadata(snapshot, id, instance_per_path, metadata_per_instance);
if reconcile_instance_properties(tree.get_instance_mut(id).unwrap(), snapshot) {
changes.updated.insert(id);
}
reconcile_instance_children(tree, id, snapshot, instance_per_path, metadata_per_instance, changes);
}
fn reify_core(snapshot: &RbxSnapshotInstance) -> RbxInstanceProperties {
let mut properties = HashMap::new();
for (key, value) in &snapshot.properties {
properties.insert(key.clone(), value.clone());
}
let instance = RbxInstanceProperties {
name: snapshot.name.to_string(),
class_name: snapshot.class_name.to_string(),
properties,
};
instance
}
fn reconcile_instance_properties(instance: &mut RbxInstanceProperties, snapshot: &RbxSnapshotInstance) -> bool {
let mut has_diffs = false;
if instance.name != snapshot.name {
instance.name = snapshot.name.to_string();
has_diffs = true;
}
if instance.class_name != snapshot.class_name {
instance.class_name = snapshot.class_name.to_string();
has_diffs = true;
}
let mut property_updates = HashMap::new();
for (key, instance_value) in &instance.properties {
match snapshot.properties.get(key) {
Some(snapshot_value) => {
if snapshot_value != instance_value {
property_updates.insert(key.clone(), Some(snapshot_value.clone()));
}
},
None => {
property_updates.insert(key.clone(), None);
},
}
}
for (key, snapshot_value) in &snapshot.properties {
if property_updates.contains_key(key) {
continue;
}
match instance.properties.get(key) {
Some(instance_value) => {
if snapshot_value != instance_value {
property_updates.insert(key.clone(), Some(snapshot_value.clone()));
}
},
None => {
property_updates.insert(key.clone(), Some(snapshot_value.clone()));
},
}
}
has_diffs = has_diffs || !property_updates.is_empty();
for (key, change) in property_updates.drain() {
match change {
Some(value) => instance.properties.insert(key, value),
None => instance.properties.remove(&key),
};
}
has_diffs
}
fn reconcile_instance_children(
tree: &mut RbxTree,
id: RbxId,
snapshot: &RbxSnapshotInstance,
instance_per_path: &mut PathMap<HashSet<RbxId>>,
metadata_per_instance: &mut HashMap<RbxId, MetadataPerInstance>,
changes: &mut InstanceChanges,
) {
let mut children_to_maybe_update: Vec<(RbxId, &RbxSnapshotInstance)> = Vec::new();
let mut children_to_add: Vec<(usize, &RbxSnapshotInstance)> = Vec::new();
let mut children_to_remove: Vec<RbxId> = Vec::new();
let mut ids_to_snapshot_indices = HashMap::new();
let mut visited_snapshot_indices = vec![false; snapshot.children.len()];
let children_ids = tree.get_instance(id).unwrap().get_children_ids();
for &child_id in children_ids {
let child_instance = tree.get_instance(child_id).unwrap();
let mut matching_snapshot = None;
for (snapshot_index, child_snapshot) in snapshot.children.iter().enumerate() {
if visited_snapshot_indices[snapshot_index] {
continue;
}
if child_snapshot.name == child_instance.name {
ids_to_snapshot_indices.insert(child_id, snapshot_index);
visited_snapshot_indices[snapshot_index] = true;
matching_snapshot = Some(child_snapshot);
break;
}
}
match matching_snapshot {
Some(child_snapshot) => {
children_to_maybe_update.push((child_instance.get_id(), child_snapshot));
}
None => {
children_to_remove.push(child_instance.get_id());
}
}
}
for (snapshot_index, child_snapshot) in snapshot.children.iter().enumerate() {
if !visited_snapshot_indices[snapshot_index] {
children_to_add.push((snapshot_index, child_snapshot));
}
}
for child_id in &children_to_remove {
if let Some(subtree) = tree.remove_instance(*child_id) {
for id in subtree.iter_all_ids() {
metadata_per_instance.remove(&id);
changes.removed.insert(id);
}
}
}
for (snapshot_index, child_snapshot) in &children_to_add {
let id = reify_subtree(child_snapshot, tree, id, instance_per_path, metadata_per_instance, changes);
ids_to_snapshot_indices.insert(id, *snapshot_index);
}
for (child_id, child_snapshot) in &children_to_maybe_update {
reconcile_subtree(tree, *child_id, child_snapshot, instance_per_path, metadata_per_instance, changes);
}
let instance = tree.get_instance_mut(id).unwrap();
instance.sort_children_unstable_by_key(|id| ids_to_snapshot_indices.get(&id).unwrap());
}