diff --git a/CnCTDRAMapEditor/Model/OverlayType.cs b/CnCTDRAMapEditor/Model/OverlayType.cs index 90bd5a7..92ecce2 100644 --- a/CnCTDRAMapEditor/Model/OverlayType.cs +++ b/CnCTDRAMapEditor/Model/OverlayType.cs @@ -47,6 +47,10 @@ namespace MobiusEditor.Model public Image Thumbnail { get; set; } + public String GraphicsSource { get; private set; } + + public int ForceTileNr { get; private set; } + public bool[,] OccupyMask => new bool[1, 1] { { true } }; public bool IsResource => (Flag & (OverlayTypeFlag.TiberiumOrGold | OverlayTypeFlag.Gems)) != OverlayTypeFlag.None; @@ -64,15 +68,22 @@ namespace MobiusEditor.Model // No reason not to allow placing decorations and flag pedestal. public bool IsPlaceable => (Flag & (OverlayTypeFlag.Crate | OverlayTypeFlag.Decoration | OverlayTypeFlag.Flag)) != OverlayTypeFlag.None; - public OverlayType(sbyte id, string name, string textId, TheaterType[] theaters, OverlayTypeFlag flag) + public OverlayType(sbyte id, string name, string textId, TheaterType[] theaters, OverlayTypeFlag flag, String graphicsLoadOverride, int forceTileNr) { ID = id; Name = name; - DisplayName = Globals.TheGameTextManager[textId] + " (" + Name.ToUpperInvariant() + ")"; + GraphicsSource = graphicsLoadOverride == null ? name : graphicsLoadOverride; + ForceTileNr = forceTileNr; + DisplayName = Globals.TheGameTextManager[textId] + " (" + GraphicsSource.ToUpperInvariant() + ")"; Theaters = theaters; Flag = flag; } + public OverlayType(sbyte id, string name, string textId, TheaterType[] theaters, OverlayTypeFlag flag) + :this(id, name, textId, theaters, flag, null, -1) + { + } + public OverlayType(sbyte id, string name, string textId, OverlayTypeFlag flag) : this(id, name, textId, null, flag) { @@ -124,7 +135,8 @@ namespace MobiusEditor.Model public void Init(TheaterType theater) { var oldImage = Thumbnail; - if (Globals.TheTilesetManager.GetTileData(theater.Tilesets, Name, 0, out Tile tile, (Flag & OverlayTypeFlag.Decoration) != 0)) + int tilenr = ForceTileNr == -1 ? 0 : ForceTileNr; + if (Globals.TheTilesetManager.GetTileData(theater.Tilesets, GraphicsSource, tilenr, out Tile tile, (Flag & OverlayTypeFlag.Decoration) != 0)) { var size = tile.Image.Size; var maxSize = new Size(Globals.OriginalTileWidth, Globals.OriginalTileWidth); diff --git a/CnCTDRAMapEditor/Model/Smudge.cs b/CnCTDRAMapEditor/Model/Smudge.cs index 5b1ced6..621fba9 100644 --- a/CnCTDRAMapEditor/Model/Smudge.cs +++ b/CnCTDRAMapEditor/Model/Smudge.cs @@ -47,7 +47,12 @@ namespace MobiusEditor.Model protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - public object Clone() + public object ICloneable.Clone() + { + return Clone(); + } + + public Smudge Clone() { return new Smudge() { diff --git a/CnCTDRAMapEditor/Render/MapRenderer.cs b/CnCTDRAMapEditor/Render/MapRenderer.cs index 0f9f102..5250f92 100644 --- a/CnCTDRAMapEditor/Render/MapRenderer.cs +++ b/CnCTDRAMapEditor/Render/MapRenderer.cs @@ -109,9 +109,7 @@ namespace MobiusEditor.Render var tileSize = new Size(Globals.OriginalTileWidth / tileScale, Globals.OriginalTileHeight / tileScale); var tiberiumOrGoldTypes = map.OverlayTypes.Where(t => t.IsTiberiumOrGold).Select(t => t).ToArray(); var gemTypes = map.OverlayTypes.Where(t => t.IsGem).ToArray(); - var overlappingRenderList = new List<(Rectangle, Action)>(); - Func> renderLocations = null; if (locations != null) { @@ -131,18 +129,15 @@ namespace MobiusEditor.Render } renderLocations = allCells; } - if ((layers & MapLayerFlag.Template) != MapLayerFlag.None) { foreach (var topLeft in renderLocations()) { map.Metrics.GetCell(topLeft, out int cell); - var template = map.Templates[topLeft]; TemplateType ttype = template?.Type ?? map.TemplateTypes.Where(t => t.Flag == TemplateTypeFlag.Clear).FirstOrDefault(); var name = ttype.Name; var icon = template?.Icon ?? ((cell & 0x03) | ((cell >> 4) & 0x0C)); - if (Globals.TheTilesetManager.GetTileData(map.Theater.Tilesets, name, icon, out Tile tile)) { var renderBounds = new Rectangle(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height, tileSize.Width, tileSize.Height); @@ -176,7 +171,6 @@ namespace MobiusEditor.Render { continue; } - if ((overlay.Type.IsResource && ((layers & MapLayerFlag.Resources) != MapLayerFlag.None)) || (overlay.Type.IsWall && ((layers & MapLayerFlag.Walls) != MapLayerFlag.None)) || ((layers & MapLayerFlag.Overlay) != MapLayerFlag.None)) @@ -194,13 +188,11 @@ namespace MobiusEditor.Render { continue; } - string tileName = terrain.Type.Name; if ((terrain.Type.TemplateType & TemplateTypeFlag.OreMine) != TemplateTypeFlag.None) { tileName = "OREMINE"; } - if (Globals.TheTilesetManager.GetTileData(map.Theater.Tilesets, tileName, terrain.Icon, out Tile tile)) { var tint = terrain.Tint; @@ -218,7 +210,6 @@ namespace MobiusEditor.Render ); imageAttributes.SetColorMatrix(colorMatrix); } - var location = new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height); var size = new Size(tile.Image.Width / tileScale, tile.Image.Height / tileScale); var maxSize = new Size(terrain.Type.Size.Width * tileSize.Width, terrain.Type.Size.Height * tileSize.Height); @@ -246,7 +237,6 @@ namespace MobiusEditor.Render } } } - if ((layers & MapLayerFlag.Buildings) != MapLayerFlag.None) { foreach (var (topLeft, building) in map.Buildings.OfType()) @@ -255,11 +245,9 @@ namespace MobiusEditor.Render { continue; } - overlappingRenderList.Add(Render(gameType, map.Theater, topLeft, tileSize, tileScale, building)); } } - if ((layers & MapLayerFlag.Infantry) != MapLayerFlag.None) { foreach (var (topLeft, infantryGroup) in map.Technos.OfType()) @@ -268,7 +256,6 @@ namespace MobiusEditor.Render { continue; } - for (int i = 0; i < infantryGroup.Infantry.Length; ++i) { var infantry = infantryGroup.Infantry[i]; @@ -276,12 +263,10 @@ namespace MobiusEditor.Render { continue; } - overlappingRenderList.Add(Render(map.Theater, topLeft, tileSize, infantry, (InfantryStoppingType)i)); } } } - if ((layers & MapLayerFlag.Units) != MapLayerFlag.None) { foreach (var (topLeft, unit) in map.Technos.OfType()) @@ -290,11 +275,9 @@ namespace MobiusEditor.Render { continue; } - overlappingRenderList.Add(Render(gameType, map.Theater, topLeft, tileSize, unit)); } } - foreach (var (location, renderer) in overlappingRenderList.Where(x => !x.Item1.IsEmpty).OrderBy(x => x.Item1.Bottom)) { renderer(graphics); @@ -323,17 +306,14 @@ namespace MobiusEditor.Render ); imageAttributes.SetColorMatrix(colorMatrix); } - if (Globals.TheTilesetManager.GetTileData(theater.Tilesets, smudge.Type.Name, smudge.Icon, out Tile tile)) { var location = new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height); var smudgeBounds = new Rectangle(location, smudge.Type.RenderSize); - void render(Graphics g) { g.DrawImage(tile.Image, smudgeBounds, 0, 0, tile.Image.Width, tile.Image.Height, GraphicsUnit.Pixel, imageAttributes); } - return (smudgeBounds, render); } else @@ -345,7 +325,7 @@ namespace MobiusEditor.Render public static (Rectangle, Action) Render(TheaterType theater, OverlayType[] tiberiumOrGoldTypes, OverlayType[] gemTypes, Point topLeft, Size tileSize, int tileScale, Overlay overlay) { - var name = overlay.Type.Name; + string name; if (overlay.Type.IsGem) { name = gemTypes[new Random(randomSeed ^ topLeft.GetHashCode()).Next(tiberiumOrGoldTypes.Length)].Name; @@ -354,15 +334,19 @@ namespace MobiusEditor.Render { name = tiberiumOrGoldTypes[new Random(randomSeed ^ topLeft.GetHashCode()).Next(tiberiumOrGoldTypes.Length)].Name; } + else + { + name = overlay.Type.GraphicsSource; + } // For Decoration types, generate dummy if not found. - if (Globals.TheTilesetManager.GetTileData(theater.Tilesets, name, overlay.Icon, out Tile tile, (overlay.Type.Flag & OverlayTypeFlag.Decoration) != 0)) + int icon = overlay.Type.ForceTileNr == -1 ? overlay.Icon : overlay.Type.ForceTileNr; + if (Globals.TheTilesetManager.GetTileData(theater.Tilesets, name, icon, out Tile tile, (overlay.Type.Flag & OverlayTypeFlag.Decoration) != 0)) { var size = (overlay.Type.IsCrate || overlay.Type.IsFlag) ? new Size(tile.Image.Width / tileScale, tile.Image.Height / tileScale) : tileSize; var location = new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height) + new Size(tileSize.Width / 2, tileSize.Height / 2) - new Size(size.Width / 2, size.Height / 2); var overlayBounds = new Rectangle(location, size); - var tint = overlay.Tint; void render(Graphics g) { @@ -380,15 +364,13 @@ namespace MobiusEditor.Render ); imageAttributes.SetColorMatrix(colorMatrix); } - g.DrawImage(tile.Image, overlayBounds, 0, 0, tile.Image.Width, tile.Image.Height, GraphicsUnit.Pixel, imageAttributes); } - return (overlayBounds, render); } else { - Debug.Print(string.Format("Overlay {0} ({1}) not found", overlay.Type.Name, overlay.Icon)); + Debug.Print(string.Format("Overlay {0} ({1}) not found", name, icon)); return (Rectangle.Empty, (g) => { }); } } @@ -396,7 +378,6 @@ namespace MobiusEditor.Render public static (Rectangle, Action) Render(GameType gameType, TheaterType theater, Point topLeft, Size tileSize, int tileScale, Building building) { var tint = building.Tint; - var stringFormat = new StringFormat { Alignment = StringAlignment.Center, @@ -434,7 +415,6 @@ namespace MobiusEditor.Render icon = damageIcon; } } - if (Globals.TheTilesetManager.GetTeamColorTileData(theater.Tilesets, building.Type.Tilename, icon, Globals.TheTeamColorManager[building.House.BuildingTeamColor], out Tile tile)) { var location = new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height); @@ -443,7 +423,6 @@ namespace MobiusEditor.Render // Graphics are too large. Scale them down using the largest dimension. if ((size.Width >= size.Height) && (size.Width > maxSize.Width)) { - size.Height = size.Height * maxSize.Width / size.Width; size.Width = maxSize.Width; } @@ -457,7 +436,6 @@ namespace MobiusEditor.Render int locY = (maxSize.Height - size.Height) / 2 + location.Y; var paintBounds = new Rectangle(locX, locY, size.Width, size.Height); var buildingBounds = new Rectangle(location, maxSize); - Tile factoryOverlayTile = null; // Draw no factory overlay over the collapse frame. if (building.Type.FactoryOverlay != null && (building.Strength > 1 || !hasCollapseFrame)) @@ -470,7 +448,6 @@ namespace MobiusEditor.Render } Globals.TheTilesetManager.GetTeamColorTileData(theater.Tilesets, building.Type.FactoryOverlay, overlayIcon, Globals.TheTeamColorManager[building.House.BuildingTeamColor], out factoryOverlayTile); } - void render(Graphics g) { var imageAttributes = new ImageAttributes(); @@ -504,7 +481,6 @@ namespace MobiusEditor.Render g.DrawString(text, SystemFonts.CaptionFont, fakeTextBrush, textBounds, stringFormat); } } - if (building.BasePriority >= 0) { var text = building.BasePriority.ToString(); @@ -521,26 +497,23 @@ namespace MobiusEditor.Render } } } - return (buildingBounds, render); } else { Debug.Print(string.Format("Building {0} (0) not found", building.Type.Name)); return (Rectangle.Empty, (g) => { }); - } + } } public static (Rectangle, Action) Render(TheaterType theater, Point topLeft, Size tileSize, Infantry infantry, InfantryStoppingType infantryStoppingType) { var icon = HumanShape[Facing32[infantry.Direction.ID]]; - string teamColor = infantry.House?.UnitTeamColor; if (Globals.TheTilesetManager.GetTeamColorTileData(theater.Tilesets, infantry.Type.Name, icon, Globals.TheTeamColorManager[teamColor], out Tile tile)) { var baseLocation = new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height) + new Size(tileSize.Width / 2, tileSize.Height / 2); - var offset = Point.Empty; switch (infantryStoppingType) { @@ -562,7 +535,6 @@ namespace MobiusEditor.Render break; } baseLocation.Offset(offset); - var virtualBounds = new Rectangle( new Point(baseLocation.X - (tile.OpaqueBounds.Width / 2), baseLocation.Y - tile.OpaqueBounds.Height), tile.OpaqueBounds.Size @@ -571,7 +543,6 @@ namespace MobiusEditor.Render baseLocation - new Size(infantry.Type.RenderSize.Width / 2, infantry.Type.RenderSize.Height / 2), infantry.Type.RenderSize ); - var tint = infantry.Tint; void render(Graphics g) { @@ -591,7 +562,6 @@ namespace MobiusEditor.Render } g.DrawImage(tile.Image, renderBounds, 0, 0, tile.Image.Width, tile.Image.Height, GraphicsUnit.Pixel, imageAttributes); } - return (virtualBounds, render); } else @@ -692,7 +662,6 @@ namespace MobiusEditor.Render location - new Size(unit.Type.RenderSize.Width / 2, unit.Type.RenderSize.Height / 2), unit.Type.RenderSize ); - Tile radarTile = null; if ((unit.Type == RedAlert.UnitTypes.MGG) || (unit.Type == RedAlert.UnitTypes.MRJammer) || @@ -700,7 +669,6 @@ namespace MobiusEditor.Render { Globals.TheTilesetManager.GetTeamColorTileData(theater.Tilesets, unit.Type.Name, 32, Globals.TheTeamColorManager[teamColor], out radarTile); } - Tile turretTile = null; if (unit.Type.HasTurret) { @@ -777,7 +745,7 @@ namespace MobiusEditor.Render { if (unit.Type.IsVessel) { - + // TODO } else if (unit.Type == RedAlert.UnitTypes.Jeep) { @@ -798,17 +766,14 @@ namespace MobiusEditor.Render turretAdjust = TurretAdjust[Facing32[unit.Direction.ID]]; } } - var turretBounds = renderBounds; turretBounds.Offset( turretAdjust.X * tileSize.Width / Globals.PixelWidth, turretAdjust.Y * tileSize.Height / Globals.PixelHeight ); - g.DrawImage(turretTile.Image, turretBounds, 0, 0, tile.Image.Width, tile.Image.Height, GraphicsUnit.Pixel, imageAttributes); } } - return (renderBounds, render); } else diff --git a/CnCTDRAMapEditor/TiberianDawn/GamePlugin.cs b/CnCTDRAMapEditor/TiberianDawn/GamePlugin.cs index 0ad9d0a..c6d261e 100644 --- a/CnCTDRAMapEditor/TiberianDawn/GamePlugin.cs +++ b/CnCTDRAMapEditor/TiberianDawn/GamePlugin.cs @@ -188,7 +188,8 @@ namespace MobiusEditor.TiberianDawn using (var iniReader = new StreamReader(iniPath)) using (var binReader = new BinaryReader(new FileStream(binPath, FileMode.Open, FileAccess.Read))) { - ini.Parse(iniReader); + string iniText = FixRoad2Load(iniReader); + ini.Parse(iniText); errors.AddRange(LoadINI(ini)); LoadBinary(binReader); } @@ -221,6 +222,78 @@ namespace MobiusEditor.TiberianDawn return errors; } + private string FixRoad2Load(StreamReader iniReader) + { + string iniText = iniReader.ReadToEnd(); + string[] iniTextArr = iniText.Replace("\r\n", "\n").Replace('\r', '\n').Split('\n'); + Dictionary dupeRoadDetect = new Dictionary(); + Regex roadRegex = new Regex("^\\s*(\\d+)\\s*=\\s*" + OverlayTypes.Road.Name + "\\s*$", RegexOptions.IgnoreCase); + string road2dummy = "=" + OverlayTypes.Road2.Name.ToUpper(); + bool inOverlay = false; + for (int i = 0; i < iniTextArr.Length; ++i) + { + string currLine = iniTextArr[i].Trim(); + if (currLine.StartsWith("[")) + { + inOverlay = "[Overlay]".Equals(currLine, StringComparison.InvariantCultureIgnoreCase); + continue; + } + if (inOverlay) + { + Match match = roadRegex.Match(currLine); + if (match.Success) + { + int cellNumber = Int32.Parse(match.Groups[1].Value); + int cur = dupeRoadDetect.TryGetValue(cellNumber, out int curVal) ? curVal : 0; + dupeRoadDetect[cellNumber] = cur + 1; + } + } + } + if (dupeRoadDetect.Any(k => k.Value > 1)) + { + inOverlay = false; + List newIniText = new List(); + for (int i = 0; i < iniTextArr.Length; ++i) + { + string currLine = iniTextArr[i].Trim(); + if (currLine.StartsWith("[")) + { + inOverlay = "[Overlay]".Equals(currLine, StringComparison.InvariantCultureIgnoreCase); + } + Match match; + if (!inOverlay || !(match = roadRegex.Match(iniTextArr[i])).Success) + { + newIniText.Add(currLine); + } + else + { + int cellNumber = Int32.Parse(match.Groups[1].Value); + int roadAmount; + if (dupeRoadDetect.TryGetValue(cellNumber, out roadAmount)) + { + if (roadAmount == 1) + { + newIniText.Add(currLine); + } + else if (roadAmount > 1) + { + newIniText.Add(cellNumber + road2dummy); + // Ensures TryGetValue succeeds, but nothing is written for any following matches. + dupeRoadDetect[cellNumber] = -1; + } + } + } + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < newIniText.Count; ++i) + { + sb.Append(newIniText[i]).Append("\r\n"); + } + iniText = sb.ToString(); + } + return iniText; + } + private IEnumerable LoadINI(INI ini) { var errors = new List(); @@ -878,7 +951,7 @@ namespace MobiusEditor.TiberianDawn Dictionary correctedEdges = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var edge in Globals.Edges) correctedEdges.Add(edge, edge); - String defaultEdge = Globals.Edges.FirstOrDefault() ?? String.Empty; + string defaultEdge = Globals.Edges.FirstOrDefault() ?? string.Empty; foreach (var house in Map.Houses) { if (house.Type.ID < 0) @@ -956,7 +1029,8 @@ namespace MobiusEditor.TiberianDawn SaveINI(ini, fileType); using (var iniWriter = new StreamWriter(iniPath)) { - iniWriter.Write(ini.ToString()); + //iniWriter.Write(ini.ToString()); + FixRoad2Save(ini, iniWriter); } using (var binStream = new FileStream(binPath, FileMode.Create)) @@ -1026,6 +1100,36 @@ namespace MobiusEditor.TiberianDawn return true; } + private void FixRoad2Save(INI ini, StreamWriter iniWriter) + { + // Special code to make the second state of ROAD cells work. + string roadLine = "=" + OverlayTypes.Road.Name.ToUpperInvariant() + "\r\n"; + Regex roadDetect = new Regex("^\\s*(\\d+)\\s*=\\s*" + OverlayTypes.Road2.Name + "\\s*$", RegexOptions.IgnoreCase); + string[] iniString = ini.ToString().Replace("\r\n", "\n").Replace('\r', '\n').Split('\n'); + bool inOverlay = false; + for (int i = 0; i < iniString.Length; i++) + { + string currLine = iniString[i].Trim(); + if (currLine.StartsWith("[")) + { + inOverlay = "[Overlay]".Equals(currLine, StringComparison.InvariantCultureIgnoreCase); + } + Match match; + if (inOverlay && (match = roadDetect.Match(currLine)).Success) + { + string newRoad = match.Groups[1].Value + roadLine; + // Write twice to achieve second state. + iniWriter.Write(newRoad); + iniWriter.Write(newRoad); + } + else + { + iniWriter.Write(currLine); + iniWriter.Write("\r\n"); + } + } + } + private void SaveINI(INI ini, FileType fileType) { if (extraSections != null) @@ -1245,7 +1349,7 @@ namespace MobiusEditor.TiberianDawn Random rd = new Random(); foreach (var (cell, overlay) in Map.Overlay) { - String overlayName = overlay.Type.Name; + string overlayName = overlay.Type.Name; if (tiberium.IsMatch(overlayName)) overlayName = "TI" + rd.Next(1, 13); overlaySection[cell.ToString()] = overlayName.ToUpperInvariant(); diff --git a/CnCTDRAMapEditor/TiberianDawn/OverlayTypes.cs b/CnCTDRAMapEditor/TiberianDawn/OverlayTypes.cs index cfac1f7..f4d4537 100644 --- a/CnCTDRAMapEditor/TiberianDawn/OverlayTypes.cs +++ b/CnCTDRAMapEditor/TiberianDawn/OverlayTypes.cs @@ -41,6 +41,7 @@ namespace MobiusEditor.TiberianDawn public static readonly OverlayType Tiberium11 = new OverlayType(16, "ti11", OverlayTypeFlag.TiberiumOrGold); public static readonly OverlayType Tiberium12 = new OverlayType(17, "ti12", OverlayTypeFlag.TiberiumOrGold); public static readonly OverlayType Road = new OverlayType(18, "road", "Concrete Road", OverlayTypeFlag.Decoration); + public static readonly OverlayType Road2 = new OverlayType(19, "road2", "Concrete Road (full)", null, OverlayTypeFlag.Decoration, "road", 1); // Not available to place down sadly. //public static readonly OverlayType Squishy = new OverlayType(19, "SQUISH", OverlayTypeFlag.Decoration); public static readonly OverlayType V12 = new OverlayType(20, "v12", "TEXT_STRUCTURE_TITLE_CIV12", new TheaterType[] { TheaterTypes.Temperate }); diff --git a/CnCTDRAMapEditor/Utility/Tileset.cs b/CnCTDRAMapEditor/Utility/Tileset.cs index 1179946..18d2bfa 100644 --- a/CnCTDRAMapEditor/Utility/Tileset.cs +++ b/CnCTDRAMapEditor/Utility/Tileset.cs @@ -41,6 +41,8 @@ namespace MobiusEditor.Utility public class Tileset { + private static string DummyFormatTga = "DATA\\ART\\TEXTURES\\SRGB\\FALLBACK_DUMMY\\{0}_{1:D4}.tga"; + private class TileData { public int FPS { get; set; } @@ -119,27 +121,45 @@ namespace MobiusEditor.Utility { return false; } - if (!this.tiles.TryGetValue(name, out Dictionary shapes) && !generateFallback) + Dictionary shapes; + if (!this.tiles.TryGetValue(name, out shapes) && !generateFallback) { return false; } + name = name.ToUpperInvariant(); if (shapes == null && generateFallback) { + if (shape < 0) + { + shape = 0; + } + string dummy = String.Format(DummyFormatTga, name, shape); shapes = new Dictionary(); TileData dummyData = new TileData(); - dummyData.Frames = new string[] { "DATA\\ART\\TEXTURES\\SRGB\\FALLBACK_DUMMY\\" + name + "_" + shape.ToString("D4") + ".tga" }; - shapes.Add(0, dummyData); + dummyData.Frames = new string[] { dummy }; + shapes.Add(shape, dummyData); + // Add it, so it's present for the next lookup. + this.tiles[name] = shapes; } - if (shape < 0) { shape = Math.Max(0, shapes.Max(kv => kv.Key) + shape + 1); } if (!shapes.TryGetValue(shape, out TileData tileData)) { - return false; + if (generateFallback) + { + // Shape was found, but specific frame was not. Add it. + string dummy = String.Format(DummyFormatTga, name, shape); + tileData = new TileData(); + tileData.Frames = new string[] { dummy }; + shapes.Add(shape, tileData); + } + else + { + return false; + } } - var key = teamColor?.Name ?? string.Empty; if (!tileData.TeamColorTiles.TryGetValue(key, out Tile[] tileDataTiles)) {