diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
index 0ee62bda50..56e8a65815 100644
--- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
+++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
@@ -617,6 +617,7 @@
+
diff --git a/OpenRA.Mods.Common/UtilityCommands/FixClassicTilesets.cs b/OpenRA.Mods.Common/UtilityCommands/FixClassicTilesets.cs
new file mode 100644
index 0000000000..6b0f95ce8b
--- /dev/null
+++ b/OpenRA.Mods.Common/UtilityCommands/FixClassicTilesets.cs
@@ -0,0 +1,104 @@
+#region Copyright & License Information
+/*
+ * Copyright 2007-2015 The OpenRA Developers (see AUTHORS)
+ * This file is part of OpenRA, which is free software. It is made
+ * available to you under the terms of the GNU General Public License
+ * as published by the Free Software Foundation. For more information,
+ * see COPYING.
+ */
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using OpenRA.FileSystem;
+using OpenRA.Graphics;
+using OpenRA.Traits;
+using StyleCop;
+
+namespace OpenRA.Mods.Common.UtilityCommands
+{
+ class FixClassicTilesets : IUtilityCommand
+ {
+ public string Name { get { return "--fix-classic-tilesets"; } }
+
+ [Desc("Fixes missing template tile definitions and adds filename extensions.")]
+ public void Run(ModData modData, string[] args)
+ {
+ // HACK: The engine code assumes that Game.modData is set.
+ Game.ModData = modData;
+ GlobalFileSystem.LoadFromManifest(Game.ModData.Manifest);
+
+ var imageField = typeof(TerrainTemplateInfo).GetField("Image");
+ var pickAnyField = typeof(TerrainTemplateInfo).GetField("PickAny");
+ var tileInfoField = typeof(TerrainTemplateInfo).GetField("tileInfo", BindingFlags.NonPublic | BindingFlags.Instance);
+ var terrainTypeField = typeof(TerrainTileInfo).GetField("TerrainType");
+ var terrainLeftColorField = typeof(TerrainTileInfo).GetField("LeftColor");
+ var terrainRightColorField = typeof(TerrainTileInfo).GetField("RightColor");
+ var empty = new Size(0, 0);
+ var single = new int2(1, 1);
+
+ foreach (var t in Game.ModData.Manifest.TileSets)
+ {
+ var ts = new TileSet(Game.ModData, t);
+ var exts = new[] { "" }.Concat(ts.Extensions);
+ var frameCache = new FrameCache(Game.ModData.SpriteLoaders, ts.Extensions);
+
+ Console.WriteLine("Tileset: " + ts.Name);
+ foreach (var template in ts.Templates.Values)
+ {
+ // Find the sprite associated with this template
+ foreach (var ext in exts)
+ {
+ Stream s;
+ if (!GlobalFileSystem.TryOpenWithExts(template.Image, new[] { ext }, out s))
+ continue;
+
+ // Rewrite the template image (normally readonly) using reflection
+ imageField.SetValue(template, template.Image + ext);
+
+ // Fetch the private tileInfo array so that we can write new entries
+ var tileInfo = (TerrainTileInfo[])tileInfoField.GetValue(template);
+
+ // Open the file and search for any implicit frames
+ var allFrames = frameCache[template.Image];
+ var frames = template.Frames != null ? template.Frames.Select(f => allFrames[f]).ToArray() : allFrames;
+
+ // Resize array for new entries
+ if (frames.Length > template.TilesCount)
+ {
+ var oldLength = template.TilesCount;
+ var ti = new TerrainTileInfo[frames.Length];
+ Array.Copy(tileInfo, ti, template.TilesCount);
+ tileInfoField.SetValue(template, ti);
+ tileInfo = ti;
+ }
+
+ for (var i = 0; i < template.TilesCount; i++)
+ {
+ if (template[i] == null && frames[i] != null && frames[i].Size != empty)
+ {
+ tileInfo[i] = new TerrainTileInfo();
+ var ti = ts.GetTerrainIndex("Clear");
+ terrainTypeField.SetValue(tileInfo[i], ti);
+ terrainLeftColorField.SetValue(tileInfo[i], ts[ti].Color);
+ terrainRightColorField.SetValue(tileInfo[i], ts[ti].Color);
+ Console.WriteLine("Fixing entry for {0}:{1}", template.Image, i);
+ }
+ }
+
+ if (template.TilesCount > 1 && template.Size == single)
+ pickAnyField.SetValue(template, true);
+
+ s.Dispose();
+ }
+ }
+
+ ts.Save(t);
+ }
+ }
+ }
+}