using UnityEngine; using UnityEditor; using System.Collections.Generic; using System.Linq; using System.Text; namespace GitMerge { /// /// One instance of this class represents one GameObject with relevance to the merge process. /// Holds all MergeActions that can be applied to the GameObject or its Components. /// Is considered as "merged" when all its MergeActions are "merged". /// public class GameObjectMergeActions { /// /// Reference to "our" version of the GameObject. /// public GameObject ours { private set; get; } /// /// Reference to "their" versoin of the GameObject. /// public GameObject theirs { private set; get; } private string name; public bool merged { private set; get; } public bool hasActions { get { return actions.Count > 0; } } /// /// All actions available for solving specific conflicts on the GameObject. /// private List actions; public GameObjectMergeActions(GameObject ours, GameObject theirs) { actions = new List(); this.ours = ours; this.theirs = theirs; GenerateName(); if(theirs && !ours) { actions.Add(new MergeActionNewGameObject(ours, theirs)); } if(ours && !theirs) { actions.Add(new MergeActionDeleteGameObject(ours, theirs)); } if(ours && theirs) { FindPropertyDifferences(); FindComponentDifferences(); } //Some Actions have a default and are merged from the beginning. //If all the others did was to add GameObjects, we're done with merging from the start. CheckIfMerged(); } /// /// Generate a title for this object /// private void GenerateName() { name = ""; if(ours) { name = "Your[" + GetPath(ours) + "]"; } if(theirs) { if(ours) { name += " vs. "; } name += "Their[" + GetPath(theirs) + "]"; } } /// /// Finds the differences between properties of the two GameObjects. /// That means the name, layer, tag... everything that's not part of a Component. Also, the parent. /// private void FindPropertyDifferences() { CheckForDifferentParents(); FindPropertyDifferences(ours, theirs); } /// /// Since parenting is quite special, here's some dedicated handling. /// private void CheckForDifferentParents() { var transform = ours.GetComponent(); var ourParent = transform.parent; var theirParent = theirs.GetComponent().parent; if(ourParent != theirParent) { actions.Add(new MergeActionParenting(transform, ourParent, theirParent)); } } /// /// Check for Components that one of the sides doesn't have, and/or for defferent values /// on Components. /// private void FindComponentDifferences() { var ourComponents = ours.GetComponents(); var theirComponents = theirs.GetComponents(); //Map "their" Components to their respective ids var theirDict = new Dictionary(); foreach(var theirComponent in theirComponents) { //Ignore null components if(theirComponent != null) { theirDict.Add(ObjectIDFinder.GetIdentifierFor(theirComponent), theirComponent); } } foreach(var ourComponent in ourComponents) { //Ignore null components if(ourComponent == null) continue; //Try to find "their" equivalent to our Components var id = ObjectIDFinder.GetIdentifierFor(ourComponent); Component theirComponent; theirDict.TryGetValue(id, out theirComponent); if(theirComponent) //Both Components exist { FindPropertyDifferences(ourComponent, theirComponent); //Remove "their" Component from the dict to only keep those new to us theirDict.Remove(id); } else //Component doesn't exist in their version, offer a deletion { actions.Add(new MergeActionDeleteComponent(ours, ourComponent)); } } //Everything left in the dict is a... foreach(var theirComponent in theirDict.Values) { //...new Component from them actions.Add(new MergeActionNewComponent(ours, theirComponent)); } } /// /// Find all the values different in "our" and "their" version of a component. /// private void FindPropertyDifferences(Object ourObject, Object theirObject) { var ourSerialized = new SerializedObject(ourObject); var theirSerialized = new SerializedObject(theirObject); var ourProperty = ourSerialized.GetIterator(); if(ourProperty.NextVisible(true)) { var theirProperty = theirSerialized.GetIterator(); theirProperty.NextVisible(true); while(ourProperty.NextVisible(false)) { theirProperty.NextVisible(false); if(ourObject is GameObject) { if(MergeManager.isMergingPrefab) { //If merging a prefab, ignore the gameobject name. if(ourProperty.GetPlainName() == "Name") { continue; } } } if(DifferentValues(ourProperty, theirProperty)) { //We found a difference, accordingly add a MergeAction actions.Add(new MergeActionChangeValues(ours, ourObject, ourProperty.Copy(), theirProperty.Copy())); } } } } /// /// Returns true when the two properties have different values, false otherwise. /// private static bool DifferentValues(SerializedProperty ourProperty, SerializedProperty theirProperty) { if(!ourProperty.IsRealArray()) { //Regular single-value property if(DifferentValuesFlat(ourProperty, theirProperty)) { return true; } } else { //Array property if(ourProperty.arraySize != theirProperty.arraySize) { return true; } var op = ourProperty.Copy(); var tp = theirProperty.Copy(); op.Next(true); op.Next(true); tp.Next(true); tp.Next(true); for(int i = 0; i < ourProperty.arraySize; ++i) { op.Next(false); tp.Next(false); if(DifferentValuesFlat(op, tp)) { return true; } } } return false; } private static bool DifferentValuesFlat(SerializedProperty ourProperty, SerializedProperty theirProperty) { var our = ourProperty.GetValue(true); var their = theirProperty.GetValue(true); return !object.Equals(our, their); } /// /// Get the path of a GameObject in the hierarchy. /// private static string GetPath(GameObject g) { var t = g.transform; var sb = new StringBuilder(t.name); while(t.parent != null) { t = t.parent; sb.Insert(0, t.name + "/"); } return sb.ToString(); } private void CheckIfMerged() { merged = actions.TrueForAll(action => action.merged); } /// /// Use "our" version for all conflicts. /// This is used on all GameObjectMergeActions objects when the merge is aborted. /// public void UseOurs() { foreach(var action in actions) { action.UseOurs(); } merged = true; } /// /// Use "their" version for all conflicts. /// public void UseTheirs() { foreach(var action in actions) { action.UseTheirs(); } merged = true; } //If the foldout is open private bool open; public void OnGUI() { if(open) { GUI.backgroundColor = new Color(0, 0, 0, .8f); } else { GUI.backgroundColor = merged ? new Color(0, .5f, 0, .8f) : new Color(.5f, 0, 0, .8f); } GUILayout.BeginVertical(Resources.styles.mergeActions); GUI.backgroundColor = Color.white; GUILayout.BeginHorizontal(); open = EditorGUILayout.Foldout(open, new GUIContent(name)); if(ours && GUILayout.Button("Focus", EditorStyles.miniButton, GUILayout.Width(100))) { //Highlight the instance of the prefab, not the prefab itself //Otherwise, "ours". var objectToHighlight = MergeManager.isMergingPrefab ? MergeManagerPrefab.ourPrefabInstance : ours; objectToHighlight.Highlight(); } GUILayout.EndHorizontal(); if(open) { //Display all merge actions. foreach(var action in actions) { if(action.OnGUIMerge()) { CheckIfMerged(); } } } else { GUILayout.BeginHorizontal(); if(GUILayout.Button("Use ours >>>", EditorStyles.miniButton)) { UseOurs(); } if(GUILayout.Button("<<< Use theirs", EditorStyles.miniButton)) { UseTheirs(); } GUILayout.EndHorizontal(); } //If "ours" is null, the GameObject doesn't exist in one of the versions. //Try to get a reference if the object exists in the current merging state. //If it exists, the new/gelete MergeAction will have a reference. if(!ours) { foreach(var action in actions) { ours = action.ours; } } GUILayout.EndVertical(); GUI.backgroundColor = Color.white; } } }