diff --git a/.gitattributes b/.gitattributes
index 0630300756a..6292d191fab 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -34,6 +34,7 @@
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
+Resources/Maps/**.yml merge=mapping-merge-driver
###############################################################################
# behavior for image files
diff --git a/.gitignore b/.gitignore
index 05b654564fa..98164b71c99 100644
--- a/.gitignore
+++ b/.gitignore
@@ -284,3 +284,7 @@ BuildFiles/Windows/Godot/*
# Windows image file caches
Thumbs.db
ehthumbs.db
+
+# Merge driver stuff
+Content.Tools/test/out.yml
+
diff --git a/Content.Tools/Content.Tools.csproj b/Content.Tools/Content.Tools.csproj
new file mode 100644
index 00000000000..e3a5e3bca9a
--- /dev/null
+++ b/Content.Tools/Content.Tools.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net5.0
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Tools/Map.cs b/Content.Tools/Map.cs
new file mode 100644
index 00000000000..6e6ed592976
--- /dev/null
+++ b/Content.Tools/Map.cs
@@ -0,0 +1,83 @@
+using System.IO;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using YamlDotNet.Core;
+using Robust.Shared.Utility;
+using YamlDotNet.RepresentationModel;
+
+namespace Content.Tools
+{
+ public class Map
+ {
+ public Map(string path)
+ {
+ Path = path;
+
+ using var reader = new StreamReader(path);
+ var stream = new YamlStream();
+
+ stream.Load(reader);
+
+ Root = stream.Documents[0].RootNode;
+ TilemapNode = (YamlMappingNode) Root["tilemap"];
+ GridsNode = (YamlSequenceNode) Root["grids"];
+ _entitiesNode = (YamlSequenceNode) Root["entities"];
+
+ foreach (var entity in _entitiesNode)
+ {
+ var uid = uint.Parse(entity["uid"].AsString());
+ if (uid >= NextAvailableEntityId)
+ NextAvailableEntityId = uid + 1;
+ Entities[uid] = (YamlMappingNode) entity;
+ }
+ }
+
+ // Core
+
+ public string Path { get; }
+
+ public YamlNode Root { get; }
+
+ // Useful
+
+ public YamlMappingNode TilemapNode { get; }
+
+ public YamlSequenceNode GridsNode { get; }
+
+ // Entities lookup
+
+ private YamlSequenceNode _entitiesNode { get; }
+
+ public Dictionary Entities { get; } = new Dictionary();
+
+ public uint MaxId => Entities.Max(entry => entry.Key);
+
+ public uint NextAvailableEntityId { get; set; }
+
+ // ----
+
+ public void Save(string fileName)
+ {
+ // Update entities node
+ _entitiesNode.Children.Clear();
+ foreach (var kvp in Entities)
+ _entitiesNode.Add(kvp.Value);
+
+ using var writer = new StreamWriter(fileName);
+ var document = new YamlDocument(Root);
+ var stream = new YamlStream(document);
+ var emitter = new Emitter(writer);
+ var fixer = new TypeTagPreserver(emitter);
+
+ stream.Save(fixer, false);
+
+ writer.Flush();
+ }
+
+ public void Save()
+ {
+ Save(Path);
+ }
+ }
+}
diff --git a/Content.Tools/MappingMergeDriver.cs b/Content.Tools/MappingMergeDriver.cs
new file mode 100644
index 00000000000..f069a7a5bbe
--- /dev/null
+++ b/Content.Tools/MappingMergeDriver.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using YamlDotNet.Core;
+using YamlDotNet.RepresentationModel;
+
+namespace Content.Tools
+{
+ internal static class MappingMergeDriver
+ {
+ /// %A: Our file
+ /// %O: Origin (common, base) file
+ /// %B: Other file
+ /// %P: Actual filename of the resulting file
+ public static void Main(string[] args)
+ {
+ var ours = new Map(args[0]);
+ var based = new Map(args[1]); // On what?
+ var other = new Map(args[2]);
+
+ if ((ours.GridsNode.Children.Count != 1) || (based.GridsNode.Children.Count != 1) || (other.GridsNode.Children.Count != 1))
+ {
+ Console.WriteLine("one or more files had an amount of grids not equal to 1");
+ Environment.Exit(1);
+ }
+
+ if (!(new Merger(ours, based, other).Merge()))
+ {
+ Console.WriteLine("unable to merge!");
+ Environment.Exit(1);
+ }
+
+ ours.Save();
+ Environment.Exit(0);
+ }
+ }
+}
diff --git a/Content.Tools/Merger.cs b/Content.Tools/Merger.cs
new file mode 100644
index 00000000000..1b5aaa4fe3c
--- /dev/null
+++ b/Content.Tools/Merger.cs
@@ -0,0 +1,356 @@
+using System;
+using System.IO;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using YamlDotNet.Core;
+using YamlDotNet.RepresentationModel;
+
+namespace Content.Tools
+{
+ public class Merger
+ {
+ public Map MapOurs { get; }
+ public Map MapBased { get; }
+ public Map MapOther { get; }
+
+ public Dictionary TileMapFromOtherToOurs { get; } = new Dictionary();
+ public Dictionary TileMapFromBasedToOurs { get; } = new Dictionary();
+ public Dictionary EntityMapFromOtherToOurs { get; } = new Dictionary();
+ public List EntityListDirectMerge { get; } = new List();
+
+ private const int ExpectedChunkSize = 16 * 16 * 4;
+
+ public Merger(Map ours, Map based, Map other)
+ {
+ MapOurs = ours;
+ MapBased = based;
+ MapOther = other;
+ }
+
+ public bool Merge()
+ {
+ PlanTileMapping(TileMapFromOtherToOurs, MapOther);
+ PlanTileMapping(TileMapFromBasedToOurs, MapBased);
+ MergeTiles();
+ PlanEntityMapping();
+ return MergeEntities();
+ }
+
+ // -- Tiles --
+
+ public void PlanTileMapping(Dictionary relativeOtherToOurs, Map relativeOther)
+ {
+ var mapping = new Dictionary();
+ uint nextAvailable = 0;
+ foreach (var kvp in MapOurs.TilemapNode)
+ {
+ var k = uint.Parse(kvp.Key.ToString());
+ var v = kvp.Value.ToString();
+ mapping[v] = k;
+ if (k >= nextAvailable)
+ nextAvailable = k + 1;
+ }
+ foreach (var kvp in relativeOther.TilemapNode)
+ {
+ var k = uint.Parse(kvp.Key.ToString());
+ var v = kvp.Value.ToString();
+ if (mapping.ContainsKey(v))
+ {
+ relativeOtherToOurs[k] = mapping[v];
+ }
+ else
+ {
+ MapOurs.TilemapNode.Add(nextAvailable.ToString(CultureInfo.InvariantCulture), v);
+ relativeOtherToOurs[k] = nextAvailable++;
+ }
+ }
+ }
+
+ public void MergeTiles()
+ {
+ var a = MapOurs.GridsNode.Children[0];
+ var b = MapBased.GridsNode.Children[0];
+ var c = MapOther.GridsNode.Children[0];
+ var aChunks = a["chunks"];
+ var bChunks = b["chunks"];
+ var cChunks = c["chunks"];
+ MergeTileChunks((YamlSequenceNode) aChunks, (YamlSequenceNode) bChunks, (YamlSequenceNode) cChunks);
+ }
+
+ public void MergeTileChunks(YamlSequenceNode aChunks, YamlSequenceNode bChunks, YamlSequenceNode cChunks)
+ {
+ var aMap = ConvertTileChunks(aChunks);
+ var bMap = ConvertTileChunks(bChunks);
+ var cMap = ConvertTileChunks(cChunks);
+
+ var xMap = new HashSet();
+ foreach (var kvp in aMap)
+ xMap.Add(kvp.Key);
+ // don't include b because that would mess with chunk deletion
+ foreach (var kvp in cMap)
+ xMap.Add(kvp.Key);
+
+ foreach (var ind in xMap)
+ {
+ using var a = new MemoryStream(GetChunkBytes(aMap, ind));
+ using var b = new MemoryStream(GetChunkBytes(bMap, ind));
+ using var c = new MemoryStream(GetChunkBytes(cMap, ind));
+ using var aR = new BinaryReader(a);
+ using var bR = new BinaryReader(b);
+ using var cR = new BinaryReader(c);
+
+ var outB = new byte[ExpectedChunkSize];
+
+ {
+ using (var outS = new MemoryStream(outB))
+ using (var outW = new BinaryWriter(outS))
+
+ for (var i = 0; i < ExpectedChunkSize; i += 4)
+ {
+ var aI = aR.ReadUInt32();
+ var bI = MapTileId(bR.ReadUInt32(), TileMapFromBasedToOurs);
+ var cI = MapTileId(cR.ReadUInt32(), TileMapFromOtherToOurs);
+ // cI needs translation.
+
+ uint result = aI;
+ if (aI == bI)
+ {
+ // If aI == bI then aI did not change anything, so cI always wins
+ result = cI;
+ }
+ else if (bI != cI)
+ {
+ // If bI != cI then cI definitely changed something (conflict, but overrides aI)
+ result = cI;
+ Console.WriteLine("WARNING: Tile (" + ind + ")[" + i + "] was changed by both branches.");
+ }
+ outW.Write(result);
+ }
+ }
+
+ // Actually output chunk
+ if (!aMap.ContainsKey(ind))
+ {
+ var res = new YamlMappingNode();
+ res.Children["ind"] = ind;
+ aMap[ind] = res;
+ }
+ aMap[ind].Children["tiles"] = Convert.ToBase64String(outB);
+ }
+ }
+
+ public uint MapTileId(uint src, Dictionary mapping)
+ {
+ return (src & 0xFFFF0000) | mapping[src & 0xFFFF];
+ }
+
+ public Dictionary ConvertTileChunks(YamlSequenceNode chunks)
+ {
+ var map = new Dictionary();
+ foreach (var chunk in chunks)
+ map[chunk["ind"].ToString()] = (YamlMappingNode) chunk;
+ return map;
+ }
+
+ public byte[] GetChunkBytes(Dictionary chunks, string ind)
+ {
+ if (!chunks.ContainsKey(ind))
+ return new byte[ExpectedChunkSize];
+ return Convert.FromBase64String(chunks[ind]["tiles"].ToString());
+ }
+
+ // -- Entities --
+
+ public void PlanEntityMapping()
+ {
+ // Ok, so here's how it works:
+ // 1. Entities that do not exist in "based" are additions.
+ // 2. Entities that exist in "based" but do not exist in the one map or the other are removals.
+
+ // Find modifications and deletions
+ foreach (var kvp in MapBased.Entities)
+ {
+ var deletedByOurs = !MapOurs.Entities.ContainsKey(kvp.Key);
+ var deletedByOther = !MapOther.Entities.ContainsKey(kvp.Key);
+ if (deletedByOther && !deletedByOurs)
+ {
+ // Delete
+ MapOurs.Entities.Remove(kvp.Key);
+ }
+ else if (!(deletedByOurs || deletedByOther))
+ {
+ // Modify
+ EntityMapFromOtherToOurs[kvp.Key] = kvp.Key;
+ }
+ }
+
+ // Find additions
+ foreach (var kvp in MapOther.Entities)
+ {
+ if (!MapBased.Entities.ContainsKey(kvp.Key))
+ {
+ // New
+ var newId = MapOurs.NextAvailableEntityId++;
+ EntityMapFromOtherToOurs[kvp.Key] = newId;
+ }
+ }
+ }
+
+ public bool MergeEntities()
+ {
+ bool success = true;
+ foreach (var kvp in EntityMapFromOtherToOurs)
+ {
+ // For debug use.
+ // Console.WriteLine("Entity C/" + kvp.Key + " -> A/" + kvp.Value);
+ YamlMappingNode oursEnt;
+ if (MapOurs.Entities.ContainsKey(kvp.Value))
+ {
+ oursEnt = MapOurs.Entities[kvp.Value];
+ if (!MapBased.Entities.TryGetValue(kvp.Value, out var basedEnt))
+ {
+ basedEnt = oursEnt;
+ }
+
+ if (!MergeEntityNodes(oursEnt, basedEnt, MapOther.Entities[kvp.Key]))
+ {
+ Console.WriteLine("Unable to successfully merge entity C/" + kvp.Key);
+ success = false;
+ }
+ }
+ else
+ {
+ oursEnt = (YamlMappingNode) YamlTools.CopyYamlNodes(MapOther.Entities[kvp.Key]);
+ if (!MapEntity(oursEnt)) {
+ Console.WriteLine("Unable to successfully import entity C/" + kvp.Key);
+ success = false;
+ } else {
+ MapOurs.Entities[kvp.Value] = oursEnt;
+ }
+ }
+ oursEnt.Children["uid"] = kvp.Value.ToString(CultureInfo.InvariantCulture);
+ }
+ return success;
+ }
+
+ public bool MergeEntityNodes(YamlMappingNode ours, YamlMappingNode based, YamlMappingNode other)
+ {
+ // Copy to intermediate
+ var otherMapped = (YamlMappingNode) YamlTools.CopyYamlNodes(other);
+ if (!MapEntity(otherMapped))
+ return false;
+ // Merge stuff that isn't components
+ var path = "Entity" + (other["uid"].ToString());
+ YamlTools.MergeYamlMappings(ours, based, otherMapped, path, new string[] {"components"});
+ // Components are special
+ var ourComponents = new Dictionary();
+ var basedComponents = new Dictionary();
+ var ourComponentsNode = (YamlSequenceNode) ours["components"];
+ var basedComponentsNode = (YamlSequenceNode) based["components"];
+ var otherComponentsNode = (YamlSequenceNode) otherMapped["components"];
+ foreach (var component in ourComponentsNode)
+ {
+ var name = component["type"].ToString();
+ ourComponents[name] = (YamlMappingNode) component;
+ }
+ foreach (var component in basedComponentsNode)
+ {
+ var name = component["type"].ToString();
+ basedComponents[name] = (YamlMappingNode) component;
+ }
+ foreach (var otherComponent in otherComponentsNode)
+ {
+ var name = otherComponent["type"].ToString();
+ if (ourComponents.ContainsKey(name))
+ {
+ var ourComponent = ourComponents[name];
+ if (!basedComponents.TryGetValue(name, out var basedComponent))
+ basedComponent = new YamlMappingNode();
+
+ YamlTools.MergeYamlNodes(ourComponent, basedComponent, otherComponent, path + "/components/" + name);
+ }
+ else
+ {
+ ourComponentsNode.Add(otherComponent);
+ }
+ }
+ return true;
+ }
+
+ public bool MapEntity(YamlMappingNode other)
+ {
+ var path = "Entity" + (other["uid"].ToString());
+ if (other.Children.ContainsKey("components"))
+ {
+ var components = (YamlSequenceNode) other["components"];
+ foreach (var component in components)
+ {
+ var type = component["type"].ToString();
+ if (type == "Transform")
+ {
+ if (!MapEntityProperty((YamlMappingNode) component, "parent", path))
+ return false;
+ }
+ else if (type == "ContainerContainer")
+ {
+ MapEntityRecursiveAndBadly(component, path);
+ }
+ }
+ }
+ return true;
+ }
+
+ public bool MapEntityProperty(YamlMappingNode node, string property, string path)
+ {
+ if (node.Children.ContainsKey(property)) {
+ var prop = node[property];
+ if (prop is YamlScalarNode)
+ return MapEntityProperty((YamlScalarNode) prop, path + "/" + property);
+ }
+ return true;
+ }
+
+ public bool MapEntityProperty(YamlScalarNode node, string path)
+ {
+ if (uint.TryParse(node.ToString(), out var uid))
+ {
+ if (EntityMapFromOtherToOurs.ContainsKey(uid))
+ {
+ node.Value = EntityMapFromOtherToOurs[uid].ToString(CultureInfo.InvariantCulture);
+ }
+ else
+ {
+ Console.WriteLine($"Error finding UID in MapEntityRecursiveAndBadly {path}. To fix this, the merge driver needs to be improved.");
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public bool MapEntityRecursiveAndBadly(YamlNode node, string path)
+ {
+ switch (node)
+ {
+ case YamlSequenceNode subSequence:
+ var idx = 0;
+ foreach (var val in subSequence)
+ if (!MapEntityRecursiveAndBadly(val, path + "/" + (idx++)))
+ return false;
+ return true;
+ case YamlMappingNode subMapping:
+ foreach (var kvp in subMapping)
+ if (!MapEntityRecursiveAndBadly(kvp.Key, path))
+ return false;
+ foreach (var kvp in subMapping)
+ if (!MapEntityRecursiveAndBadly(kvp.Value, path + "/" + kvp.Key.ToString()))
+ return false;
+ return true;
+ case YamlScalarNode subScalar:
+ return MapEntityProperty(subScalar, path);
+ default:
+ throw new ArgumentException($"Unrecognized YAML node type: {node.GetType()} at {path}");
+ }
+ }
+ }
+}
diff --git a/Content.Tools/TypeTagPreserver.cs b/Content.Tools/TypeTagPreserver.cs
new file mode 100644
index 00000000000..94996767d3e
--- /dev/null
+++ b/Content.Tools/TypeTagPreserver.cs
@@ -0,0 +1,25 @@
+using YamlDotNet.Core;
+using YamlDotNet.Core.Events;
+
+namespace Content.Tools
+{
+ public class TypeTagPreserver : IEmitter
+ {
+ public TypeTagPreserver(IEmitter emitter)
+ {
+ Emitter = emitter;
+ }
+
+ private IEmitter Emitter { get; }
+
+ public void Emit(ParsingEvent @event)
+ {
+ if (@event is MappingStart mapping)
+ {
+ @event = new MappingStart(mapping.Anchor, mapping.Tag, false, mapping.Style, mapping.Start, mapping.End);
+ }
+
+ Emitter.Emit(@event);
+ }
+ }
+}
diff --git a/Content.Tools/YamlTools.cs b/Content.Tools/YamlTools.cs
new file mode 100644
index 00000000000..ec1f4322846
--- /dev/null
+++ b/Content.Tools/YamlTools.cs
@@ -0,0 +1,199 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using YamlDotNet.Core;
+using YamlDotNet.RepresentationModel;
+
+namespace Content.Tools
+{
+ public static class YamlTools
+ {
+ public static YamlNode CopyYamlNodes(YamlNode other)
+ {
+ switch (other)
+ {
+ case YamlSequenceNode subSequence:
+ YamlSequenceNode tmp1 = new YamlSequenceNode();
+ MergeYamlSequences((YamlSequenceNode) tmp1, new YamlSequenceNode(), (YamlSequenceNode) other, "");
+ return tmp1;
+ case YamlMappingNode subMapping:
+ YamlMappingNode tmp2 = new YamlMappingNode();
+ MergeYamlMappings((YamlMappingNode) tmp2, new YamlMappingNode(), (YamlMappingNode) other, "", new string[] {});
+ return tmp2;
+ case YamlScalarNode subScalar:
+ YamlScalarNode tmp3 = new YamlScalarNode();
+ CopyYamlScalar(tmp3, subScalar);
+ return tmp3;
+ default:
+ throw new ArgumentException($"Unrecognized YAML node type for copy: {other.GetType()}", nameof(other));
+ }
+ }
+
+ public static bool TriTypeMatch(YamlNode ours, YamlNode based, YamlNode other)
+ {
+ var refType = other.GetType();
+ if (refType != based.GetType())
+ return false;
+ if (refType != ours.GetType())
+ return false;
+ return true;
+ }
+
+ public static void MergeYamlNodes(YamlNode ours, YamlNode based, YamlNode other, string path)
+ {
+ if (!TriTypeMatch(ours, based, other))
+ throw new ArgumentException($"Node type mismatch at {path}");
+ switch (other)
+ {
+ case YamlSequenceNode subSequence:
+ MergeYamlSequences((YamlSequenceNode) ours, (YamlSequenceNode) based, (YamlSequenceNode) other, path);
+ break;
+ case YamlMappingNode subMapping:
+ MergeYamlMappings((YamlMappingNode) ours, (YamlMappingNode) based, (YamlMappingNode) other, path, new string[] {});
+ break;
+ case YamlScalarNode subScalar:
+ // Console.WriteLine(path + " - " + ours + " || " + based + " || " + other);
+ var scalarA = (YamlScalarNode) ours;
+ var scalarB = (YamlScalarNode) based;
+ var scalarC = (YamlScalarNode) other;
+ var aeb = (scalarA.Value == scalarB.Value);
+ var cneb = (scalarC.Value != scalarB.Value);
+ if (aeb || cneb)
+ CopyYamlScalar(scalarA, scalarC);
+ // Console.WriteLine(path + " . " + ours + " || " + based + " || " + other);
+ break;
+ default:
+ throw new ArgumentException($"Unrecognized YAML node type at {path}: {other.GetType()}", nameof(other));
+ }
+ }
+
+ public static void MergeYamlSequences(YamlSequenceNode ours, YamlSequenceNode based, YamlSequenceNode other, string path)
+ {
+ if ((ours.Children.Count == based.Children.Count) && (other.Children.Count == ours.Children.Count))
+ {
+ // this is terrible and doesn't do proper rearrange detection
+ // but it looks as if vectors might be arrays
+ // so rearrange detection might break more stuff...
+ // nope, they aren't, but still good to have
+ for (var i = 0; i < ours.Children.Count; i++)
+ MergeYamlNodes(ours.Children[i], based.Children[i], other.Children[i], path + "/" + i);
+ return;
+ }
+ // for now, just copy other -> ours
+ // I am aware this is terrible
+ ours.Children.Clear();
+ foreach (var c in other.Children)
+ ours.Add(CopyYamlNodes(c));
+ }
+
+ public static void MergeYamlMappings(YamlMappingNode ours, YamlMappingNode based, YamlMappingNode other, string path, string[] ignoreThese)
+ {
+ // Deletions/modifications
+ foreach (var kvp in based)
+ {
+ if (ignoreThese.Contains(kvp.Key.ToString()))
+ continue;
+
+ var localPath = path + "/" + kvp.Key.ToString();
+ var deletedByOurs = !ours.Children.ContainsKey(kvp.Key);
+ var deletedByOther = !other.Children.ContainsKey(kvp.Key);
+ if (deletedByOther && (!deletedByOurs))
+ {
+ // Delete
+ ours.Children.Remove(kvp.Key);
+ }
+ else if (!(deletedByOurs || deletedByOther))
+ {
+ // Modify
+ var a = ours[kvp.Key];
+ var b = kvp.Value; // based[kvp.Key]
+ var c = other[kvp.Key];
+ if (!TriTypeMatch(a, b, c))
+ {
+ Console.WriteLine("Warning: Type mismatch (defaulting to value C) at " + localPath);
+ ours.Children[kvp.Key] = CopyYamlNodes(c);
+ }
+ else
+ {
+ MergeYamlNodes(a, b, c, localPath);
+ }
+ }
+ }
+ // Additions
+ foreach (var kvp in other)
+ {
+ if (ignoreThese.Contains(kvp.Key.ToString()))
+ continue;
+
+ var localPath = path + "/" + kvp.Key.ToString();
+ if (!based.Children.ContainsKey(kvp.Key))
+ {
+ if (ours.Children.ContainsKey(kvp.Key))
+ {
+ // Both sides added the same key. Try to merge.
+ var a = ours[kvp.Key];
+ var b = based[kvp.Key];
+ var c = kvp.Value; // other[kvp.Key]
+ if (!TriTypeMatch(a, b, c))
+ {
+ Console.WriteLine("Warning: Type mismatch (defaulting to value C) at " + localPath);
+ ours.Children[kvp.Key] = CopyYamlNodes(c);
+ }
+ else
+ {
+ MergeYamlNodes(a, b, c, localPath);
+ }
+ }
+ else
+ {
+ // Well that was easy
+ ours.Children[kvp.Key] = CopyYamlNodes(kvp.Value);
+ }
+ }
+ }
+ }
+
+ // NOTE: This is a heuristic ONLY! And is also not used at the moment because sequence matching isn't in place.
+ // It could also be massively improved.
+ public static float YamlNodesHeuristic(YamlNode a, YamlNode b)
+ {
+ if (a.GetType() != b.GetType())
+ return 0.0f;
+ switch (a)
+ {
+ case YamlSequenceNode x:
+ return YamlSequencesHeuristic((YamlSequenceNode) a, (YamlSequenceNode) b);
+ case YamlMappingNode y:
+ return YamlMappingsHeuristic((YamlMappingNode) a, (YamlMappingNode) b);
+ case YamlScalarNode z:
+ return (((YamlScalarNode) a).Value == ((YamlScalarNode) b).Value) ? 1.0f : 0.0f;
+ default:
+ throw new ArgumentException($"Unrecognized YAML node type: {a.GetType()}", nameof(a));
+ }
+ }
+
+ public static float YamlSequencesHeuristic(YamlSequenceNode a, YamlSequenceNode b)
+ {
+ if (a.Children.Count != b.Children.Count)
+ return 0.0f;
+ if (a.Children.Count == 0)
+ return 1.0f;
+ var total = 0.0f;
+ for (var i = 0; i < a.Children.Count; i++)
+ total += YamlNodesHeuristic(a.Children[i], b.Children[i]);
+ return total / a.Children.Count;
+ }
+
+ public static float YamlMappingsHeuristic(YamlMappingNode a, YamlMappingNode b)
+ {
+ return (a == b) ? 1.0f : 0.0f;
+ }
+
+ public static void CopyYamlScalar(YamlScalarNode dst, YamlScalarNode src)
+ {
+ dst.Value = src.Value;
+ dst.Style = src.Style;
+ }
+ }
+}
diff --git a/Content.Tools/test/0A.yml b/Content.Tools/test/0A.yml
new file mode 100644
index 00000000000..d977df527ae
--- /dev/null
+++ b/Content.Tools/test/0A.yml
@@ -0,0 +1,95 @@
+meta:
+ format: 2
+ name: DemoStation
+ author: Space-Wizards
+ postmapinit: false
+tilemap:
+ 0: space
+ 7: floor_dark
+ 8: floor_elevator_shaft
+ 9: floor_freezer
+ 10: floor_gold
+ 11: floor_green_circuit
+ 12: floor_hydro
+ 13: floor_lino
+ 14: floor_mono
+ 15: floor_reinforced
+ 16: floor_rock_vault
+ 17: floor_showroom
+ 18: floor_snow
+ 19: floor_steel
+ 20: floor_steel_dirty
+ 21: floor_techmaint
+ 22: floor_white
+ 23: floor_wood
+ 24: lattice
+ 25: plating
+ 26: underplating
+grids:
+- settings:
+ chunksize: 16
+ tilesize: 1
+ snapsize: 1
+ chunks:
+ - ind: "-2,-2"
+ comment: "Ew in A | FQ in B | Fg in C | Fg in Out - SHOULD CAUSE CONFLICT WARNING"
+ tiles: EwAAABMAAAATAAAAEwAAABMAAAATAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABcAAAAaAAAAEwAAABMAAAATAAAAGgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAaAAAAFgAAABYAAAAaAAAAGgAAABMAAAATAAAAEwAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABoAAAATAAAAEwAAABMAAAAaAAAAGgAAABMAAAAaAAAAEwAAABoAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAAaAAAAEwAAABoAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAAaAAAAGgAAABoAAAAVAAAAGgAAABoAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAGgAAABkAAAAZAAAAGQAAABkAAAAZAAAAGgAAABoAAAAaAAAAGgAAABoAAAATAAAAGgAAABMAAAATAAAAEwAAABUAAAAZAAAAFQAAABUAAAAVAAAAGQAAABkAAAAZAAAAFQAAABUAAAAVAAAAGgAAABoAAAATAAAAEwAAABMAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGQAAABUAAAAaAAAAFQAAAA==
+ - ind: "-1,-1"
+ tiles: EwAAABYAAAAWAAAAFgAAABYAAAATAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABoAAAAWAAAAFgAAABYAAAAWAAAAEwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAEwAAABMAAAAaAAAAEwAAABMAAAAaAAAAGgAAABoAAAAaAAAAGgAAABkAAAAaAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAGgAAABMAAAAZAAAAGgAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABoAAAATAAAAGQAAABoAAAATAAAAEwAAABMAAAAaAAAAEwAAABoAAAAaAAAAEwAAABMAAAAaAAAAGgAAABMAAAAaAAAAGgAAABkAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAAZAAAAFQAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAGgAAABoAAAAaAAAAGgAAAA==
+ - ind: "-1,0"
+ tiles: EwAAABYAAAAWAAAAFgAAABYAAAATAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABoAAAAWAAAAFgAAABYAAAAWAAAAEwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAEwAAABMAAAAaAAAAEwAAABMAAAAaAAAAGgAAABoAAAAaAAAAGgAAABkAAAAaAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAGgAAABMAAAAZAAAAGgAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABoAAAATAAAAGQAAABoAAAATAAAAEwAAABMAAAAaAAAAEwAAABoAAAAaAAAAEwAAABMAAAAaAAAAGgAAABMAAAAaAAAAGgAAABkAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAAZAAAAFQAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAGgAAABoAAAAaAAAAGgAAAA==
+ - ind: "0,0"
+ tiles: FwAAABMAAAATAAAAEwAAABMAAAATAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABcAAAAaAAAAEwAAABMAAAATAAAAGgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAaAAAAFgAAABYAAAAaAAAAGgAAABMAAAATAAAAEwAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABoAAAATAAAAEwAAABMAAAAaAAAAGgAAABMAAAAaAAAAEwAAABoAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAAaAAAAEwAAABoAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAAaAAAAGgAAABoAAAAVAAAAGgAAABoAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAGgAAABkAAAAZAAAAGQAAABkAAAAZAAAAGgAAABoAAAAaAAAAGgAAABoAAAATAAAAGgAAABMAAAATAAAAEwAAABUAAAAZAAAAFQAAABUAAAAVAAAAGQAAABkAAAAZAAAAFQAAABUAAAAVAAAAGgAAABoAAAATAAAAEwAAABMAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGQAAABUAAAAaAAAAFQAAAA==
+entities:
+- uid: 0
+ type: FakeTestDummy
+ components:
+ - parent: 855
+ pos: -5.5,-1.5
+ rot: -1.5707963267948966 rad
+ type: Transform
+ - type: ContainerContainer
+ typeChange:
+ - 0
+ - 1
+ - 2
+ example:
+ - 0
+ - 1
+ - 2
+ - 3
+ - 855
+ - "contributionA"
+- uid: 1
+ type: FakeTestDummy
+ components:
+ - parent: 0
+ pos: -15.5,-14.5
+ rot: -1.5707963267948966 rad
+ type: Transform
+- uid: 2
+ type: FakeTestDummy
+ components:
+ - parent: 1
+ pos: -15.5,-14.5
+ rot: -1.5707963267948966 rad
+ type: Transform
+ - type: ThereShouldBeTwoOfTheseInOutputOfMergeTestAAA
+- uid: 3
+ type: AnnoyingPlaceholderOnPurpose
+ components:
+ - parent: 0
+ type: Transform
+- uid: 855
+ components:
+ - name: Saltern Station
+ type: MetaData
+ - parent: null
+ type: Transform
+ - index: 0
+ type: MapGrid
+ - shapes:
+ - !type:PhysShapeGrid
+ grid: 0
+ type: Physics
+
diff --git a/Content.Tools/test/0B.yml b/Content.Tools/test/0B.yml
new file mode 100644
index 00000000000..27430439da5
--- /dev/null
+++ b/Content.Tools/test/0B.yml
@@ -0,0 +1,86 @@
+meta:
+ format: 2
+ name: DemoStation
+ author: Space-Wizards
+ postmapinit: false
+tilemap:
+ 0: space
+ 7: floor_dark
+ 8: floor_elevator_shaft
+ 9: floor_freezer
+ 10: floor_gold
+ 11: floor_green_circuit
+ 12: floor_hydro
+ 13: floor_lino
+ 14: floor_mono
+ 15: floor_reinforced
+ 16: floor_rock_vault
+ 17: floor_showroom
+ 18: floor_snow
+ 19: floor_steel
+ 20: floor_steel_dirty
+ 21: floor_techmaint
+ 22: floor_white
+ 23: floor_wood
+ 24: lattice
+ 25: plating
+ 26: underplating
+grids:
+- settings:
+ chunksize: 16
+ tilesize: 1
+ snapsize: 1
+ chunks:
+ - ind: "-2,-2"
+ comment: "Ew in A | FQ in B | Fg in C | Fg in Out - SHOULD CAUSE CONFLICT WARNING"
+ tiles: FQAAABMAAAATAAAAEwAAABMAAAATAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABcAAAAaAAAAEwAAABMAAAATAAAAGgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAaAAAAFgAAABYAAAAaAAAAGgAAABMAAAATAAAAEwAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABoAAAATAAAAEwAAABMAAAAaAAAAGgAAABMAAAAaAAAAEwAAABoAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAAaAAAAEwAAABoAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAAaAAAAGgAAABoAAAAVAAAAGgAAABoAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAGgAAABkAAAAZAAAAGQAAABkAAAAZAAAAGgAAABoAAAAaAAAAGgAAABoAAAATAAAAGgAAABMAAAATAAAAEwAAABUAAAAZAAAAFQAAABUAAAAVAAAAGQAAABkAAAAZAAAAFQAAABUAAAAVAAAAGgAAABoAAAATAAAAEwAAABMAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGQAAABUAAAAaAAAAFQAAAA==
+ - ind: "-1,-1"
+ tiles: FQAAABUAAAAVAAAAFQAAABUAAAAVAAAAFQAAABUAAAAVAAAAGgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABkAAAAZAAAAGQAAABkAAAAZAAAAGQAAABkAAAAVAAAAFQAAABoAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAaAAAAGgAAABUAAAAaAAAAGgAAABoAAAAZAAAAFQAAABUAAAAaAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFQAAABUAAAAVAAAAFQAAABUAAAAaAAAAGQAAABUAAAAVAAAAGgAAABYAAAAWAAAAFgAAABoAAAAaAAAAGgAAABUAAAAVAAAAFQAAABUAAAAVAAAAGgAAABkAAAAVAAAAFQAAABoAAAAaAAAAFgAAABoAAAAaAAAAGQAAABkAAAAVAAAAFQAAABUAAAAVAAAAFQAAABoAAAAZAAAAGQAAABkAAAAZAAAAGQAAABkAAAAZAAAAGQAAABkAAAAZAAAAFQAAABUAAAAVAAAAFQAAABUAAAAaAAAAGQAAABUAAAAVAAAAFQAAABUAAAAVAAAAFQAAABUAAAAVAAAAFQAAABoAAAAaAAAAGgAAABUAAAAaAAAAGgAAABkAAAAZAAAAGQAAABkAAAAZAAAAGQAAABkAAAAZAAAAFQAAABUAAAAVAAAAFQAAABUAAAAVAAAAFQAAABUAAAAZAAAAFQAAAA0AAAANAAAADQAAAA0AAAANAAAAGgAAABUAAAAVAAAAGQAAABkAAAAZAAAAGQAAABkAAAAZAAAAGQAAABkAAAANAAAADQAAAA0AAAANAAAADQAAABoAAAAVAAAAFQAAABkAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAADQAAAA0AAAANAAAADQAAAA0AAAAaAAAAGgAAABoAAAAZAAAAEwAAAAkAAAAJAAAACQAAAAkAAAAJAAAAGgAAAA0AAAANAAAADQAAAA0AAAANAAAAFwAAABcAAAAXAAAAGgAAABoAAAAJAAAACQAAAAkAAAAJAAAACQAAABoAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABoAAAAaAAAAGgAAABMAAAAaAAAAGgAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAaAAAAFgAAABYAAAAWAAAAFgAAABYAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAGgAAABYAAAAWAAAAFgAAABYAAAATAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAAA==
+ - ind: "-1,0"
+ tiles: EwAAABYAAAAWAAAAFgAAABYAAAATAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABoAAAAWAAAAFgAAABYAAAAWAAAAEwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAEwAAABMAAAAaAAAAEwAAABMAAAAaAAAAGgAAABoAAAAaAAAAGgAAABkAAAAaAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAGgAAABMAAAAZAAAAGgAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABoAAAATAAAAGQAAABoAAAATAAAAEwAAABMAAAAaAAAAEwAAABoAAAAaAAAAEwAAABMAAAAaAAAAGgAAABMAAAAaAAAAGgAAABkAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAAZAAAAFQAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAGgAAABoAAAAaAAAAGgAAAA==
+ - ind: "0,-1"
+ tiles: FgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABYAAAAWAAAAFgAAABYAAAAaAAAAFgAAABYAAAAWAAAAFgAAABYAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAAWAAAAFgAAABYAAAAWAAAAGgAAABYAAAAWAAAAFgAAABYAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAFgAAABYAAAAWAAAAFgAAABoAAAAWAAAAFgAAABYAAAAWAAAAGgAAABoAAAATAAAAEwAAABMAAAAaAAAAGgAAABoAAAAWAAAAGgAAABoAAAAaAAAAFgAAABYAAAAWAAAAGgAAABkAAAAaAAAAEwAAABMAAAATAAAAGgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAZAAAAGgAAABMAAAATAAAAEwAAABoAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAGQAAABoAAAATAAAAEwAAABMAAAAaAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABkAAAAaAAAAEwAAABMAAAATAAAAGgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAZAAAAFQAAABMAAAATAAAAEwAAABoAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFQAAABoAAAATAAAAEwAAABMAAAAaAAAAGgAAABYAAAAaAAAAGgAAABYAAAAWAAAAGgAAABoAAAAWAAAAFgAAABoAAAAaAAAAEwAAABMAAAATAAAAGgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAaAAAAFgAAABYAAAAXAAAAGgAAABMAAAATAAAAEwAAABoAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAGgAAABYAAAAWAAAAFwAAABoAAAATAAAAEwAAABMAAAAaAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABoAAAAaAAAAFgAAABcAAAATAAAAEwAAABMAAAATAAAAEwAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAaAAAAFgAAABYAAAAXAAAAEwAAABMAAAATAAAAEwAAABMAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAGgAAABYAAAAWAAAAFwAAABMAAAATAAAAEwAAABMAAAATAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABoAAAAWAAAAFgAAAA==
+ - ind: "0,0"
+ tiles: FwAAABMAAAATAAAAEwAAABMAAAATAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABcAAAAaAAAAEwAAABMAAAATAAAAGgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAaAAAAFgAAABYAAAAaAAAAGgAAABMAAAATAAAAEwAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABoAAAATAAAAEwAAABMAAAAaAAAAGgAAABMAAAAaAAAAEwAAABoAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAAaAAAAEwAAABoAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAAaAAAAGgAAABoAAAAVAAAAGgAAABoAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAGgAAABkAAAAZAAAAGQAAABkAAAAZAAAAGgAAABoAAAAaAAAAGgAAABoAAAATAAAAGgAAABMAAAATAAAAEwAAABUAAAAZAAAAFQAAABUAAAAVAAAAGQAAABkAAAAZAAAAFQAAABUAAAAVAAAAGgAAABoAAAATAAAAEwAAABMAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGQAAABUAAAAaAAAAFQAAAA==
+entities:
+- uid: 0
+ type: FakeTestDummy
+ components:
+ - parent: 855
+ pos: -15.5,-11.5
+ rot: -1.5707963267948966 rad
+ type: Transform
+ - type: ContainerContainer
+ typeChange:
+ - 0
+ - 1
+ example:
+ - 0
+ - 1
+ - 3
+ - 855
+- uid: 1
+ type: FakeTestDummy
+ components:
+ - parent: 0
+ pos: -15.5,-14.5
+ rot: -1.5707963267948966 rad
+ type: Transform
+- uid: 3
+ type: AnnoyingPlaceholderOnPurpose
+ components:
+ - parent: 0
+ type: Transform
+- uid: 855
+ components:
+ - name: Saltern Station
+ type: MetaData
+ - parent: null
+ type: Transform
+ - index: 0
+ type: MapGrid
+ - shapes:
+ - !type:PhysShapeGrid
+ grid: 0
+ type: Physics
+
diff --git a/Content.Tools/test/0C.yml b/Content.Tools/test/0C.yml
new file mode 100644
index 00000000000..9d1634c44f4
--- /dev/null
+++ b/Content.Tools/test/0C.yml
@@ -0,0 +1,105 @@
+meta:
+ format: 2
+ name: DemoStation
+ author: Space-Wizards
+ postmapinit: false
+tilemap:
+ 0: space
+ 1: floor_asteroid_coarse_sand0
+ 2: floor_asteroid_coarse_sand1
+ 3: floor_asteroid_coarse_sand2
+ 4: floor_asteroid_coarse_sand_dug
+ 5: floor_asteroid_sand
+ 6: floor_asteroid_tile
+ 7: floor_dark
+ 8: floor_elevator_shaft
+ 9: floor_freezer
+ 10: floor_gold
+ 11: floor_green_circuit
+ 12: floor_hydro
+ 13: floor_lino
+ 14: floor_mono
+ 15: floor_reinforced
+ 16: floor_rock_vault
+ 17: floor_showroom
+ 18: floor_snow
+ 19: floor_steel
+ 20: floor_steel_dirty
+ 21: floor_techmaint
+ 22: floor_white
+ 23: floor_wood
+ 24: lattice
+ 25: plating
+ 26: underplating
+grids:
+- settings:
+ chunksize: 16
+ tilesize: 1
+ snapsize: 1
+ chunks:
+ - ind: "-2,-2"
+ comment: "Ew in A | FQ in B | Fg in C | Fg in Out - SHOULD CAUSE CONFLICT WARNING"
+ tiles: FgAAABMAAAATAAAAEwAAABMAAAATAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABcAAAAaAAAAEwAAABMAAAATAAAAGgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAaAAAAFgAAABYAAAAaAAAAGgAAABMAAAATAAAAEwAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABoAAAATAAAAEwAAABMAAAAaAAAAGgAAABMAAAAaAAAAEwAAABoAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAAaAAAAEwAAABoAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAAaAAAAGgAAABoAAAAVAAAAGgAAABoAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAGgAAABkAAAAZAAAAGQAAABkAAAAZAAAAGgAAABoAAAAaAAAAGgAAABoAAAATAAAAGgAAABMAAAATAAAAEwAAABUAAAAZAAAAFQAAABUAAAAVAAAAGQAAABkAAAAZAAAAFQAAABUAAAAVAAAAGgAAABoAAAATAAAAEwAAABMAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGQAAABUAAAAaAAAAFQAAAA==
+ - ind: "-1,-1"
+ tiles: FQAAABUAAAAVAAAAFQAAABUAAAAVAAAAFQAAABUAAAAVAAAAGgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABkAAAAZAAAAGQAAABkAAAAZAAAAGQAAABkAAAAVAAAAFQAAABoAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAaAAAAGgAAABUAAAAaAAAAGgAAABoAAAAZAAAAFQAAABUAAAAaAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFQAAABUAAAAVAAAAFQAAABUAAAAaAAAAGQAAABUAAAAVAAAAGgAAABYAAAAWAAAAFgAAABoAAAAaAAAAGgAAABUAAAAVAAAAFQAAABUAAAAVAAAAGgAAABkAAAAVAAAAFQAAABoAAAAaAAAAFgAAABoAAAAaAAAAGQAAABkAAAAVAAAAFQAAABUAAAAVAAAAFQAAABoAAAAZAAAAGQAAABkAAAAZAAAAGQAAABkAAAAZAAAAGQAAABkAAAAZAAAAFQAAABUAAAAVAAAAFQAAABUAAAAaAAAAGQAAABUAAAAVAAAAFQAAABUAAAAVAAAAFQAAABUAAAAVAAAAFQAAABoAAAAaAAAAGgAAABUAAAAaAAAAGgAAABkAAAAZAAAAGQAAABkAAAAZAAAAGQAAABkAAAAZAAAAFQAAABUAAAAVAAAAFQAAABUAAAAVAAAAFQAAABUAAAAZAAAAFQAAAA0AAAANAAAADQAAAA0AAAANAAAAGgAAABUAAAAVAAAAGQAAABkAAAAZAAAAGQAAABkAAAAZAAAAGQAAABkAAAANAAAADQAAAA0AAAANAAAADQAAABoAAAAVAAAAFQAAABkAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAADQAAAA0AAAANAAAADQAAAA0AAAAaAAAAGgAAABoAAAAZAAAAEwAAAAkAAAAJAAAACQAAAAkAAAAJAAAAGgAAAA0AAAANAAAADQAAAA0AAAANAAAAFwAAABcAAAAXAAAAGgAAABoAAAAJAAAACQAAAAkAAAAJAAAACQAAABoAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABoAAAAaAAAAGgAAABMAAAAaAAAAGgAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAaAAAAFgAAABYAAAAWAAAAFgAAABYAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAGgAAABYAAAAWAAAAFgAAABYAAAATAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAAA==
+ - ind: "-1,0"
+ tiles: EwAAABYAAAAWAAAAFgAAABYAAAATAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABoAAAAWAAAAFgAAABYAAAAWAAAAEwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAXAAAAFwAAABcAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAEwAAABMAAAAaAAAAEwAAABMAAAAaAAAAGgAAABoAAAAaAAAAGgAAABkAAAAaAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAGgAAABMAAAAZAAAAGgAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABoAAAATAAAAGQAAABoAAAATAAAAEwAAABMAAAAaAAAAEwAAABoAAAAaAAAAEwAAABMAAAAaAAAAGgAAABMAAAAaAAAAGgAAABkAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAAZAAAAFQAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAEwAAABMAAAATAAAAEwAAABoAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAGgAAABoAAAAaAAAAGgAAAA==
+ - ind: "0,-1"
+ tiles: FgAAABMAAAATAAAAEwAAABMAAAATAAAAGgAAABYAAAAWAAAAFgAAABYAAAAaAAAAFgAAABYAAAAWAAAAFgAAABYAAAATAAAAEwAAABMAAAATAAAAEwAAABoAAAAWAAAAFgAAABYAAAAWAAAAGgAAABYAAAAWAAAAFgAAABYAAAAaAAAAEwAAABMAAAATAAAAEwAAABMAAAAaAAAAFgAAABYAAAAWAAAAFgAAABoAAAAWAAAAFgAAABYAAAAWAAAAGgAAABoAAAATAAAAEwAAABMAAAAaAAAAGgAAABoAAAAWAAAAGgAAABoAAAAaAAAAFgAAABYAAAAWAAAAGgAAABkAAAAaAAAAEwAAABMAAAATAAAAGgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAZAAAAGgAAABMAAAATAAAAEwAAABoAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAGQAAABoAAAATAAAAEwAAABMAAAAaAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABkAAAAaAAAAEwAAABMAAAATAAAAGgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAZAAAAFQAAABMAAAATAAAAEwAAABoAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFQAAABoAAAATAAAAEwAAABMAAAAaAAAAGgAAABYAAAAaAAAAGgAAABYAAAAWAAAAGgAAABoAAAAWAAAAFgAAABoAAAAaAAAAEwAAABMAAAATAAAAGgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAaAAAAFgAAABYAAAAXAAAAGgAAABMAAAATAAAAEwAAABoAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAGgAAABYAAAAWAAAAFwAAABoAAAATAAAAEwAAABMAAAAaAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABoAAAAaAAAAFgAAABcAAAATAAAAEwAAABMAAAATAAAAEwAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAaAAAAFgAAABYAAAAXAAAAEwAAABMAAAATAAAAEwAAABMAAAAWAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAGgAAABYAAAAWAAAAFwAAABMAAAATAAAAEwAAABMAAAATAAAAFgAAABYAAAAWAAAAFgAAABYAAAAWAAAAFgAAABoAAAAWAAAAFgAAAA==
+entities:
+- uid: 0
+ type: FakeTestDummy
+ components:
+ - parent: 855
+ pos: -15.5,-11.5
+ rot: -1.5707963267948966 rad
+ type: Transform
+ - type: ContainerContainer
+ typeChange:
+ something: true
+ example:
+ - 0
+ - 1
+ - 2
+ - 3
+ - 4
+ - 855
+ - "contributionC"
+- uid: 1
+ type: FakeTestDummy
+ components:
+ - parent: 0
+ pos: -5.5,-4.5
+ rot: -1.5707963267948966 rad
+ type: Transform
+- uid: 2
+ type: FakeTestDummy
+ components:
+ - parent: 1
+ pos: -15.5,-14.5
+ rot: -1.5707963267948966 rad
+ type: Transform
+ - type: ThereShouldBeTwoOfTheseInOutputOfMergeTestBBB
+- uid: 3
+ type: AnnoyingPlaceholderOnPurpose
+ components:
+ - parent: 0
+ type: Transform
+- uid: 4
+ type: ThisShouldHaveParentChangedToFollowUID
+ components:
+ - parent: 2
+ type: Transform
+- uid: 855
+ components:
+ - name: Saltern Station
+ type: MetaData
+ - parent: null
+ type: Transform
+ - index: 0
+ type: MapGrid
+ - shapes:
+ - !type:PhysShapeGrid
+ grid: 0
+ type: Physics
+
diff --git a/Content.Tools/test/run.sh b/Content.Tools/test/run.sh
new file mode 100755
index 00000000000..7809b4dc92a
--- /dev/null
+++ b/Content.Tools/test/run.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+cp 0A.yml out.yml
+../bin/Debug/net5.0/Content.Tools out.yml 0B.yml 0C.yml
diff --git a/SpaceStation14.sln b/SpaceStation14.sln
index 9736962bea9..2ed361745fb 100644
--- a/SpaceStation14.sln
+++ b/SpaceStation14.sln
@@ -89,6 +89,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XamlX", "RobustToolbox\Xaml
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XamlX.Runtime", "RobustToolbox\XamlX\src\XamlX.Runtime\XamlX.Runtime.csproj", "{440426C1-8DCA-43F6-967F-94439B8DAF47}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Content.Tools", "Content.Tools\Content.Tools.csproj", "{75AB8F8D-9E56-4B12-85E3-E03A852B31CC}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -193,6 +195,10 @@ Global
{440426C1-8DCA-43F6-967F-94439B8DAF47}.Debug|Any CPU.Build.0 = Debug|Any CPU
{440426C1-8DCA-43F6-967F-94439B8DAF47}.Release|Any CPU.ActiveCfg = Release|Any CPU
{440426C1-8DCA-43F6-967F-94439B8DAF47}.Release|Any CPU.Build.0 = Release|Any CPU
+ {75AB8F8D-9E56-4B12-85E3-E03A852B31CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {75AB8F8D-9E56-4B12-85E3-E03A852B31CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {75AB8F8D-9E56-4B12-85E3-E03A852B31CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {75AB8F8D-9E56-4B12-85E3-E03A852B31CC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Tools/mapping-merge-driver.sh b/Tools/mapping-merge-driver.sh
new file mode 100755
index 00000000000..0421278f311
--- /dev/null
+++ b/Tools/mapping-merge-driver.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+# Add this to .git/config:
+# [merge "mapping-merge-driver"]
+# name = Merge driver for maps
+# driver = Tools/mapping-merge-driver.sh %A %O %B
+
+dotnet run --project ./Content.Tools "$@"
+