CnC_Remastered_Collection

Command and Conquer: Red Alert
Log | Files | Refs | README | LICENSE

MapRenderer.cs (37089B)


      1 //
      2 // Copyright 2020 Electronic Arts Inc.
      3 //
      4 // The Command & Conquer Map Editor and corresponding source code is free 
      5 // software: you can redistribute it and/or modify it under the terms of 
      6 // the GNU General Public License as published by the Free Software Foundation, 
      7 // either version 3 of the License, or (at your option) any later version.
      8 
      9 // The Command & Conquer Map Editor and corresponding source code is distributed 
     10 // in the hope that it will be useful, but with permitted additional restrictions 
     11 // under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT 
     12 // distributed with this program. You should have received a copy of the 
     13 // GNU General Public License along with permitted additional restrictions 
     14 // with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
     15 using MobiusEditor.Interface;
     16 using MobiusEditor.Model;
     17 using MobiusEditor.Utility;
     18 using System;
     19 using System.Collections.Generic;
     20 using System.Diagnostics;
     21 using System.Drawing;
     22 using System.Drawing.Imaging;
     23 using System.Linq;
     24 
     25 namespace MobiusEditor.Render
     26 {
     27     public static class MapRenderer
     28     {
     29         private static readonly int[] Facing16 = new int[256]
     30         {
     31             0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,
     32             2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,
     33             4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,
     34             6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,
     35             8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,
     36             10,10,10,10,10,10,10,10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,12,12,12,12,12,12,12,12,
     37             12,12,12,12,12,12,12,12,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,14,14,14,14,14,14,14,14,
     38             14,14,14,14,14,14,14,14,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,0,0,0,0,0,0,0,0
     39         };
     40 
     41         private static readonly int[] Facing32 = new int[256]
     42         {
     43             0,0,0,0,0,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,
     44             3,4,4,4,4,4,4,5,5,5,5,5,5,5,6,6,6,6,6,6,6,7,7,7,7,7,7,7,8,8,8,8,
     45             8,8,8,9,9,9,9,9,9,9,10,10,10,10,10,10,10,11,11,11,11,11,11,11,12,12,12,12,12,12,12,12,
     46             13,13,13,13,13,13,13,13,14,14,14,14,14,14,14,14,14,15,15,15,15,15,15,15,15,15,16,16,16,16,16,16,
     47             16,16,16,16,16,17,17,17,17,17,17,17,17,17,18,18,18,18,18,18,18,18,18,19,19,19,19,19,19,19,19,19,
     48             19,20,20,20,20,20,20,21,21,21,21,21,21,21,22,22,22,22,22,22,22,23,23,23,23,23,23,23,24,24,24,24,
     49             24,24,24,25,25,25,25,25,25,25,26,26,26,26,26,26,26,27,27,27,27,27,27,27,28,28,28,28,28,28,28,28,
     50             29,29,29,29,29,29,29,29,30,30,30,30,30,30,30,30,30,31,31,31,31,31,31,31,31,31,0,0,0,0,0,0
     51         };
     52 
     53         private static readonly int[] HumanShape = new int[32]
     54         {
     55             0,0,7,7,7,7,6,6,6,6,5,5,5,5,5,4,4,4,3,3,3,3,2,2,2,2,1,1,1,1,1,0
     56         };
     57 
     58         private static readonly int[] BodyShape = new int[32]
     59         {
     60             0,31,30,29,28,27,26,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1
     61         };
     62 
     63         private static readonly Point[] TurretAdjust = new Point[]
     64         {
     65             new Point(1, 2),	// N
     66             new Point(-1, 1),
     67             new Point(-2, 0),
     68             new Point(-3, 0),
     69             new Point(-3, 1),	// NW
     70             new Point(-4, -1),
     71             new Point(-4, -1),
     72             new Point(-5, -2),
     73             new Point(-5, -3),	// W
     74             new Point(-5, -3),
     75             new Point(-3, -3),
     76             new Point(-3, -4),
     77             new Point(-3, -4),	// SW
     78             new Point(-3, -5),
     79             new Point(-2, -5),
     80             new Point(-1, -5),
     81             new Point(0, -5),	// S
     82             new Point(1, -6),
     83             new Point(2, -5),
     84             new Point(3, -5),
     85             new Point(4, -5),	// SE
     86             new Point(6, -4),
     87             new Point(6, -3),
     88             new Point(6, -3),
     89             new Point(6, -3),	// E
     90             new Point(5, -1),
     91             new Point(5, -1),
     92             new Point(4, 0),
     93             new Point(3, 0),	// NE
     94             new Point(2, 0),
     95             new Point(2, 1),
     96             new Point(1, 2)
     97         };
     98 
     99         private static readonly int[] tiberiumCounts = new int[] { 0, 1, 3, 4, 6, 7, 8, 10, 11 };
    100         private static readonly int randomSeed;
    101 
    102         static MapRenderer()
    103         {
    104             randomSeed = Guid.NewGuid().GetHashCode();
    105         }
    106 
    107         public static void Render(GameType gameType, Map map, Graphics graphics, ISet<Point> locations, MapLayerFlag layers, int tileScale)
    108         {
    109             var tileSize = new Size(Globals.OriginalTileWidth / tileScale, Globals.OriginalTileHeight / tileScale);
    110             var tiberiumOrGoldTypes = map.OverlayTypes.Where(t => t.IsTiberiumOrGold).Select(t => t).ToArray();
    111             var gemTypes = map.OverlayTypes.Where(t => t.IsGem).ToArray();
    112 
    113             var overlappingRenderList = new List<(Rectangle, Action<Graphics>)>();
    114 
    115             Func<IEnumerable<Point>> renderLocations = null;
    116             if (locations != null)
    117             {
    118                 renderLocations = () => locations;
    119             }
    120             else
    121             {
    122                 IEnumerable<Point> allCells()
    123                 {
    124                     for (var y = 0; y < map.Metrics.Height; ++y)
    125                     {
    126                         for (var x = 0; x < map.Metrics.Width; ++x)
    127                         {
    128                             yield return new Point(x, y);
    129                         }
    130                     }
    131                 }
    132 
    133                 renderLocations = allCells;
    134             }
    135 
    136             if ((layers & MapLayerFlag.Template) != MapLayerFlag.None)
    137             {
    138                 foreach (var topLeft in renderLocations())
    139                 {
    140                     map.Metrics.GetCell(topLeft, out int cell);
    141 
    142                     var template = map.Templates[topLeft];
    143                     var name = template?.Type.Name ?? map.TemplateTypes.Where(t => t.Equals("clear1")).FirstOrDefault().Name;
    144                     var icon = template?.Icon ?? ((cell & 0x03) | ((cell >> 4) & 0x0C));
    145 
    146                     if (Globals.TheTilesetManager.GetTileData(map.Theater.Tilesets, name, icon, out Tile tile))
    147                     {
    148                         var renderBounds = new Rectangle(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height, tileSize.Width, tileSize.Height);
    149                         graphics.DrawImage(tile.Image, renderBounds);
    150                     }
    151                     else
    152                     {
    153                         Debug.Print(string.Format("Template {0} ({1}) not found", name, icon));
    154                     }
    155                 }
    156             }
    157 
    158             if ((layers & MapLayerFlag.Smudge) != MapLayerFlag.None)
    159             {
    160                 foreach (var topLeft in renderLocations())
    161                 {
    162                     var smudge = map.Smudge[topLeft];
    163                     if (smudge != null)
    164                     {
    165                         Render(map.Theater, topLeft, tileSize, smudge).Item2(graphics);
    166                     }
    167                 }
    168             }
    169 
    170             if ((layers & MapLayerFlag.OverlayAll) != MapLayerFlag.None)
    171             {
    172                 foreach (var topLeft in renderLocations())
    173                 {
    174                     var overlay = map.Overlay[topLeft];
    175                     if (overlay == null)
    176                     {
    177                         continue;
    178                     }
    179 
    180                     if ((overlay.Type.IsResource && ((layers & MapLayerFlag.Resources) != MapLayerFlag.None)) ||
    181                         (overlay.Type.IsWall && ((layers & MapLayerFlag.Walls) != MapLayerFlag.None)) ||
    182                         ((layers & MapLayerFlag.Overlay) != MapLayerFlag.None))
    183                     {
    184                         Render(map.Theater, tiberiumOrGoldTypes, gemTypes, topLeft, tileSize, tileScale, overlay).Item2(graphics);
    185                     }
    186                 }
    187             }
    188 
    189             if ((layers & MapLayerFlag.Terrain) != MapLayerFlag.None)
    190             {
    191                 foreach (var (topLeft, terrain) in map.Technos.OfType<Terrain>())
    192                 {
    193                     if ((locations != null) && !locations.Contains(topLeft))
    194                     {
    195                         continue;
    196                     }
    197 
    198                     string tileName = terrain.Type.Name;
    199                     if ((terrain.Type.TemplateType & TemplateTypeFlag.OreMine) != TemplateTypeFlag.None)
    200                     {
    201                         tileName = "OREMINE";
    202                     }
    203 
    204                     if (Globals.TheTilesetManager.GetTileData(map.Theater.Tilesets, tileName, terrain.Icon, out Tile tile))
    205                     {
    206                         var tint = terrain.Tint;
    207                         var imageAttributes = new ImageAttributes();
    208                         if (tint != Color.White)
    209                         {
    210                             var colorMatrix = new ColorMatrix(new float[][]
    211                             {
    212                                 new float[] {tint.R / 255.0f, 0, 0, 0, 0},
    213                                 new float[] {0, tint.G / 255.0f, 0, 0, 0},
    214                                 new float[] {0, 0, tint.B / 255.0f, 0, 0},
    215                                 new float[] {0, 0, 0, tint.A / 255.0f, 0},
    216                                 new float[] {0, 0, 0, 0, 1},
    217                             }
    218                             );
    219                             imageAttributes.SetColorMatrix(colorMatrix);
    220                         }
    221 
    222                         var location = new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height);
    223                         var size = new Size(tile.Image.Width / tileScale, tile.Image.Height / tileScale);
    224                         var terrainBounds = new Rectangle(location, size);
    225                         overlappingRenderList.Add((terrainBounds, g => g.DrawImage(tile.Image, terrainBounds, 0, 0, tile.Image.Width, tile.Image.Height, GraphicsUnit.Pixel, imageAttributes)));
    226                     }
    227                     else
    228                     {
    229                         Debug.Print(string.Format("Terrain {0} ({1}) not found", tileName, terrain.Icon));
    230                     }
    231                 }
    232             }
    233 
    234             if ((layers & MapLayerFlag.Buildings) != MapLayerFlag.None)
    235             {
    236                 foreach (var (topLeft, building) in map.Buildings.OfType<Building>())
    237                 {
    238                     if ((locations != null) && !locations.Contains(topLeft))
    239                     {
    240                         continue;
    241                     }
    242 
    243                     overlappingRenderList.Add(Render(gameType, map.Theater, topLeft, tileSize, tileScale, building));
    244                 }
    245             }
    246 
    247             if ((layers & MapLayerFlag.Infantry) != MapLayerFlag.None)
    248             {
    249                 foreach (var (topLeft, infantryGroup) in map.Technos.OfType<InfantryGroup>())
    250                 {
    251                     if ((locations != null) && !locations.Contains(topLeft))
    252                     {
    253                         continue;
    254                     }
    255 
    256                     for (int i = 0; i < infantryGroup.Infantry.Length; ++i)
    257                     {
    258                         var infantry = infantryGroup.Infantry[i];
    259                         if (infantry == null)
    260                         {
    261                             continue;
    262                         }
    263 
    264                         overlappingRenderList.Add(Render(map.Theater, topLeft, tileSize, infantry, (InfantryStoppingType)i));
    265                     }
    266                 }
    267             }
    268 
    269             if ((layers & MapLayerFlag.Units) != MapLayerFlag.None)
    270             {
    271                 foreach (var (topLeft, unit) in map.Technos.OfType<Unit>())
    272                 {
    273                     if ((locations != null) && !locations.Contains(topLeft))
    274                     {
    275                         continue;
    276                     }
    277 
    278                     overlappingRenderList.Add(Render(gameType, map.Theater, topLeft, tileSize, unit));
    279                 }
    280             }
    281 
    282             foreach (var (location, renderer) in overlappingRenderList.Where(x => !x.Item1.IsEmpty).OrderBy(x => x.Item1.Bottom))
    283             {
    284                 renderer(graphics);
    285             }
    286         }
    287 
    288         public static void Render(GameType gameType, Map map, Graphics graphics, ISet<Point> locations, MapLayerFlag layers)
    289         {
    290             Render(gameType, map, graphics, locations, layers, Globals.TileScale);
    291         }
    292 
    293         public static (Rectangle, Action<Graphics>) Render(TheaterType theater, Point topLeft, Size tileSize, Smudge smudge)
    294         {
    295             var tint = smudge.Tint;
    296             var imageAttributes = new ImageAttributes();
    297             if (tint != Color.White)
    298             {
    299                 var colorMatrix = new ColorMatrix(new float[][]
    300                 {
    301                     new float[] {tint.R / 255.0f, 0, 0, 0, 0},
    302                     new float[] {0, tint.G / 255.0f, 0, 0, 0},
    303                     new float[] {0, 0, tint.B / 255.0f, 0, 0},
    304                     new float[] {0, 0, 0, tint.A / 255.0f, 0},
    305                     new float[] {0, 0, 0, 0, 1},
    306                 }
    307                 );
    308                 imageAttributes.SetColorMatrix(colorMatrix);
    309             }
    310 
    311             if (Globals.TheTilesetManager.GetTileData(theater.Tilesets, smudge.Type.Name, smudge.Icon, out Tile tile))
    312             {
    313                 var location = new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height);
    314                 var smudgeBounds = new Rectangle(location, smudge.Type.RenderSize);
    315 
    316                 void render(Graphics g)
    317                 {
    318                     g.DrawImage(tile.Image, smudgeBounds, 0, 0, tile.Image.Width, tile.Image.Height, GraphicsUnit.Pixel, imageAttributes);
    319                 }
    320 
    321                 return (smudgeBounds, render);
    322             }
    323             else
    324             {
    325                 Debug.Print(string.Format("Smudge {0} ({1}) not found", smudge.Type.Name, smudge.Icon));
    326                 return (Rectangle.Empty, (g) => { });
    327             }
    328         }
    329 
    330         public static (Rectangle, Action<Graphics>) Render(TheaterType theater, OverlayType[] tiberiumOrGoldTypes, OverlayType[] gemTypes, Point topLeft, Size tileSize, int tileScale, Overlay overlay)
    331         {
    332             var name = overlay.Type.Name;
    333             if (overlay.Type.IsGem)
    334             {
    335                 name = gemTypes[new Random(randomSeed ^ topLeft.GetHashCode()).Next(tiberiumOrGoldTypes.Length)].Name;
    336             }
    337             else if (overlay.Type.IsTiberiumOrGold)
    338             {
    339                 name = tiberiumOrGoldTypes[new Random(randomSeed ^ topLeft.GetHashCode()).Next(tiberiumOrGoldTypes.Length)].Name;
    340             }
    341 
    342             if (Globals.TheTilesetManager.GetTileData(theater.Tilesets, name, overlay.Icon, out Tile tile))
    343             {
    344                 var size = (overlay.Type.IsCrate || overlay.Type.IsFlag) ? new Size(tile.Image.Width / tileScale, tile.Image.Height / tileScale) : tileSize;
    345                 var location = new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height)
    346                     + new Size(tileSize.Width / 2, tileSize.Height / 2)
    347                     - new Size(size.Width / 2, size.Height / 2);
    348                 var overlayBounds = new Rectangle(location, size);
    349 
    350                 var tint = overlay.Tint;
    351                 void render(Graphics g)
    352                 {
    353                     var imageAttributes = new ImageAttributes();
    354                     if (tint != Color.White)
    355                     {
    356                         var colorMatrix = new ColorMatrix(new float[][]
    357                         {
    358                             new float[] {tint.R / 255.0f, 0, 0, 0, 0},
    359                             new float[] {0, tint.G / 255.0f, 0, 0, 0},
    360                             new float[] {0, 0, tint.B / 255.0f, 0, 0},
    361                             new float[] {0, 0, 0, tint.A / 255.0f, 0},
    362                             new float[] {0, 0, 0, 0, 1},
    363                         }
    364                         );
    365                         imageAttributes.SetColorMatrix(colorMatrix);
    366                     }
    367 
    368                     g.DrawImage(tile.Image, overlayBounds, 0, 0, tile.Image.Width, tile.Image.Height, GraphicsUnit.Pixel, imageAttributes);
    369                 }
    370 
    371                 return (overlayBounds, render);
    372             }
    373             else
    374             {
    375                 Debug.Print(string.Format("Overlay {0} ({1}) not found", overlay.Type.Name, overlay.Icon));
    376                 return (Rectangle.Empty, (g) => { });
    377             }
    378         }
    379 
    380         public static (Rectangle, Action<Graphics>) Render(GameType gameType, TheaterType theater, Point topLeft, Size tileSize, int tileScale, Building building)
    381         {
    382             var tint = building.Tint;
    383 
    384             var stringFormat = new StringFormat
    385             {
    386                 Alignment = StringAlignment.Center,
    387                 LineAlignment = StringAlignment.Center
    388             };
    389             var fakeBackgroundBrush = new SolidBrush(Color.FromArgb(building.Tint.A / 2, Color.Black));
    390             var fakeTextBrush = new SolidBrush(Color.FromArgb(building.Tint.A, Color.White));
    391             var baseBackgroundBrush = new SolidBrush(Color.FromArgb(building.Tint.A / 2, Color.Black));
    392             var baseTextBrush = new SolidBrush(Color.FromArgb(building.Tint.A, Color.Red));
    393 
    394             var icon = 0;
    395             if (building.Type.HasTurret)
    396             {
    397                 icon = BodyShape[Facing32[building.Direction.ID]];
    398                 if (building.Strength < 128)
    399                 {
    400                     switch (gameType)
    401                     {
    402                         case GameType.TiberianDawn:
    403                             icon += 64;
    404                             break;
    405                         case GameType.RedAlert:
    406                             icon += building.Type.Equals("sam") ? 35 : 64;
    407                             break;
    408                     }
    409                 }
    410             }
    411             else
    412             {
    413                 if (building.Strength <= 1)
    414                 {
    415                     icon = -1;
    416                 }
    417                 else if (building.Strength < 128)
    418                 {
    419                     icon = -2;
    420                     if (building.Type.Equals("weap") || building.Type.Equals("weaf"))
    421                     {
    422                         icon = 1;
    423                     }
    424                     else if ((gameType == GameType.TiberianDawn) && building.Type.Equals("proc"))
    425                     {
    426                         icon = 30;
    427                     }
    428                     else if (building.Type.Equals("eye"))
    429                     {
    430                         icon = 16;
    431                     }
    432                     else if (building.Type.Equals("silo"))
    433                     {
    434                         icon = 5;
    435                     }
    436                     else if (building.Type.Equals("fix"))
    437                     {
    438                         icon = 7;
    439                     }
    440                     else if (building.Type.Equals("v19"))
    441                     {
    442                         icon = 14;
    443                     }
    444                 }
    445             }
    446 
    447             if (Globals.TheTilesetManager.GetTeamColorTileData(theater.Tilesets, building.Type.Tilename, icon, Globals.TheTeamColorManager[building.House.BuildingTeamColor], out Tile tile))
    448             {
    449                 var location = new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height);
    450                 var size = new Size(tile.Image.Width / tileScale, tile.Image.Height / tileScale);
    451                 var maxSize = new Size(building.Type.Size.Width * tileSize.Width, building.Type.Size.Height * tileSize.Height);
    452                 if ((size.Width >= size.Height) && (size.Width > maxSize.Width))
    453                 {
    454                     size.Height = size.Height * maxSize.Width / size.Width;
    455                     size.Width = maxSize.Width;
    456                 }
    457                 else if ((size.Height >= size.Width) && (size.Height > maxSize.Height))
    458                 {
    459                     size.Width = size.Width * maxSize.Height / size.Height;
    460                     size.Height = maxSize.Height;
    461                 }
    462                 var buildingBounds = new Rectangle(location, size);
    463 
    464                 Tile factoryOverlayTile = null;
    465                 if (building.Type.FactoryOverlay != null)
    466                 {
    467                     int overlayIcon = 0;
    468                     if (building.Strength < 128)
    469                     {
    470                         switch (gameType)
    471                         {
    472                             case GameType.TiberianDawn:
    473                                 overlayIcon = 10;
    474                                 break;
    475                             case GameType.RedAlert:
    476                                 overlayIcon = 4;
    477                                 break;
    478                         }
    479                     }
    480 
    481                     Globals.TheTilesetManager.GetTeamColorTileData(theater.Tilesets, building.Type.FactoryOverlay, overlayIcon, Globals.TheTeamColorManager[building.House.BuildingTeamColor], out factoryOverlayTile);
    482                 }
    483 
    484                 void render(Graphics g)
    485                 {
    486                     var imageAttributes = new ImageAttributes();
    487                     if (tint != Color.White)
    488                     {
    489                         var colorMatrix = new ColorMatrix(new float[][]
    490                         {
    491                             new float[] {tint.R / 255.0f, 0, 0, 0, 0},
    492                             new float[] {0, tint.G / 255.0f, 0, 0, 0},
    493                             new float[] {0, 0, tint.B / 255.0f, 0, 0},
    494                             new float[] {0, 0, 0, tint.A / 255.0f, 0},
    495                             new float[] {0, 0, 0, 0, 1},
    496                         }
    497                         );
    498                         imageAttributes.SetColorMatrix(colorMatrix);
    499                     }
    500 
    501                     if (factoryOverlayTile != null)
    502                     {
    503                         g.DrawImage(tile.Image, buildingBounds, 0, 0, tile.Image.Width, tile.Image.Height, GraphicsUnit.Pixel, imageAttributes);
    504                         g.DrawImage(factoryOverlayTile.Image, buildingBounds, 0, 0, tile.Image.Width, tile.Image.Height, GraphicsUnit.Pixel, imageAttributes);
    505                     }
    506                     else
    507                     {
    508                         g.DrawImage(tile.Image, buildingBounds, 0, 0, tile.Image.Width, tile.Image.Height, GraphicsUnit.Pixel, imageAttributes);
    509                     }
    510 
    511                     if (building.Type.IsFake)
    512                     {
    513                         var text = Globals.TheGameTextManager["TEXT_UI_FAKE"];
    514                         var textSize = g.MeasureString(text, SystemFonts.CaptionFont) + new SizeF(6.0f, 6.0f);
    515                         var textBounds = new RectangleF(buildingBounds.Location, textSize);
    516                         g.FillRectangle(fakeBackgroundBrush, textBounds);
    517                         g.DrawString(text, SystemFonts.CaptionFont, fakeTextBrush, textBounds, stringFormat);
    518                     }
    519 
    520                     if (building.BasePriority >= 0)
    521                     {
    522                         var text = building.BasePriority.ToString();
    523                         var textSize = g.MeasureString(text, SystemFonts.CaptionFont) + new SizeF(6.0f, 6.0f);
    524                         var textBounds = new RectangleF(buildingBounds.Location +
    525                             new Size((int)((buildingBounds.Width - textSize.Width) / 2.0f), (int)(buildingBounds.Height - textSize.Height)),
    526                             textSize
    527                         );
    528                         g.FillRectangle(baseBackgroundBrush, textBounds);
    529                         g.DrawString(text, SystemFonts.CaptionFont, baseTextBrush, textBounds, stringFormat);
    530                     }
    531                 }
    532 
    533                 return (buildingBounds, render);
    534             }
    535             else
    536             {
    537                 Debug.Print(string.Format("Building {0} (0) not found", building.Type.Name));
    538                 return (Rectangle.Empty, (g) => { });
    539             }
    540         }
    541 
    542         public static (Rectangle, Action<Graphics>) Render(TheaterType theater, Point topLeft, Size tileSize, Infantry infantry, InfantryStoppingType infantryStoppingType)
    543         {
    544             var icon = HumanShape[Facing32[infantry.Direction.ID]];
    545 
    546             string teamColor = infantry.House?.UnitTeamColor;
    547             if (Globals.TheTilesetManager.GetTeamColorTileData(theater.Tilesets, infantry.Type.Name, icon, Globals.TheTeamColorManager[teamColor], out Tile tile))
    548             {
    549                 var baseLocation = new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height)
    550                     + new Size(tileSize.Width / 2, tileSize.Height / 2);
    551 
    552                 var offset = Point.Empty;
    553                 switch (infantryStoppingType)
    554                 {
    555                     case InfantryStoppingType.UpperLeft:
    556                         offset.X = -tileSize.Width / 4;
    557                         offset.Y = -tileSize.Height / 4;
    558                         break;
    559                     case InfantryStoppingType.UpperRight:
    560                         offset.X = tileSize.Width / 4;
    561                         offset.Y = -tileSize.Height / 4;
    562                         break;
    563                     case InfantryStoppingType.LowerLeft:
    564                         offset.X = -tileSize.Width / 4;
    565                         offset.Y = tileSize.Height / 4;
    566                         break;
    567                     case InfantryStoppingType.LowerRight:
    568                         offset.X = tileSize.Width / 4;
    569                         offset.Y = tileSize.Height / 4;
    570                         break;
    571                 }
    572                 baseLocation.Offset(offset);
    573 
    574                 var virtualBounds = new Rectangle(
    575                     new Point(baseLocation.X - (tile.OpaqueBounds.Width / 2), baseLocation.Y - tile.OpaqueBounds.Height),
    576                     tile.OpaqueBounds.Size
    577                 );
    578                 var renderBounds = new Rectangle(
    579                     baseLocation - new Size(infantry.Type.RenderSize.Width / 2, infantry.Type.RenderSize.Height / 2),
    580                     infantry.Type.RenderSize
    581                 );
    582 
    583                 var tint = infantry.Tint;
    584                 void render(Graphics g)
    585                 {
    586                     var imageAttributes = new ImageAttributes();
    587                     if (tint != Color.White)
    588                     {
    589                         var colorMatrix = new ColorMatrix(new float[][]
    590                         {
    591                             new float[] {tint.R / 255.0f, 0, 0, 0, 0},
    592                             new float[] {0, tint.G / 255.0f, 0, 0, 0},
    593                             new float[] {0, 0, tint.B / 255.0f, 0, 0},
    594                             new float[] {0, 0, 0, tint.A / 255.0f, 0},
    595                             new float[] {0, 0, 0, 0, 1},
    596                         }
    597                         );
    598                         imageAttributes.SetColorMatrix(colorMatrix);
    599                     }
    600                     g.DrawImage(tile.Image, renderBounds, 0, 0, tile.Image.Width, tile.Image.Height, GraphicsUnit.Pixel, imageAttributes);
    601                 }
    602 
    603                 return (virtualBounds, render);
    604             }
    605             else
    606             {
    607                 Debug.Print(string.Format("Infantry {0} ({1}) not found", infantry.Type.Name, icon));
    608                 return (Rectangle.Empty, (g) => { });
    609             }
    610         }
    611 
    612         public static (Rectangle, Action<Graphics>) Render(GameType gameType, TheaterType theater, Point topLeft, Size tileSize, Unit unit)
    613         {
    614             int icon = 0;
    615             if (gameType == GameType.TiberianDawn)
    616             {
    617                 if (unit.Type == TiberianDawn.UnitTypes.GunBoat)
    618                 {
    619                     switch (unit.Direction.Facing)
    620                     {
    621                         case FacingType.NorthEast:
    622                         case FacingType.East:
    623                         case FacingType.SouthEast:
    624                             icon = 96;
    625                             break;
    626                         default:
    627                             icon = 0;
    628                             break;
    629                     }
    630                 }
    631                 else if ((unit.Type == TiberianDawn.UnitTypes.Tric) ||
    632                          (unit.Type == TiberianDawn.UnitTypes.Trex) ||
    633                          (unit.Type == TiberianDawn.UnitTypes.Rapt) ||
    634                          (unit.Type == TiberianDawn.UnitTypes.Steg))
    635                 {
    636                     var facing = ((unit.Direction.ID + 0x10) & 0xFF) >> 5;
    637                     icon = BodyShape[facing + ((facing > 0) ? 24 : 0)];
    638                 }
    639                 else if ((unit.Type == TiberianDawn.UnitTypes.Hover) ||
    640                          (unit.Type == TiberianDawn.UnitTypes.Visceroid))
    641                 {
    642                     icon = 0;
    643                 }
    644                 else
    645                 {
    646                     icon = BodyShape[Facing32[unit.Direction.ID]];
    647                 }
    648             }
    649             else if (gameType == GameType.RedAlert)
    650             {
    651                 if (unit.Type.IsAircraft)
    652                 {
    653                     if ((unit.Type == RedAlert.UnitTypes.Tran) ||
    654                         (unit.Type == RedAlert.UnitTypes.Heli) ||
    655                         (unit.Type == RedAlert.UnitTypes.Hind))
    656                     {
    657                         icon = BodyShape[Facing32[unit.Direction.ID]];
    658                     }
    659                     else
    660                     {
    661                         icon = BodyShape[Facing16[unit.Direction.ID] * 2] / 2;
    662                     }
    663                 }
    664                 else if (unit.Type.IsVessel)
    665                 {
    666                     if ((unit.Type == RedAlert.UnitTypes.Transport) ||
    667                         (unit.Type == RedAlert.UnitTypes.Carrier))
    668                     {
    669                         icon = 0;
    670                     }
    671                     else
    672                     {
    673                         icon = BodyShape[Facing16[unit.Direction.ID] * 2] >> 1;
    674                     }
    675                 }
    676                 else
    677                 {
    678                     if ((unit.Type == RedAlert.UnitTypes.Ant1) ||
    679                         (unit.Type == RedAlert.UnitTypes.Ant2) ||
    680                         (unit.Type == RedAlert.UnitTypes.Ant3))
    681                     {
    682                         icon = ((BodyShape[Facing32[unit.Direction.ID]] + 2) / 4) & 0x07;
    683                     }
    684                     else
    685                     {
    686                         icon = BodyShape[Facing32[unit.Direction.ID]];
    687                     }
    688                 }
    689             }
    690 
    691             string teamColor = null;
    692             if (unit.House != null)
    693             {
    694                 if (!unit.House.OverrideTeamColors.TryGetValue(unit.Type.Name, out teamColor))
    695                 {
    696                     teamColor = unit.House.UnitTeamColor;
    697                 }
    698             }
    699 
    700             if (Globals.TheTilesetManager.GetTeamColorTileData(theater.Tilesets, unit.Type.Name, icon, Globals.TheTeamColorManager[teamColor], out Tile tile))
    701             {
    702                 var location =
    703                     new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height) +
    704                     new Size(tileSize.Width / 2, tileSize.Height / 2);
    705                 var renderBounds = new Rectangle(
    706                     location - new Size(unit.Type.RenderSize.Width / 2, unit.Type.RenderSize.Height / 2),
    707                     unit.Type.RenderSize
    708                 );
    709 
    710                 Tile radarTile = null;
    711                 if ((unit.Type == RedAlert.UnitTypes.MGG) ||
    712                     (unit.Type == RedAlert.UnitTypes.MRJammer) ||
    713                     (unit.Type == RedAlert.UnitTypes.Tesla))
    714                 {
    715                     Globals.TheTilesetManager.GetTeamColorTileData(theater.Tilesets, unit.Type.Name, 32, Globals.TheTeamColorManager[teamColor], out radarTile);
    716                 }
    717 
    718                 Tile turretTile = null;
    719                 if (unit.Type.HasTurret)
    720                 {
    721                     var turretName = unit.Type.Name;
    722                     var turretIcon = icon + 32;
    723                     if (unit.Type == RedAlert.UnitTypes.Phase)
    724                     {
    725                         turretIcon += 6;
    726                     }
    727 #if TODO
    728                     else if (unit.Type == RedAlert.UnitTypes.Cruiser)
    729                     {
    730                         turretName = "TURR";
    731                         turretIcon = BodyShape[Facing32[unit.Direction.ID]];
    732                     }
    733                     else if (unit.Type == RedAlert.UnitTypes.Destroyer)
    734                     {
    735                         turretName = "SSAM";
    736                         turretIcon = BodyShape[Facing32[unit.Direction.ID]];
    737                     }
    738                     else if (unit.Type == RedAlert.UnitTypes.PTBoat)
    739                     {
    740                         turretName = "MGUN";
    741                         turretIcon = BodyShape[Facing32[unit.Direction.ID]];
    742                     }
    743 #endif
    744 
    745                     Globals.TheTilesetManager.GetTeamColorTileData(theater.Tilesets, turretName, turretIcon, Globals.TheTeamColorManager[teamColor], out turretTile);
    746                 }
    747 
    748                 var tint = unit.Tint;
    749                 void render(Graphics g)
    750                 {
    751                     var imageAttributes = new ImageAttributes();
    752                     if (tint != Color.White)
    753                     {
    754                         var colorMatrix = new ColorMatrix(new float[][]
    755                         {
    756                             new float[] {tint.R / 255.0f, 0, 0, 0, 0},
    757                             new float[] {0, tint.G / 255.0f, 0, 0, 0},
    758                             new float[] {0, 0, tint.B / 255.0f, 0, 0},
    759                             new float[] {0, 0, 0, tint.A / 255.0f, 0},
    760                             new float[] {0, 0, 0, 0, 1},
    761                         }
    762                         );
    763                         imageAttributes.SetColorMatrix(colorMatrix);
    764                     }
    765 
    766                     g.DrawImage(tile.Image, renderBounds, 0, 0, tile.Image.Width, tile.Image.Height, GraphicsUnit.Pixel, imageAttributes);
    767 
    768                     if (radarTile != null)
    769                     {
    770                         Point turretAdjust = Point.Empty;
    771                         if (unit.Type == RedAlert.UnitTypes.MGG)
    772                         {
    773                             turretAdjust = TurretAdjust[Facing32[unit.Direction.ID]];
    774                         }
    775                         else if (unit.Type != RedAlert.UnitTypes.Tesla)
    776                         {
    777                             turretAdjust.Y = -5;
    778                         }
    779 
    780                         var radarBounds = renderBounds;
    781                         radarBounds.Offset(
    782                             turretAdjust.X * tileSize.Width / Globals.PixelWidth,
    783                             turretAdjust.Y * tileSize.Height / Globals.PixelHeight
    784                         );
    785 
    786                         g.DrawImage(radarTile.Image, radarBounds, 0, 0, tile.Image.Width, tile.Image.Height, GraphicsUnit.Pixel, imageAttributes);
    787                     }
    788                     if (turretTile != null)
    789                     {
    790                         Point turretAdjust = Point.Empty;
    791                         if (gameType == GameType.RedAlert)
    792                         {
    793                             if (unit.Type.IsVessel)
    794                             {
    795 
    796                             }
    797                             else if (unit.Type == RedAlert.UnitTypes.Jeep)
    798                             {
    799                                 turretAdjust.Y = -4;
    800                             }
    801                         }
    802                         else if (gameType == GameType.TiberianDawn)
    803                         {
    804                             if ((unit.Type == TiberianDawn.UnitTypes.Jeep) ||
    805                                 (unit.Type == TiberianDawn.UnitTypes.Buggy))
    806                             {
    807                                 turretAdjust.Y = -4;
    808                             }
    809                             else if ((unit.Type == TiberianDawn.UnitTypes.SAM) ||
    810                                      (unit.Type == TiberianDawn.UnitTypes.MLRS))
    811                             {
    812                                 turretAdjust = TurretAdjust[Facing32[unit.Direction.ID]];
    813                             }
    814                         }
    815 
    816                         var turretBounds = renderBounds;
    817                         turretBounds.Offset(
    818                             turretAdjust.X * tileSize.Width / Globals.PixelWidth,
    819                             turretAdjust.Y * tileSize.Height / Globals.PixelHeight
    820                         );
    821 
    822                         g.DrawImage(turretTile.Image, turretBounds, 0, 0, tile.Image.Width, tile.Image.Height, GraphicsUnit.Pixel, imageAttributes);
    823                     }
    824                 }
    825 
    826                 return (renderBounds, render);
    827             }
    828             else
    829             {
    830                 Debug.Print(string.Format("Unit {0} ({1}) not found", unit.Type.Name, icon));
    831                 return (Rectangle.Empty, (g) => { });
    832             }
    833         }
    834     }
    835 }