using System; using System.IO; using System.Collections.Generic; using System.Globalization; using YamlDotNet.RepresentationModel; namespace Content.Tools { public sealed 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"]; YamlTools.MergeYamlMappings(ours, based, otherMapped, path, new[] {"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"]; 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)) return false; return true; case YamlScalarNode subScalar: return MapEntityProperty(subScalar, path); default: throw new ArgumentException($"Unrecognized YAML node type: {node.GetType()} at {path}"); } } } }