// // Copyright 2020 Electronic Arts Inc. // // The Command & Conquer Map Editor and corresponding source code is free // software: you can redistribute it and/or modify it under the terms of // the GNU General Public License as published by the Free Software Foundation, // either version 3 of the License, or (at your option) any later version. // // The Command & Conquer Map Editor and corresponding source code is distributed // in the hope that it will be useful, but with permitted additional restrictions // under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT // distributed with this program. You should have received a copy of the // GNU General Public License along with permitted additional restrictions // with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection using MobiusEditor.Interface; using MobiusEditor.Model; using MobiusEditor.Utility; using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Drawing.Text; using System.Linq; using System.Text; namespace MobiusEditor.Render { public class RenderInfo { // paint position, paint action, true if flat, sub-position. // This works in 'classic pixels'; 1/24th of a cell. public Point RenderBasePoint; public Action RenderAction { get; private set; } public int ZOrder { get; private set; } public ITechno RenderedObject { get; private set; } public bool IsRendered { get; set; } public RenderInfo(Point renderPosition, Action paintAction, int zOrder, ITechno paintedObject) { this.RenderBasePoint = renderPosition; this.RenderAction = paintAction; this.ZOrder = zOrder; this.RenderedObject = paintedObject; this.IsRendered = false; } public RenderInfo(Point renderPosition, Action paintAction, ITechno paintedObject) :this(renderPosition, paintAction, Globals.ZOrderDefault, paintedObject) { } } public static class MapRenderer { private static readonly int[] Facing16 = new int[256] { 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, 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, 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, 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, 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, 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, 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, 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 }; private static readonly int[] Facing32 = new int[256] { 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, 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, 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, 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, 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, 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, 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, 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 }; private static readonly int[] HumanShape = new int[32] { 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 }; private static readonly int[] BodyShape = new int[32] { 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 }; /// /// Cosine table. Technically signed bytes, but stored as 00-FF for simplicity. /// private static byte[] CosTable = { 0x00, 0x03, 0x06, 0x09, 0x0c, 0x0f, 0x12, 0x15, 0x18, 0x1b, 0x1e, 0x21, 0x24, 0x27, 0x2a, 0x2d, 0x30, 0x33, 0x36, 0x39, 0x3b, 0x3e, 0x41, 0x43, 0x46, 0x49, 0x4b, 0x4e, 0x50, 0x52, 0x55, 0x57, 0x59, 0x5b, 0x5e, 0x60, 0x62, 0x64, 0x65, 0x67, 0x69, 0x6b, 0x6c, 0x6e, 0x6f, 0x71, 0x72, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7b, 0x7c, 0x7d, 0x7d, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7f, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7d, 0x7d, 0x7c, 0x7b, 0x7b, 0x7a, 0x79, 0x78, 0x77, 0x76, 0x75, 0x74, 0x72, 0x71, 0x70, 0x6e, 0x6c, 0x6b, 0x69, 0x67, 0x66, 0x64, 0x62, 0x60, 0x5e, 0x5b, 0x59, 0x57, 0x55, 0x52, 0x50, 0x4e, 0x4b, 0x49, 0x46, 0x43, 0x41, 0x3e, 0x3b, 0x39, 0x36, 0x33, 0x30, 0x2d, 0x2a, 0x27, 0x24, 0x21, 0x1e, 0x1b, 0x18, 0x15, 0x12, 0x0f, 0x0c, 0x09, 0x06, 0x03, 0x00, 0xfd, 0xfa, 0xf7, 0xf4, 0xf1, 0xee, 0xeb, 0xe8, 0xe5, 0xe2, 0xdf, 0xdc, 0xd9, 0xd6, 0xd3, 0xd0, 0xcd, 0xca, 0xc7, 0xc5, 0xc2, 0xbf, 0xbd, 0xba, 0xb7, 0xb5, 0xb2, 0xb0, 0xae, 0xab, 0xa9, 0xa7, 0xa5, 0xa2, 0xa0, 0x9e, 0x9c, 0x9a, 0x99, 0x97, 0x95, 0x94, 0x92, 0x91, 0x8f, 0x8e, 0x8c, 0x8b, 0x8a, 0x89, 0x88, 0x87, 0x86, 0x85, 0x85, 0x84, 0x83, 0x83, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x83, 0x83, 0x84, 0x85, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8e, 0x8f, 0x90, 0x92, 0x94, 0x95, 0x97, 0x99, 0x9a, 0x9c, 0x9e, 0xa0, 0xa2, 0xa5, 0xa7, 0xa9, 0xab, 0xae, 0xb0, 0xb2, 0xb5, 0xb7, 0xba, 0xbd, 0xbf, 0xc2, 0xc5, 0xc7, 0xca, 0xcd, 0xd0, 0xd3, 0xd6, 0xd9, 0xdc, 0xdf, 0xe2, 0xe5, 0xe8, 0xeb, 0xee, 0xf1, 0xf4, 0xf7, 0xfa, 0xfd, }; /// /// Sine table. Technically signed bytes, but stored as 00-FF for simplicity. /// private static byte[] SinTable = { 0x7f, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7d, 0x7d, 0x7c, 0x7b, 0x7b, 0x7a, 0x79, 0x78, 0x77, 0x76, 0x75, 0x74, 0x72, 0x71, 0x70, 0x6e, 0x6c, 0x6b, 0x69, 0x67, 0x66, 0x64, 0x62, 0x60, 0x5e, 0x5b, 0x59, 0x57, 0x55, 0x52, 0x50, 0x4e, 0x4b, 0x49, 0x46, 0x43, 0x41, 0x3e, 0x3b, 0x39, 0x36, 0x33, 0x30, 0x2d, 0x2a, 0x27, 0x24, 0x21, 0x1e, 0x1b, 0x18, 0x15, 0x12, 0x0f, 0x0c, 0x09, 0x06, 0x03, 0x00, 0xfd, 0xfa, 0xf7, 0xf4, 0xf1, 0xee, 0xeb, 0xe8, 0xe5, 0xe2, 0xdf, 0xdc, 0xd9, 0xd6, 0xd3, 0xd0, 0xcd, 0xca, 0xc7, 0xc5, 0xc2, 0xbf, 0xbd, 0xba, 0xb7, 0xb5, 0xb2, 0xb0, 0xae, 0xab, 0xa9, 0xa7, 0xa5, 0xa2, 0xa0, 0x9e, 0x9c, 0x9a, 0x99, 0x97, 0x95, 0x94, 0x92, 0x91, 0x8f, 0x8e, 0x8c, 0x8b, 0x8a, 0x89, 0x88, 0x87, 0x86, 0x85, 0x85, 0x84, 0x83, 0x83, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x83, 0x83, 0x84, 0x85, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8e, 0x8f, 0x90, 0x92, 0x94, 0x95, 0x97, 0x99, 0x9a, 0x9c, 0x9e, 0xa0, 0xa2, 0xa5, 0xa7, 0xa9, 0xab, 0xae, 0xb0, 0xb2, 0xb5, 0xb7, 0xba, 0xbd, 0xbf, 0xc2, 0xc5, 0xc7, 0xca, 0xcd, 0xd0, 0xd3, 0xd6, 0xd9, 0xdc, 0xdf, 0xe2, 0xe5, 0xe8, 0xeb, 0xee, 0xf1, 0xf4, 0xf7, 0xfa, 0xfd, 0x00, 0x03, 0x06, 0x09, 0x0c, 0x0f, 0x12, 0x15, 0x18, 0x1b, 0x1e, 0x21, 0x24, 0x27, 0x2a, 0x2d, 0x30, 0x33, 0x36, 0x39, 0x3b, 0x3e, 0x41, 0x43, 0x46, 0x49, 0x4b, 0x4e, 0x50, 0x52, 0x55, 0x57, 0x59, 0x5b, 0x5e, 0x60, 0x62, 0x64, 0x65, 0x67, 0x69, 0x6b, 0x6c, 0x6e, 0x6f, 0x71, 0x72, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7b, 0x7c, 0x7d, 0x7d, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, }; private static void MovePoint(ref int x, ref int y, byte dir, int distance, int perspectiveDivider) { x += ((sbyte)CosTable[dir] * distance) >> 7; y += -(((sbyte)SinTable[dir] * distance / perspectiveDivider) >> 7); } private static readonly short[] HeliDistanceAdjust = { 8, 9, 10, 9, 8, 9, 10, 9 }; private static readonly Point[] BackTurretAdjust = new Point[] { new Point(1, 2), // N new Point(-1, 1), new Point(-2, 0), new Point(-3, 0), new Point(-3, 1), // NW new Point(-4, -1), new Point(-4, -1), new Point(-5, -2), new Point(-5, -3), // W new Point(-5, -3), new Point(-3, -3), new Point(-3, -4), new Point(-3, -4), // SW new Point(-3, -5), new Point(-2, -5), new Point(-1, -5), new Point(0, -5), // S new Point(1, -6), new Point(2, -5), new Point(3, -5), new Point(4, -5), // SE new Point(6, -4), new Point(6, -3), new Point(6, -3), new Point(6, -3), // E new Point(5, -1), new Point(5, -1), new Point(4, 0), new Point(3, 0), // NE new Point(2, 0), new Point(2, 1), new Point(1, 2) }; public static void Render(GameInfo gameInfo, Map map, Graphics graphics, ISet locations, MapLayerFlag layers, double tileScale, ShapeCacheManager cacheManager) { bool disposeCacheManager = false; if (cacheManager == null) { cacheManager = new ShapeCacheManager(); disposeCacheManager = true; } // tileScale should always be given so it results in an exact integer tile size. Math.Round was added to account for .999 situations in the floats. Size tileSize = new Size(Math.Max(1, (int)Math.Round(Globals.OriginalTileWidth * tileScale)), Math.Max(1, (int)Math.Round(Globals.OriginalTileHeight * tileScale))); //Size tileSize = new Size(Math.Max(1, (int)(Globals.OriginalTileWidth * tileScale)), Math.Max(1, (int)(Globals.OriginalTileHeight * tileScale))); TheaterType theater = map.Theater; // paint position, paint action, true if flat, sub-position. List overlappingRenderList = new List(); Func> renderLocations = null; if (locations != null) { renderLocations = () => locations.OrderBy(p => p.Y * map.Metrics.Width + p.X); } else { IEnumerable allCells() { for (int y = 0; y < map.Metrics.Height; ++y) { for (int x = 0; x < map.Metrics.Width; ++x) { yield return new Point(x, y); } } } renderLocations = allCells; } CompositingQuality backupCompositingQuality = graphics.CompositingQuality; InterpolationMode backupInterpolationMode = graphics.InterpolationMode; SmoothingMode backupSmoothingMode = graphics.SmoothingMode; PixelOffsetMode backupPixelOffsetMode = graphics.PixelOffsetMode; // Check if high-quality tile resizing is useful. SetRenderSettings(graphics, false); bool isSmooth = backupCompositingQuality != graphics.CompositingQuality || backupInterpolationMode != graphics.InterpolationMode || backupSmoothingMode != graphics.SmoothingMode || backupPixelOffsetMode != graphics.PixelOffsetMode; // No need to restore the settings; the high quality tile resizing makes all tiles the // required size, and if isSmooth is false, the settings were already on pixel resize. if ((layers & MapLayerFlag.Template) != MapLayerFlag.None) { TemplateType clear = map.TemplateTypes.Where(t => t.Flag == TemplateTypeFlag.Clear).FirstOrDefault(); foreach (Point topLeft in renderLocations()) { Template template = map.Templates[topLeft]; TemplateType ttype = template?.Type ?? clear; string name = ttype.Name; // For clear terrain, calculate icon from 0-15 using map position. int icon = template?.Icon ?? ((topLeft.X & 0x03) | ((topLeft.Y) & 0x03) << 2); // If something is actually placed on the map, show it, even if it has no graphics. string tileName = "template_" + name + "_" + icon.ToString("D4") + "_" + tileSize.Width + "x" + tileSize.Height + (isSmooth ? "_smooth" : String.Empty); Bitmap tileImg = cacheManager.GetImage(tileName); Rectangle renderBounds = new Rectangle(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height, tileSize.Width, tileSize.Height); if (tileImg == null) { bool success = Globals.TheTilesetManager.GetTileData(name, icon, out Tile tile, true, false); if (tile != null && tile.Image != null) { Bitmap tileImage = tile.Image; if (!isSmooth || (tileSize.Width == tileImage.Width && tileSize.Height == tileImage.Height)) { tileImg = tileImage.RemoveAlpha(); } else { // Results in a new image that is scaled to the correct size, without edge artifacts. Bitmap scaledImage = tileImage.HighQualityScale(tileSize.Width, tileSize.Height, backupCompositingQuality, backupInterpolationMode, backupSmoothingMode, backupPixelOffsetMode); scaledImage.RemoveAlphaOnCurrent(); tileImg = scaledImage; } cacheManager.AddImage(tileName, tileImg); } } if (tileImg != null) { graphics.DrawImage(tileImg, renderBounds); } else { Debug.Print(string.Format("Template {0} ({1}) could not be rendered.", name, icon)); } } } // Since high-quality scaling is now done on the tiles themselves, the actual map tile painting is done // with pixel interpolation mode because it is faster, and the tiles are already correctly sized anyway. // So now, restore the actual requested settings. graphics.CompositingQuality = backupCompositingQuality; graphics.InterpolationMode = backupInterpolationMode; graphics.SmoothingMode = backupSmoothingMode; graphics.PixelOffsetMode = backupPixelOffsetMode; // Attached bibs are counted under Buildings, not Smudge. if ((layers & MapLayerFlag.Buildings) != MapLayerFlag.None) { foreach (Point topLeft in renderLocations()) { Smudge smudge = map.Smudge[topLeft]; // Don't render bibs in theaters which don't contain them. if (smudge != null && smudge.Type.IsAutoBib && (!Globals.FilterTheaterObjects || smudge.Type.ExistsInTheater)) { RenderSmudge(topLeft, tileSize, tileScale, smudge, isSmooth, cacheManager).Item2(graphics); } } } if ((layers & MapLayerFlag.Smudge) != MapLayerFlag.None) { foreach (Point topLeft in renderLocations()) { Smudge smudge = map.Smudge[topLeft]; if (smudge != null && !smudge.Type.IsAutoBib) { RenderSmudge(topLeft, tileSize, tileScale, smudge, isSmooth, cacheManager).Item2(graphics); } } } if ((layers & MapLayerFlag.OverlayAll) != MapLayerFlag.None) { foreach (Point location in renderLocations()) { Overlay overlay = map.Overlay[location]; if (overlay == null) { continue; } if (Globals.CratesOnTop && overlay.Type.IsCrate && (layers & MapLayerFlag.Overlay) != MapLayerFlag.None) { // if "CratesOnTop" logic is active, crates are skipped here and painted afterwards. continue; } bool paintAsWall = overlay.Type.IsWall && (layers & MapLayerFlag.Walls) != MapLayerFlag.None; bool paintAsResource = overlay.Type.IsResource && (layers & MapLayerFlag.Resources) != MapLayerFlag.None; bool paintAsOverlay = overlay.Type.IsOverlay && (layers & MapLayerFlag.Overlay) != MapLayerFlag.None; if (paintAsWall || paintAsResource || paintAsOverlay) { RenderOverlay(gameInfo, location, map.Bounds, tileSize, tileScale, overlay, false).Item2(graphics); } } } if ((layers & MapLayerFlag.Buildings) != MapLayerFlag.None) { foreach ((Point topLeft, Building building) in map.Buildings.OfType()) { if ((locations != null) && !locations.Contains(topLeft)) { continue; } overlappingRenderList.Add(RenderBuilding(gameInfo, map, topLeft, tileSize, tileScale, building, false)); } } if ((layers & MapLayerFlag.Infantry) != MapLayerFlag.None) { foreach ((Point topLeft, InfantryGroup infantryGroup) in map.Technos.OfType()) { if ((locations != null) && !locations.Contains(topLeft)) { continue; } foreach (InfantryStoppingType ist in InfantryGroup.RenderOrder) { Infantry infantry = infantryGroup.Infantry[(int)ist]; if (infantry == null) { continue; } overlappingRenderList.Add(RenderInfantry(topLeft, tileSize, infantry, ist, false)); } } } if ((layers & MapLayerFlag.Units) != MapLayerFlag.None) { foreach ((Point topLeft, Unit unit) in map.Technos.OfType()) { if ((locations != null) && !locations.Contains(topLeft)) { continue; } overlappingRenderList.Add(RenderUnit(gameInfo, topLeft, tileSize, unit, false)); } } if ((layers & MapLayerFlag.Terrain) != MapLayerFlag.None) { foreach ((Point topLeft, Terrain terrain) in map.Technos.OfType()) { if ((locations != null) && !locations.Contains(topLeft)) { continue; } overlappingRenderList.Add(RenderTerrain(topLeft, tileSize, tileScale, terrain, false)); } } // Paint all the rest List validRenders = overlappingRenderList.Where(obj => obj.RenderedObject != null).ToList(); int paintOrder = 0; foreach (RenderInfo info in validRenders.OrderBy(obj => obj.ZOrder).ThenBy(obj => obj.RenderBasePoint.Y).ThenByDescending(obj => obj.RenderBasePoint.X)) { info.RenderAction(graphics); info.RenderedObject.DrawOrderCache = paintOrder++; } if (Globals.CratesOnTop && (layers & MapLayerFlag.Overlay) != MapLayerFlag.None) { foreach (Point topLeft in renderLocations()) { Overlay overlay = map.Overlay[topLeft]; if (overlay == null || !overlay.Type.IsCrate) { continue; } RenderOverlay(gameInfo, topLeft, map.Bounds, tileSize, tileScale, overlay, false).Item2(graphics); } } if ((layers & MapLayerFlag.Waypoints) != MapLayerFlag.None) { // todo avoid overlapping waypoints of the same type? Dictionary flagOverlapPoints = new Dictionary(); HashSet handledPoints = new HashSet(); ITeamColor[] flagColors = map.FlagColors; bool soloMission = map.BasicSection.SoloMission; bool previewIsFlag = map.Waypoints.Where(w => w.IsPreview && Waypoint.GetMpIdFromFlag(w.Flag) != -1).Any(); int lastFlag = -1; float wpAlpha = 0.5f; if (!soloMission) { int firstFlag = -1; for (int i = 0; i < map.Waypoints.Length; i++) { Waypoint waypoint = map.Waypoints[i]; if (waypoint.IsPreview) { continue; } int mpId = Waypoint.GetMpIdFromFlag(map.Waypoints[i].Flag); if (mpId != -1) { if (firstFlag == -1) { firstFlag = i; } lastFlag = i; } } // This logic is kind of dirty; it relies on all flag points being in consecutive order. But without that, the preview logic doesn't work. for (int i = 0; i < firstFlag; i++) { Waypoint waypoint = map.Waypoints[i]; if (!waypoint.Point.HasValue || (locations != null && !locations.Contains(waypoint.Point.Value)) || !map.Metrics.GetCell(waypoint.Point.Value, out int cell) || handledPoints.Contains(cell)) { continue; } handledPoints.Add(cell); RenderWaypoint(gameInfo, soloMission, tileSize, flagColors, waypoint, wpAlpha, 0, cacheManager).Item2(graphics); } RenderWaypointFlags(graphics, gameInfo, map, map.Metrics.Bounds, tileSize, cacheManager); } for (int i = lastFlag + 1; i < map.Waypoints.Length; i++) { Waypoint waypoint = map.Waypoints[i]; if (!waypoint.Point.HasValue || (locations != null && !locations.Contains(waypoint.Point.Value)) || !map.Metrics.GetCell(waypoint.Point.Value, out int cell) || handledPoints.Contains(cell)) { continue; } handledPoints.Add(cell); RenderWaypoint(gameInfo, soloMission, tileSize, flagColors, waypoint, wpAlpha, 0, cacheManager).Item2(graphics); } } if (disposeCacheManager) { cacheManager.Reset(); } } public static void Render(GameInfo gameInfo, Map map, Graphics graphics, ISet locations, MapLayerFlag layers) { Render(gameInfo, map, graphics, locations, layers, Globals.MapTileScale, Globals.TheShapeCacheManager); } public static (Rectangle, Action) RenderSmudge(Point topLeft, Size tileSize, double tileScale, Smudge smudge, bool isSmoothRendering, ShapeCacheManager cacheManager) { if (Globals.FilterTheaterObjects && !smudge.Type.ExistsInTheater) { Debug.Print(string.Format("Smudge {0} ({1}) not available in this theater.", smudge.Type.Name, smudge.Icon)); return (Rectangle.Empty, (g) => { }); } float alphaFactor = 1.0f; Building bld = smudge.AttachedTo; if (bld != null) { if (bld.IsPreview) { alphaFactor = Globals.PreviewAlphaFloat; } if (!bld.IsPrebuilt) { alphaFactor *= Globals.UnbuiltAlphaFloat; } } else if (smudge.IsPreview) { alphaFactor = Globals.PreviewAlphaFloat; } alphaFactor = alphaFactor.Restrict(0, 1); string tileName = "smudge_" + smudge.Type.Name + "_" + smudge.Icon.ToString("D4") + "_" + tileSize.Width + "x" + tileSize.Height + (isSmoothRendering ? "_smooth" : String.Empty); Bitmap tileImg = cacheManager.GetImage(tileName); if (tileImg == null) { bool success = Globals.TheTilesetManager.GetTileData(smudge.Type.Name, smudge.Icon, out Tile tile, true, false); if (tile != null && tile.Image != null) { Bitmap tileImage = tile.Image; // Check if high-quality tile resizing is useful. if (!isSmoothRendering || (tileSize.Width == tileImage.Width && tileSize.Height == tileImage.Height)) { tileImg = new Bitmap(tileSize.Width, tileSize.Height); Rectangle smudgeBounds = RenderBounds(tile.Image.Size, new Size(1, 1), tileScale); using (Graphics g = Graphics.FromImage(tileImg)) { SetRenderSettings(g, isSmoothRendering); g.DrawImage(tileImage, smudgeBounds, new Rectangle(0, 0, tileImage.Width, tileImage.Height), GraphicsUnit.Pixel); } } else { // Results in a new image that is scaled to the correct size, without edge artifacts. tileImg = tileImage.HighQualityScale(tileSize.Width, tileSize.Height); } cacheManager.AddImage(tileName, tileImg); } } if (tileImg != null) { Rectangle smudgeBounds = new Rectangle( (tileSize.Width - tileImg.Width) / 2, (tileSize.Height - tileImg.Height) / 2, tileImg.Width, tileImg.Height); smudgeBounds.X += topLeft.X * tileSize.Width; smudgeBounds.Y += topLeft.Y * tileSize.Height; void render(Graphics g) { using (ImageAttributes imageAttributes = new ImageAttributes()) { imageAttributes.SetColorMatrix(GetColorMatrix(Color.White, 1.0f, alphaFactor)); g.DrawImage(tileImg, smudgeBounds, 0, 0, tileImg.Width, tileImg.Height, GraphicsUnit.Pixel, imageAttributes); } } return (smudgeBounds, render); } else { Debug.Print(string.Format("Smudge {0} ({1}) not found", smudge.Type.Name, smudge.Icon)); return (Rectangle.Empty, (g) => { }); } } public static (Rectangle, Action) RenderOverlay(GameInfo gameInfo, Point topLeft, Rectangle? mapBounds, Size tileSize, double tileScale, Overlay overlay, bool fullOpaque) { OverlayType ovtype = overlay.Type; string name = ovtype.GraphicsSource; int icon = ovtype.IsConcrete || ovtype.IsResource || ovtype.IsWall || ovtype.ForceTileNr == -1 ? overlay.Icon : ovtype.ForceTileNr; bool isTeleport = gameInfo != null && gameInfo.GameType == GameType.SoleSurvivor && ovtype == SoleSurvivor.OverlayTypes.Teleport && Globals.AdjustSoleTeleports; bool success = Globals.TheTilesetManager.GetTileData(name, icon, out Tile tile, true, false); if (tile != null && tile.Image != null) { int actualTopLeftX = topLeft.X * tileSize.Width; int actualTopLeftY = topLeft.Y * tileSize.Height; Rectangle overlayBounds = RenderBounds(tile.Image.Size, new Size(1, 1), tileScale); overlayBounds.X += actualTopLeftX; overlayBounds.Y += actualTopLeftY; float alphaFactor = 1.0f; if (!fullOpaque && overlay.IsPreview) { alphaFactor = Globals.PreviewAlphaFloat; } Color tint = Color.White; if (overlay.Type.IsResource && mapBounds.HasValue && !mapBounds.Value.Contains(topLeft)) { tint = Color.FromArgb(0xFF, 0x80, 0x80); // Technically the multiplication isn't needed; resources have no preview state in the editor. if (!fullOpaque) { alphaFactor *= 0.7f; } } alphaFactor = alphaFactor.Restrict(0, 1); void render(Graphics g) { using (ImageAttributes imageAttributes = new ImageAttributes()) { imageAttributes.SetColorMatrix(GetColorMatrix(tint, 1.0f, alphaFactor)); g.DrawImage(tile.Image, overlayBounds, 0, 0, tile.Image.Width, tile.Image.Height, GraphicsUnit.Pixel, imageAttributes); if (isTeleport) { // Transform ROAD tile into the teleport from SS. int blackBorderX = Math.Max(1, tileSize.Width / 24); int blackBorderY = Math.Max(1, tileSize.Height / 24); int blueWidth = tileSize.Width - blackBorderX * 2; int blueHeight = tileSize.Height - blackBorderY * 2; int blackWidth = tileSize.Width - blackBorderX * 4; int blackHeight = tileSize.Height - blackBorderY * 4; using (SolidBrush blue = new SolidBrush(Color.FromArgb(92, 164, 200))) using (SolidBrush black = new SolidBrush(Color.Black)) { g.FillRectangle(blue, actualTopLeftX + blackBorderX, actualTopLeftY + blackBorderY, blueWidth, blueHeight); g.FillRectangle(black, actualTopLeftX + blackBorderX * 2, actualTopLeftY + blackBorderY * 2, blackWidth, blackHeight); } } } } return (overlayBounds, render); } else { Debug.Print(string.Format("Overlay {0} ({1}) not found", name, icon)); return (Rectangle.Empty, (g) => { }); } } public static RenderInfo RenderTerrain(Point topLeft, Size tileSize, double tileScale, Terrain terrain, bool fullOpaque) { TerrainType type = terrain.Type; string tileName = type.GraphicsSource; bool succeeded = Globals.TheTilesetManager.GetTileData(tileName, type.DisplayIcon, out Tile tile, true, false); terrain.DrawFrameCache = type.DisplayIcon; if (!succeeded && !string.Equals(type.GraphicsSource, type.Name, StringComparison.InvariantCultureIgnoreCase)) { succeeded = Globals.TheTilesetManager.GetTileData(type.Name, type.DisplayIcon, out tile, true, false); } float alphaFactor = 1.0f; if (!fullOpaque) { if (terrain.IsPreview) { alphaFactor *= Globals.PreviewAlphaFloat; } alphaFactor = alphaFactor.Restrict(0, 1); } Size terrTSize = type.Size; Size tileISize = tile.Image.Size; Point location = new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height); Size maxSize = new Size(terrTSize.Width * tileSize.Width, terrTSize.Height * tileSize.Height); Rectangle paintBounds; if (!succeeded) { // Stretch dummy graphics over the whole size. paintBounds = new Rectangle(0, 0, (int)Math.Round(terrTSize.Width * tileISize.Width * tileScale), (int)Math.Round(terrTSize.Height * tileISize.Height * tileScale)); } else { paintBounds = RenderBounds(tileISize, terrTSize, tileScale); } paintBounds.X += location.X; paintBounds.Y += location.Y; void render(Graphics g) { using (ImageAttributes imageAttributes = new ImageAttributes()) { imageAttributes.SetColorMatrix(GetColorMatrix(Color.White, 1.0f, alphaFactor)); g.DrawImage(tile.Image, paintBounds, 0, 0, tile.Image.Width, tile.Image.Height, GraphicsUnit.Pixel, imageAttributes); } } Point centerPoint = GetTerrainRenderPoint(terrain); Point usedCenter = new Point(topLeft.X * Globals.PixelWidth + centerPoint.X, topLeft.Y * Globals.PixelHeight + centerPoint.Y); return new RenderInfo(usedCenter, render, terrain); } public static RenderInfo RenderBuilding(GameInfo gameInfo, Map map, Point topLeft, Size tileSize, double tileScale, Building building, bool fullOpaque) { float alphaFactor = 1.0f; if (!fullOpaque) { if (building.IsPreview) { alphaFactor *= Globals.PreviewAlphaFloat; } if (!building.IsPrebuilt) { alphaFactor *= Globals.UnbuiltAlphaFloat; } alphaFactor = alphaFactor.Restrict(0, 1); } int icon = building.Type.FrameOffset; int maxIcon = 0; int damageIconOffs = 0; int collapseIcon = 0; // In TD, damage is when BELOW the threshold. In RA, it's ON the threshold. int healthyMin = gameInfo.HitPointsGreenMinimum; bool isDamaged = building.Strength <= healthyMin; bool hasCollapseFrame = false; // Only fetch if damaged. BuildingType.IsSingleFrame is an override for the RA mines. Everything else works with one simple logic. if (isDamaged && !building.Type.IsSingleFrame && !building.Type.IsWall) { maxIcon = Globals.TheTilesetManager.GetTileDataLength(building.Type.GraphicsSource); hasCollapseFrame = maxIcon > 1 && maxIcon % 2 == 1; damageIconOffs = (maxIcon + (hasCollapseFrame ? 0 : 1)) / 2; collapseIcon = maxIcon - 1; } if (building.Type.HasTurret) { icon += BodyShape[Facing32[building.Direction.ID]]; if (isDamaged) { icon += damageIconOffs; } } else if (building.Type.IsWall) { icon += GetBuildingOverlayIcon(map, topLeft, building); } else { if (building.Strength <= 1 && hasCollapseFrame) { icon = collapseIcon; } else if (isDamaged) { icon += damageIconOffs; } } ITeamColor teamColor = building.Type.CanRemap ? Globals.TheTeamColorManager[building.House?.BuildingTeamColor] : null; bool succeeded = Globals.TheTilesetManager.GetTeamColorTileData(building.Type.GraphicsSource, icon, teamColor, out Tile tile, true, false); building.DrawFrameCache = icon; Point location = new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height); Size maxSize = new Size(building.Type.Size.Width * tileSize.Width, building.Type.Size.Height * tileSize.Height); Size bldTSize = building.Type.Size; Size tileISize = tile.Image.Size; Rectangle paintBounds; if (!succeeded) { // Stretch dummy graphics over the whole size. paintBounds = new Rectangle(0, 0, maxSize.Width, maxSize.Height); } else { paintBounds = RenderBounds(tileISize, bldTSize, tileScale); } Rectangle 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)) { int overlayIcon = 0; if (building.Strength <= healthyMin) { int maxOverlayIcon = Globals.TheTilesetManager.GetTileDataLength(building.Type.FactoryOverlay); overlayIcon = maxOverlayIcon / 2; } Globals.TheTilesetManager.GetTeamColorTileData(building.Type.FactoryOverlay, overlayIcon, Globals.TheTeamColorManager[building.House?.BuildingTeamColor], out factoryOverlayTile); } void render(Graphics g) { using (ImageAttributes imageAttributes = new ImageAttributes()) { imageAttributes.SetColorMatrix(GetColorMatrix(Color.White, 1.0f, alphaFactor)); if (factoryOverlayTile != null) { // Avoid factory overlay showing as semitransparent. using (Bitmap factory = new Bitmap(maxSize.Width, maxSize.Height)) { factory.SetResolution(96, 96); using (Graphics factoryG = Graphics.FromImage(factory)) { factoryG.CopyRenderSettingsFrom(g); Size renderSize = tileISize; if (!succeeded) { renderSize.Width = building.Type.Size.Width * tileSize.Width; renderSize.Height = building.Type.Size.Height * tileSize.Height; } Rectangle factBounds = RenderBounds(renderSize, building.Type.Size, tileScale); Rectangle ovrlBounds = RenderBounds(factoryOverlayTile.Image.Size, building.Type.Size, tileScale); factoryG.DrawImage(tile.Image, factBounds, 0, 0, tile.Image.Width, tile.Image.Height, GraphicsUnit.Pixel); factoryG.DrawImage(factoryOverlayTile.Image, ovrlBounds, 0, 0, factoryOverlayTile.Image.Width, factoryOverlayTile.Image.Height, GraphicsUnit.Pixel); } g.DrawImage(factory, buildingBounds, 0, 0, factory.Width, factory.Height, GraphicsUnit.Pixel, imageAttributes); } } else { paintBounds.X += location.X; paintBounds.Y += location.Y; g.DrawImage(tile.Image, paintBounds, 0, 0, tile.Image.Width, tile.Image.Height, GraphicsUnit.Pixel, imageAttributes); } } } Point centerPoint = GetBuildingRenderPoint(building); Point usedCenter = new Point(topLeft.X * Globals.PixelWidth + centerPoint.X, topLeft.Y * Globals.PixelHeight + centerPoint.Y); // "Z-Order" is for sorting buildings as floor level (0), flat on the floor (5), or sticking out of the floor (default; 10). // It determines whether pieces on unoccupied cells should overlap objects on these cells or be drawn below them. return new RenderInfo(usedCenter, render, building.Type.ZOrder, building); } private static int GetBuildingOverlayIcon(Map map, Point topLeft, Building building) { if (!building.Type.IsWall || map == null) { return 0; } BuildingType bt = building.Type; bool hasNorthWall = (map.Metrics.Adjacent(topLeft, FacingType.North, out Point north) ? map.Buildings[north] as Building : null)?.Type == bt; bool hasEastWall = (map.Metrics.Adjacent(topLeft, FacingType.East, out Point east) ? map.Buildings[east] as Building : null)?.Type == bt; bool hasSouthWall = (map.Metrics.Adjacent(topLeft, FacingType.South, out Point south) ? map.Buildings[south] as Building : null)?.Type == bt; bool hasWestWall = (map.Metrics.Adjacent(topLeft, FacingType.West, out Point west) ? map.Buildings[west] as Building : null)?.Type == bt; string btName = bt.Name; hasNorthWall |= map.Overlay.Adjacent(topLeft, FacingType.North)?.Type.Name == btName; hasEastWall |= map.Overlay.Adjacent(topLeft, FacingType.East)?.Type.Name == btName; hasSouthWall |= map.Overlay.Adjacent(topLeft, FacingType.South)?.Type.Name == btName; hasWestWall |= map.Overlay.Adjacent(topLeft, FacingType.West)?.Type.Name == btName; int icon = 0; if (hasNorthWall) { icon |= 1; } if (hasEastWall) { icon |= 2; } if (hasSouthWall) { icon |= 4; } if (hasWestWall) { icon |= 8; } return icon; } public static RenderInfo RenderInfantry(Point topLeft, Size tileSize, Infantry infantry, InfantryStoppingType infantryStoppingType, bool fullOpaque) { int icon = HumanShape[Facing32[infantry.Direction.ID]]; ITeamColor teamColor = infantry.Type.CanRemap ? Globals.TheTeamColorManager[infantry.House?.UnitTeamColor] : null; Tile tile = null; // InfantryType.Init() should have taken care of RA's classic civilian remap mess at this point, and remapped all cached source graphics. bool success = Globals.TheTilesetManager.GetTeamColorTileData(infantry.Type.GraphicsSource, icon, teamColor, out tile, true, false); if (tile == null || tile.Image == null) { Debug.Print(string.Format("Infantry {0} graphics ({1}, frame {2}) not found", infantry.Type.Name, infantry.Type.GraphicsSource, icon)); return new RenderInfo(Point.Empty, (g) => { }, infantry); } Size imSize = tile.Image.Size; Point origLocation = new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height); Point renderLocation = origLocation; Point offset; // Offset is calculated as "pixels" in the old 24-pixel cells. Size renderSize; if (success) { // Actual graphics: get tweaked positions accounting for the shape of the infantry and the fact the theoretical positions are at their feet. offset = GetInfantryRenderPoint(infantryStoppingType); Point offsetActual = new Point(offset.X * tileSize.Width / Globals.PixelWidth, offset.Y * tileSize.Height / Globals.PixelHeight); renderLocation.Offset(offsetActual); renderSize = new Size(imSize.Width * tileSize.Width / Globals.OriginalTileWidth, imSize.Height * tileSize.Height / Globals.OriginalTileHeight); } else { // Dummy graphics: use theoretical positions. offset = InfantryGroup.RenderPosition(infantryStoppingType, false); Point offsetActual = new Point(offset.X * tileSize.Width / Globals.PixelWidth, offset.Y * tileSize.Height / Globals.PixelHeight); renderLocation.Offset(offsetActual); renderSize = new Size(imSize.Width * tileSize.Width / Globals.OriginalTileWidth, imSize.Height * tileSize.Height / Globals.OriginalTileHeight); renderSize.Width /= 3; renderSize.Height /= 2; } Rectangle renderBounds = new Rectangle(renderLocation - new Size(renderSize.Width / 2, renderSize.Height / 2), renderSize); float alphaFactor = 1.0f; if (!fullOpaque) { if (infantry.IsPreview) { alphaFactor *= Globals.PreviewAlphaFloat; } alphaFactor = alphaFactor.Restrict(0, 1); } void render(Graphics g) { using (ImageAttributes imageAttributes = new ImageAttributes()) { imageAttributes.SetColorMatrix(GetColorMatrix(Color.White, 1.0f, alphaFactor)); g.DrawImage(tile.Image, renderBounds, 0, 0, tile.Image.Width, tile.Image.Height, GraphicsUnit.Pixel, imageAttributes); // Test code to visualise original 5-point die face location (green), and corrected infantry base point (feet) location (red). /*/ Size pixel = new Size(tileSize.Width / Globals.PixelWidth, tileSize.Height / Globals.PixelHeight); using (SolidBrush sb = new SolidBrush(Color.Red)) { g.FillRectangle(sb, new Rectangle(renderLocation, pixel)); } using (SolidBrush sb = new SolidBrush(Color.LimeGreen)) { g.FillRectangle(sb, new Rectangle(new Point( origLocation.X + offsetBare.X * tileSize.Width / Globals.PixelWidth, origLocation.Y + (offsetBare.Y * tileSize.Height / Globals.PixelHeight)), pixel)); } //*/ } } // Render position is the feet point, adjusted to 24-pixel cell location. return new RenderInfo(new Point(topLeft.X * Globals.PixelWidth + offset.X, topLeft.Y * Globals.PixelHeight + offset.Y), render, infantry); } public static RenderInfo RenderUnit(GameInfo gameInfo, Point topLeft, Size tileSize, Unit unit, bool fullOpaque) { int icon = 0; int bodyFrames = 0; // In TD, damage is when BELOW the threshold. In RA, it's ON the threshold. int healthyMin = gameInfo.HitPointsGreenMinimum; int damagedMin = gameInfo.HitPointsYellowMinimum; FrameUsage frameUsage = unit.Type.BodyFrameUsage; if (frameUsage.HasFlag(FrameUsage.Frames01Single)) { icon = 0; // Not actually determined, but whatever. Single frame units generally have no turret. bodyFrames = 1; } else if (frameUsage.HasFlag(FrameUsage.Frames08Cardinal)) { icon = ((BodyShape[Facing32[unit.Direction.ID]] + 2) / 4) & 0x07; bodyFrames = 8; } else if (frameUsage.HasFlag(FrameUsage.Frames16Simple)) { icon = BodyShape[Facing16[unit.Direction.ID] * 2] / 2; bodyFrames = 16; } else if (frameUsage.HasFlag(FrameUsage.Frames16Symmetrical)) { // Special case for 16-frame rotation saved as 8-frame because it is symmetrical and thus the second half of the frames is the same. icon = (BodyShape[Facing32[unit.Direction.ID]] / 2) & 0x07; bodyFrames = 8; } else if (frameUsage.HasFlag(FrameUsage.Frames32Full) || !frameUsage.HasAnyFlags(FrameUsage.FrameUsages)) { icon = BodyShape[Facing32[unit.Direction.ID]]; bodyFrames = 32; } // Special logic for TD gunboat's damaged states. // East facing is not actually possible to set in missions. This is just the turret facing. if (frameUsage.HasFlag(FrameUsage.DamageStates)) { if (unit.Strength <= healthyMin) icon += bodyFrames; if (unit.Strength <= damagedMin) icon += bodyFrames; // Skip three-step damaged frames. In practice this will just go to the east-facing ones though. bodyFrames *= 3; } // Special logic for APC-types with unload frames. if ((frameUsage & FrameUsage.HasUnloadFrames) != FrameUsage.None) { if (unit.Type.IsAircraft) { // Transport heli unload has 4 frames bodyFrames += 4; } else if (unit.Type.IsVessel) { // Boat unload has 4 frames bodyFrames += 4; } else { // APC unload has 6 frames. bodyFrames += 6; } } // Get House color. ITeamColor teamColor = null; if (unit.House != null && unit.Type.CanRemap) { string teamColorName; if (!unit.House.OverrideTeamColors.TryGetValue(unit.Type.Name, out teamColorName)) { teamColorName = unit.House?.UnitTeamColor; } teamColor = Globals.TheTeamColorManager[teamColorName]; } // Get body frame Globals.TheTilesetManager.GetTeamColorTileData(unit.Type.Name, icon, teamColor, out Tile tile, true, false); unit.DrawFrameCache = icon; if (tile == null || tile.Image == null) { Debug.Print(string.Format("Unit {0} ({1}) not found", unit.Type.Name, icon)); return new RenderInfo(Point.Empty, (g) => { }, null); } Size imSize = tile.Image.Size; Point location = new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height) + new Size(tileSize.Width / 2, tileSize.Height / 2); Size renderSize = new Size(imSize.Width * tileSize.Width / Globals.OriginalTileWidth, imSize.Height * tileSize.Height / Globals.OriginalTileHeight); Rectangle renderRect = new Rectangle(new Point(0, 0), renderSize); Rectangle renderBounds = new Rectangle(location - new Size(renderSize.Width / 2, renderSize.Height / 2), renderSize); // Turret handling Tile turretTile = null; Tile turret2Tile = null; Point turretAdjust = Point.Empty; Point turret2Adjust = Point.Empty; if (unit.Type.HasTurret) { FrameUsage turrUsage = unit.Type.TurretFrameUsage; string turretName = unit.Type.Turret ?? unit.Type.Name; string turret2Name = unit.Type.HasDoubleTurret ? unit.Type.SecondTurret ?? unit.Type.Turret ?? unit.Type.Name : null; int turret1Icon = 0; int turret2Icon = 0; if ((turrUsage & FrameUsage.Frames01Single) != FrameUsage.None) { turret1Icon = 0; turret2Icon = 0; } else if ((turrUsage & FrameUsage.Frames08Cardinal) != FrameUsage.None) { // Never used for a turret, but whatever. turret1Icon = ((BodyShape[Facing32[unit.Direction.ID]] + 2) / 4) & 0x07; turret2Icon = turret1Icon; } else if ((turrUsage & FrameUsage.Frames16Simple) != FrameUsage.None) { turret1Icon = BodyShape[Facing16[unit.Direction.ID] * 2] / 2; turret2Icon = turret1Icon; } else if ((turrUsage & FrameUsage.Frames16Symmetrical) != FrameUsage.None) { // Special case for 16-frame rotation saved as 8-frame because it is symmetrical and thus the second half of the frames is the same (MGG) turret1Icon = (BodyShape[Facing32[unit.Direction.ID]] / 2) & 7; turret2Icon = turret1Icon; } else if ((turrUsage & FrameUsage.Frames32Full) != FrameUsage.None) { turret1Icon = BodyShape[Facing32[unit.Direction.ID]]; turret2Icon = turret1Icon; } else if ((turrUsage & FrameUsage.Rotor) != FrameUsage.None) { turret1Icon = (unit.Direction.ID >> 5) % 2 == 1 ? 9 : 5; turret2Icon = (unit.Direction.ID >> 5) % 2 == 1 ? 8 : 4; } // If same as body name, add body frames. turret1Icon = unit.Type.Name.Equals(turretName, StringComparison.OrdinalIgnoreCase) ? bodyFrames + turret1Icon : turret1Icon; turret2Icon = unit.Type.Name.Equals(turret2Name, StringComparison.OrdinalIgnoreCase) ? bodyFrames + turret2Icon : turret2Icon; if (turretName != null) Globals.TheTilesetManager.GetTeamColorTileData(turretName, turret1Icon, teamColor, out turretTile, false, false); if (turret2Name != null) Globals.TheTilesetManager.GetTeamColorTileData(turret2Name, turret2Icon, teamColor, out turret2Tile, false, false); // Flatbed is a special case; if it is used, TurretOffset is ignored. if (turrUsage.HasFlag(FrameUsage.OnFlatBed)) { // OnFlatBed indicates the turret oFfset is determined by the BackTurretAdjust table. turretAdjust = BackTurretAdjust[Facing32[unit.Direction.ID]]; // Never actually used for 2 turrets. Put second turret in the front? turret2Adjust = BackTurretAdjust[Facing32[(byte)((unit.Direction.ID + DirectionTypes.South.ID) & 0xFF)]]; } else if (unit.Type.TurretOffset != 0) { // Used by ships and by the transport helicopter. int distance = unit.Type.TurretOffset; int face = (unit.Direction.ID >> 5) & 7; if (turrUsage.HasFlag(FrameUsage.Rotor)) { // Rotor stretch distance is given by a table. distance *= HeliDistanceAdjust[face]; } int x = 0; int y = 0; // For vessels, perspective stretch is simply done as '/ 2'. int perspectiveDivide = unit.Type.IsVessel ? 2 : 1; MovePoint(ref x, ref y, unit.Direction.ID, distance, perspectiveDivide); turretAdjust.X += x; turretAdjust.Y += y; if (unit.Type.HasDoubleTurret) { x = 0; y = 0; MovePoint(ref x, ref y, (byte)((unit.Direction.ID + DirectionTypes.South.ID) & 0xFF), distance, perspectiveDivide); turret2Adjust.X += x; turret2Adjust.Y += y; } } // Adjust Y-offset. turretAdjust.Y += unit.Type.TurretY; turret2Adjust.Y += unit.Type.TurretY; } float alphaFactor = 1.0f; if (!fullOpaque) { if (unit.IsPreview) { alphaFactor *= Globals.PreviewAlphaFloat; } alphaFactor = alphaFactor.Restrict(0, 1); } void render(Graphics g) { using (ImageAttributes imageAttributes = new ImageAttributes()) { imageAttributes.SetColorMatrix(GetColorMatrix(Color.White, 1.0f, alphaFactor)); // Combine body and turret to one image, then paint it. This is done because it might be semitransparent. using (Bitmap unitBm = new Bitmap(renderBounds.Width, renderBounds.Height)) { unitBm.SetResolution(96, 96); using (Graphics unitG = Graphics.FromImage(unitBm)) { unitG.CopyRenderSettingsFrom(g); if (tile != null) { unitG.DrawImage(tile.Image, renderRect, 0, 0, tile.Image.Width, tile.Image.Height, GraphicsUnit.Pixel); } if (unit.Type.HasTurret) { Point center = new Point(renderBounds.Width / 2, renderBounds.Height / 2); void RenderTurret(Graphics ug, Tile turrTile, Point turrAdjust, Size tSize) { Size turretSize = turrTile.Image.Size; Size turretRenderSize = new Size(turretSize.Width * tSize.Width / Globals.OriginalTileWidth, turretSize.Height * tSize.Height / Globals.OriginalTileHeight); Rectangle turrBounds = new Rectangle(center - new Size(turretRenderSize.Width / 2, turretRenderSize.Height / 2), turretRenderSize); turrBounds.Offset( turrAdjust.X * tSize.Width / Globals.PixelWidth, turrAdjust.Y * tSize.Height / Globals.PixelHeight ); ug.DrawImage(turrTile.Image, turrBounds, 0, 0, turrTile.Image.Width, turrTile.Image.Height, GraphicsUnit.Pixel); } if (turretTile != null && turretTile.Image != null) { RenderTurret(unitG, turretTile, turretAdjust, tileSize); } if (unit.Type.HasDoubleTurret && turret2Tile != null && turret2Tile.Image != null) { RenderTurret(unitG, turret2Tile, turret2Adjust, tileSize); } } } g.DrawImage(unitBm, renderBounds, 0, 0, renderBounds.Width, renderBounds.Height, GraphicsUnit.Pixel, imageAttributes); } } } Point centerPoint = GetVehicleRenderPoint(); Point usedCenter = new Point(topLeft.X * Globals.PixelWidth + centerPoint.X, topLeft.Y * Globals.PixelHeight + centerPoint.Y); return new RenderInfo(usedCenter, render, unit); } public static (Rectangle, Action) RenderWaypoint(GameInfo gameInfo, bool soloMission, Size tileSize, ITeamColor[] flagColors, Waypoint waypoint, float alphaFactor, int offset, ShapeCacheManager cacheManager) { if (!waypoint.Point.HasValue) { return (Rectangle.Empty, (g) => { }); } Point point = waypoint.Point.Value; bool isDefaultIcon = true; string tileGraphics = "trans.icn"; int icon = 3; ITeamColor teamColor = null; double sizeMultiplier = 1; if (waypoint.IsPreview) { alphaFactor *= Globals.PreviewAlphaFloat; } alphaFactor = alphaFactor.Restrict(0, 1); int mpId = soloMission ? -1 : Waypoint.GetMpIdFromFlag(waypoint.Flag); Bitmap image = null; if (waypoint.Flag.HasFlag(WaypointFlag.CrateSpawn)) { isDefaultIcon = false; tileGraphics = "scrate"; icon = 0; sizeMultiplier = 2; } else if (mpId >= 0 && mpId < flagColors.Length) { isDefaultIcon = false; tileGraphics = "flagfly"; icon = 0; teamColor = flagColors[mpId]; // Always paint flags as opaque. //transparencyModifier = 1.0f; } string id = "waypoint_" + tileGraphics + "_icn" + icon + "_x" + sizeMultiplier + "_mpid" + mpId; image = cacheManager.GetImage(id); if (image == null) { bool gotTile = Globals.TheTilesetManager.GetTeamColorTileData(tileGraphics, icon, teamColor, out Tile tile); if (gotTile) { image = new Bitmap(tile.Image); } else if (isDefaultIcon) { using (Bitmap selectCursor = Globals.TheTilesetManager.GetTexture(@"DATA\ART\TEXTURES\SRGB\ICON_SELECT_FRIENDLY_X2_00.DDS", tileGraphics, icon, true)) { Rectangle opaqueBounds = ImageUtils.CalculateOpaqueBounds(selectCursor); image = selectCursor.FitToBoundingBox(opaqueBounds, Globals.OriginalTileHeight, Globals.OriginalTileHeight, Color.Transparent); } } if (image != null) { cacheManager.AddImage(id, image); } } if (image == null) { Debug.Print(string.Format("Waypoint graphics {0} ({1}) not found", tileGraphics, icon)); return (Rectangle.Empty, (g) => { }); } Point location = new Point(point.X * tileSize.Width, point.Y * tileSize.Height); Size renderSize = new Size(image.Width * tileSize.Width / Globals.OriginalTileWidth, image.Height * tileSize.Height / Globals.OriginalTileHeight); renderSize.Width = (int)Math.Round(renderSize.Width * sizeMultiplier); renderSize.Height = (int)Math.Round(renderSize.Height * sizeMultiplier); Rectangle renderBounds = new Rectangle(location, renderSize); Rectangle imgBounds = new Rectangle(Point.Empty, image.Size); bool isClipping = renderSize.Width > tileSize.Width || renderSize.Height > tileSize.Height; if (tileSize.Width > renderSize.Width) { // Pad. This rounds upwards because bottom and left are generally shadows. renderBounds.X += (int)Math.Round((tileSize.Width - renderSize.Width) / 2.0, MidpointRounding.AwayFromZero); } else if (tileSize.Width < renderSize.Width) { // Crop renderBounds.Width = tileSize.Width; imgBounds.Width = (int)Math.Round(tileSize.Width / sizeMultiplier); imgBounds.X = (image.Width - imgBounds.Width) / 2; } if (tileSize.Height > renderSize.Height) { // Pad. This rounds upwards because bottom and left are generally shadows. renderBounds.Y += (int)Math.Round((tileSize.Height - renderSize.Height) / 2.0, MidpointRounding.AwayFromZero); } else if (tileSize.Height < renderSize.Height) { // Crop renderBounds.Height = tileSize.Height; imgBounds.Height = (int)Math.Round(tileSize.Height / sizeMultiplier); imgBounds.Y = (image.Height - imgBounds.Height) / 2; } // Apply offset int actualOffsetX = offset * tileSize.Width / Globals.PixelWidth; int actualOffsetY = offset * tileSize.Height / Globals.PixelHeight; renderBounds.X += actualOffsetX; renderBounds.Y += actualOffsetY; renderBounds.Width = Math.Max(0, renderBounds.Width - actualOffsetX); renderBounds.Height = Math.Max(0, renderBounds.Height - actualOffsetY); void render(Graphics g) { using (ImageAttributes imageAttributes = new ImageAttributes()) { imageAttributes.SetColorMatrix(GetColorMatrix(Color.White, 1.0f, alphaFactor)); if (renderBounds.Width > 0 && renderBounds.Height > 0 && imgBounds.Width > 0 && imgBounds.Height > 0) { g.DrawImage(image, renderBounds, imgBounds.X, imgBounds.Y, imgBounds.Width, imgBounds.Height, GraphicsUnit.Pixel, imageAttributes); } } } return (renderBounds, render); } private static Point GetVehicleRenderPoint() { return new Point(Globals.PixelWidth / 2, Globals.PixelHeight / 2); } private static Point GetInfantryRenderPoint(InfantryStoppingType ist) { return InfantryGroup.RenderPosition(ist, true); } private static Point GetTerrainRenderPoint(Terrain terrain) { return terrain.Type.CenterPoint; } private static Point GetBuildingRenderPoint(Building building) { return GeneralUtils.GetOccupiedCenter(building.Type.BaseOccupyMask, new Size(Globals.PixelWidth, Globals.PixelHeight)); } public static void RenderAllBoundsFromCell(Graphics graphics, Rectangle visibleCells, Size tileSize, IEnumerable<(int, T)> renderList, CellMetrics metrics) { RenderAllBoundsFromCell(graphics, visibleCells, tileSize, renderList, metrics, Color.Green); } public static void RenderAllBoundsFromCell(Graphics graphics, Rectangle visibleCells, Size tileSize, IEnumerable<(int, T)> renderList, CellMetrics metrics, Color boundsColor) { RenderAllBoundsFromCell(graphics, visibleCells, tileSize, renderList.Select(tp => tp.Item1), metrics, boundsColor); } public static void RenderAllBoundsFromCell(Graphics graphics, Rectangle visibleCells, Size tileSize, IEnumerable renderList, CellMetrics metrics, Color boundsColor) { using (Pen boundsPen = new Pen(boundsColor, Math.Max(1, tileSize.Width / 16.0f))) { foreach (int cell in renderList) { if (metrics.GetLocation(cell, out Point topLeft) && visibleCells.Contains(topLeft)) { Rectangle bounds = new Rectangle(new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height), tileSize); graphics.DrawRectangle(boundsPen, bounds); } } } } public static void RenderAllBoundsFromPoint(Graphics graphics, Rectangle visibleCells, Size tileSize, IEnumerable<(Point, T)> renderList) { RenderAllBoundsFromPoint(graphics, visibleCells, tileSize, renderList.Select(tp => tp.Item1), Color.Green); } public static void RenderAllBoundsFromPoint(Graphics graphics, Rectangle visibleCells, Size tileSize, IEnumerable renderList, Color boundsColor) { using (Pen boundsPen = new Pen(boundsColor, Math.Max(1, tileSize.Width / 16.0f))) { foreach (Point topLeft in renderList.Where(pt => visibleCells.Contains(pt))) { Rectangle bounds = new Rectangle(new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height), tileSize); graphics.DrawRectangle(boundsPen, bounds); } } } public static void RenderAllBoundsFromPoint(Graphics graphics, Rectangle visibleCells, Size tileSize, IEnumerable<(Point, T)> renderList, Color boundsColor) { using (Pen boundsPen = new Pen(boundsColor, Math.Max(1, tileSize.Width / 16.0f))) { foreach ((Point topLeft, T _) in renderList.Where(pt => visibleCells.Contains(pt.Item1))) { Rectangle bounds = new Rectangle(new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height), tileSize); graphics.DrawRectangle(boundsPen, bounds); } } } public static void RenderAllOccupierBoundsGreen(Graphics graphics, Rectangle visibleCells, Size tileSize, IEnumerable<(Point, T)> occupiers) where T : ICellOccupier, ICellOverlapper { RenderAllOccupierBounds(graphics, visibleCells, tileSize, occupiers, Color.Green, Color.Transparent); } public static void RenderAllOccupierCellsRed(Graphics graphics, Rectangle visibleCells, Size tileSize, IEnumerable<(Point, T)> occupiers) where T : ICellOccupier, ICellOverlapper { RenderAllOccupierBounds(graphics, visibleCells, tileSize, occupiers, Color.Transparent, Color.Red); } public static void RenderAllOccupierBounds(Graphics graphics, Rectangle visibleCells, Size tileSize, IEnumerable<(Point, T)> occupiers) where T : ICellOccupier, ICellOverlapper { RenderAllOccupierBounds(graphics, visibleCells, tileSize, occupiers, Color.Green, Color.Red); } public static void RenderAllOccupierBounds(Graphics graphics, Rectangle visibleCells, Size tileSize, IEnumerable<(Point, T)> occupiers, Color boundsColor, Color occupierColor) where T : ICellOccupier, ICellOverlapper { float boundsPenSize = Math.Max(1, tileSize.Width / 16.0f); float occupyPenSize = Math.Max(0.5f, tileSize.Width / 32.0f); if (occupyPenSize == boundsPenSize) { boundsPenSize += 2; } if (boundsColor.A != 0) { using (Pen boundsPen = new Pen(boundsColor, boundsPenSize)) { foreach ((Point topLeft, T occupier) in occupiers) { Rectangle typeBounds = occupier.OverlapBounds; Rectangle bounds = new Rectangle( new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height), new Size(typeBounds.Width * tileSize.Width, typeBounds.Height * tileSize.Height) ); graphics.DrawRectangle(boundsPen, bounds); } } } if (occupierColor.A != 0) { using (Pen occupyPen = new Pen(occupierColor, occupyPenSize)) { foreach ((Point topLeft, T occupier) in occupiers) { bool[,] occupyMask = occupier is Building bl ? bl.Type.BaseOccupyMask : occupier.OccupyMask; for (int y = 0; y < occupyMask.GetLength(0); ++y) { for (int x = 0; x < occupyMask.GetLength(1); ++x) { if (!occupyMask[y, x]) { continue; } Rectangle occupyCellBounds = new Rectangle(new Point(topLeft.X + x, topLeft.Y + y), new Size(1, 1)); Rectangle occupyBounds = new Rectangle( new Point((topLeft.X + x) * tileSize.Width, (topLeft.Y + y) * tileSize.Height), tileSize); if (visibleCells.Contains(occupyCellBounds)) { graphics.DrawRectangle(occupyPen, occupyBounds); } } } } } } } public static void RenderAllCrateOutlines(Graphics g, GameInfo gameInfo, Map map, Rectangle visibleCells, Size tileSize, double tileScale, bool onlyIfBehindObjects) { RenderAllOverlayOutlines(g, gameInfo, map, visibleCells, tileSize, tileScale, OverlayTypeFlag.WoodCrate, onlyIfBehindObjects, Globals.OutlineColorCrateWood); RenderAllOverlayOutlines(g, gameInfo, map, visibleCells, tileSize, tileScale, OverlayTypeFlag.SteelCrate, onlyIfBehindObjects, Globals.OutlineColorCrateSteel); } public static void RenderAllSolidOverlayOutlines(Graphics g, GameInfo gameInfo, Map map, Rectangle visibleCells, Size tileSize, double tileScale, bool onlyIfBehindObjects) { RenderAllOverlayOutlines(g, gameInfo, map, visibleCells, tileSize, tileScale, OverlayTypeFlag.Solid, onlyIfBehindObjects, Globals.OutlineColorSolidOverlay); RenderAllOverlayOutlines(g, gameInfo, map, visibleCells, tileSize, tileScale, OverlayTypeFlag.Wall, onlyIfBehindObjects, Globals.OutlineColorWall); } public static void RenderAllOverlayOutlines(Graphics g, GameInfo gameInfo, Map map, Rectangle visibleCells, Size tileSize, double tileScale, OverlayTypeFlag types, bool onlyIfBehindObjects, Color outlineColor) { if (outlineColor.A == 0) { return; } Dictionary includedPoints = new Dictionary(); List includedCells = new List(); float outlineThickness = 0.05f; byte alphaThreshold = (byte)(Globals.UseClassicFiles ? 0x80 : 0x40); // Get all included points in an initial sweep so they're all available in the second processing step. foreach ((int cell, Overlay overlay) in map.Overlay) { OverlayType ovlt = overlay.Type; if (!ovlt.Flag.HasAnyFlags(types) || !map.Metrics.GetLocation(cell, out Point location)|| !visibleCells.Contains(location)) { continue; } if (onlyIfBehindObjects) { if (overlay.Type.OpaqueMask == null) { continue; } bool[] toCheck = overlay.Type.OpaqueMask[0, 0]; InfantryStoppingType[] toLoop = Enum.GetValues(typeof(InfantryStoppingType)).Cast().ToArray(); bool occupied = false; for (int i = 0; i < toLoop.Length; i++) { if (!toCheck[i]) { continue; } InfantryStoppingType ist = toLoop[i]; if (IsOverlapped(map, location, true, ist, overlay, -1)) { occupied = true; break; } } if (!occupied) { continue; } } includedCells.Add(cell); includedPoints.Add(location, overlay); } // Now we have all cells to process, we can ensure the outlines for neighbouring wall cells are combined. foreach (int cell in includedCells) { if (!map.Overlay.Metrics.GetLocation(cell, out Point p) || !includedPoints.TryGetValue(p, out Overlay overlay)) { continue; } OverlayType ovlt = overlay.Type; Size cellSize = new Size(1, 1); Color outlineCol = Color.FromArgb(0xA0, outlineColor); Overlay tstOvl; // If this is a wall, exclude edge cells on sides that are the same type of wall, so they connect properly. bool includeAbove = !ovlt.IsWall || !includedPoints.TryGetValue(p.OffsetPoint(0, -1), out tstOvl) || tstOvl.Type.ID != ovlt.ID; bool includeRight = !ovlt.IsWall || !includedPoints.TryGetValue(p.OffsetPoint(1, 0), out tstOvl) || tstOvl.Type.ID != ovlt.ID; bool includeBelow = !ovlt.IsWall || !includedPoints.TryGetValue(p.OffsetPoint(0, 1), out tstOvl) || tstOvl.Type.ID != ovlt.ID; bool includeLefty = !ovlt.IsWall || !includedPoints.TryGetValue(p.OffsetPoint(-1, 0), out tstOvl) || tstOvl.Type.ID != ovlt.ID; int aroundMask = (includeAbove ? 1 : 0) | (includeRight ? 2 : 0) | (includeBelow ? 4 : 0) | (includeLefty ? 8 : 0); string ovlId = "outline_ovl_" + overlay.Type.Name + "_" + overlay.Icon + "_" + aroundMask + "_" + tileSize.Width + "x" + tileSize.Height; RegionData paintAreaRel = Globals.TheShapeCacheManager.GetShape(ovlId); if (paintAreaRel == null) { using (Bitmap bm = new Bitmap(tileSize.Width * 3, tileSize.Height * 3, PixelFormat.Format32bppArgb)) { using (Graphics ig = Graphics.FromImage(bm)) { RenderOverlay(gameInfo, new Point(1, 1), null, tileSize, tileScale, overlay, true).Item2(ig); } paintAreaRel = ImageUtils.GetOutline(tileSize, bm, outlineThickness, alphaThreshold, Globals.UseClassicFiles); // Wall connecting: if any side cells should be excluded so they connect to neighbouring cells, // intersect the region with another region containing only the desired side cells. if (ovlt.IsWall && aroundMask != (1 | 2 | 4 | 8)) { using (Region outline = new Region(paintAreaRel)) { using (Region rgn = new Region(new Rectangle(new Point(tileSize.Width, tileSize.Height), tileSize))) { // Adds side cells that aren't cut off. if (includeAbove) rgn.Union(new Rectangle(new Point(tileSize.Width/**/, /**/ 0), tileSize)); if (includeRight) rgn.Union(new Rectangle(new Point(tileSize.Width * 2, tileSize.Height/**/), tileSize)); if (includeBelow) rgn.Union(new Rectangle(new Point(tileSize.Width/**/, tileSize.Height * 2), tileSize)); if (includeLefty) rgn.Union(new Rectangle(new Point(/**/ 0, tileSize.Height/**/), tileSize)); // Not sure if needed; adds corner cells as well if neighbouring cells are added. if (includeAbove && includeRight) rgn.Union(new Rectangle(new Point(tileSize.Width * 2, /**/ 0), tileSize)); if (includeRight && includeBelow) rgn.Union(new Rectangle(new Point(tileSize.Width * 2, tileSize.Height * 2), tileSize)); if (includeBelow && includeLefty) rgn.Union(new Rectangle(new Point(/**/ 0, tileSize.Height * 2), tileSize)); if (includeLefty && includeAbove) rgn.Union(new Rectangle(new Point(/**/ 0, /**/ 0), tileSize)); outline.Intersect(rgn); paintAreaRel = outline.GetRegionData(); } } } Globals.TheShapeCacheManager.AddShape(ovlId, paintAreaRel); } } int actualTopLeftX = tileSize.Width * (p.X -1); int actualTopLeftY = tileSize.Height * (p.Y -1); if (paintAreaRel != null) { using (Region paintArea = new Region(paintAreaRel)) using (Brush brush = new SolidBrush(outlineCol)) { paintArea.Translate(actualTopLeftX, actualTopLeftY); g.FillRegion(brush, paintArea); } } } } public static void RenderAllInfantryOutlines(Graphics g, Map map, Rectangle visibleCells, Size tileSize, bool onlyIfBehindObjects) { float outlineThickness = 0.05f; byte alphaThreshold = (byte)(Globals.UseClassicFiles ? 0x80 : 0x40); //double lumThreshold = 0.01d; visibleCells.Inflate(1, 1); foreach (var (location, infantryGroup) in map.Technos.OfType().OrderBy(i => map.Metrics.GetCell(i.Location))) { Size cellSize = new Size(1, 1); if (!visibleCells.Contains(location)) { continue; } InfantryStoppingType[] toCheck = Enum.GetValues(typeof(InfantryStoppingType)).Cast().ToArray(); foreach (InfantryStoppingType ist in toCheck) { Infantry infantry = infantryGroup.Infantry[(int)ist]; if (infantry == null) { continue; } if (onlyIfBehindObjects) { if (!IsOverlapped(map, location, false, ist, infantryGroup, infantry.DrawOrderCache)) { continue; } } Color outlineCol = Color.FromArgb(0x80, Globals.TheTeamColorManager.GetBaseColor(infantry.House?.UnitTeamColor)); string infId = "outline_inf_" + infantry.Type.Name + '_' + ((int)ist) + '_' + infantry.Direction.ID + "_" + tileSize.Width + "x" + tileSize.Height; RegionData paintAreaRel = Globals.TheShapeCacheManager.GetShape(infId); if (paintAreaRel == null) { using (Bitmap bm = new Bitmap(tileSize.Width * 3, tileSize.Height * 3, PixelFormat.Format32bppArgb)) { using (Graphics ig = Graphics.FromImage(bm)) { RenderInfantry(new Point(1, 1), tileSize, infantry, ist, true).RenderAction(ig); } paintAreaRel = ImageUtils.GetOutline(tileSize, bm, outlineThickness, alphaThreshold, Globals.UseClassicFiles); Globals.TheShapeCacheManager.AddShape(infId, paintAreaRel); } } // Rendered in a 3x3 cell frame, so subtract one. int actualTopLeftX = (location.X - 1) * tileSize.Width; int actualTopLeftY = (location.Y - 1) * tileSize.Height; if (paintAreaRel != null) { using (Region paintArea = new Region(paintAreaRel)) using (Brush brush = new SolidBrush(outlineCol)) { paintArea.Translate(actualTopLeftX, actualTopLeftY); g.FillRegion(brush, paintArea); } } } } } public static void RenderAllUnitOutlines(Graphics g, GameInfo gameInfo, Map map, Rectangle visibleCells, Size tileSize, bool onlyIfBehindObjects) { RenderAllObjectOutlines(g, gameInfo, map, map.Technos.OfType(), visibleCells, tileSize, true, (h) => h.UnitTeamColor, (gr, p, unt) => RenderUnit(gameInfo, new Point(1, 1), tileSize, unt, true).RenderAction(gr), Color.Black); } public static void RenderAllBuildingOutlines(Graphics g, GameInfo gameInfo, Map map, Rectangle visibleCells, Size tileSize, double tileScale, bool onlyIfBehindObjects) { RenderAllObjectOutlines(g, gameInfo, map, map.Buildings.OfType(), visibleCells, tileSize, true, (h) => h.BuildingTeamColor, (gr, p, bld) => RenderBuilding(gameInfo, null, p, tileSize, tileScale, bld, true).RenderAction(gr), Color.Black); } public static void RenderAllTerrainOutlines(Graphics g, GameInfo gameInfo, Map map, Rectangle visibleCells, Size tileSize, double tileScale, bool onlyIfBehindObjects) { RenderAllObjectOutlines(g, gameInfo, map, map.Technos.OfType(), visibleCells, tileSize, true, null, (gr, p, trn) => RenderTerrain(p, tileSize, tileScale, trn, true).RenderAction(gr), Globals.OutlineColorTerrain); } /// /// Unified function for rendering outlines for techno objects. Infantry are still separate, because of the whole InfantryGroup mess. /// /// Techno that's being handled. /// Graphics object to paint on. /// GameInfo object for this game. /// Map, to check metrics and object overlaps. /// The list of occupiers to render outlines for. /// Visible area on the screen in which these outlines should be rendered. /// Size of the map tiles. /// True to only render objects that are overlapped by something. /// Function to get the preferred team color name from a House. /// Action to render an object of the handled type at a specific point on a graphics object. /// Fallback colour in case no House or function are available. private static void RenderAllObjectOutlines(Graphics g, GameInfo gameInfo, Map map, IEnumerable<(Point Location, T Occupier)> occupiers, Rectangle visibleCells, Size tileSize, bool onlyIfBehindObjects, Func colorPick, Action RenderAction, Color fallbackColor) where T: ITechno, ICellOverlapper, ICellOccupier, ICloneable { float outlineThickness = 0.05f; byte alphaThreshold = (byte)(Globals.UseClassicFiles ? 0x80 : 0x40); //double lumThreshold = 0.01d; visibleCells.Inflate(1, 1); foreach ((Point objLocation, T placedObj) in occupiers.OrderBy(i => map.Metrics.GetCell(i.Location))) { // This is a visibility check; check cells that are deemed "visible". bool[,][] opaqueMask = placedObj.OpaqueMask; bool[,] occupyMask = placedObj.OccupyMask; int paintOrder = placedObj.DrawOrderCache; int maskY = opaqueMask == null ? 0 : opaqueMask.GetLength(0); int maskX = opaqueMask == null ? 0 : opaqueMask.GetLength(1); // If not in currently viewed area, ignore. Rectangle objBounds = new Rectangle(objLocation, placedObj.OccupyMask.GetDimensions()); if (!visibleCells.IntersectsWith(objBounds)) { continue; } if (onlyIfBehindObjects) { // Select actual map points for all visible points in opaqueMask bool allOpaque = true; // only evaluate center point Point[] opaquePoints = Enumerable.Range(0, maskY).SelectMany(nrY => Enumerable.Range(0, maskX).Select(nrX => new Point(nrX, nrY))) .Where(pt => opaqueMask[pt.Y, pt.X] != null && opaqueMask[pt.Y, pt.X].Length > 0 && opaqueMask[pt.Y, pt.X].Any()).ToArray(); foreach (Point opaquePoint in opaquePoints) { Point realPoint = new Point(objLocation.X + opaquePoint.X, objLocation.Y + opaquePoint.Y); InfantryStoppingType[] toCheck = Enum.GetValues(typeof(InfantryStoppingType)).Cast().ToArray(); bool isSubOverlapped = false; bool[] opaqueCellMask = opaqueMask[opaquePoint.Y, opaquePoint.X]; foreach (InfantryStoppingType ist in toCheck) { if (!opaqueCellMask[(int)ist]) { continue; } if (IsOverlapped(map, realPoint, false, ist, placedObj, paintOrder)) { isSubOverlapped = true; break; } } // if any of the occupied sub-cells are overlapped, consider it partially overlapped and thus eligible for outline. if (!isSubOverlapped) { allOpaque = false; break; } } if (!allOpaque) { continue; } } Size cellSize = new Size(maskX + 2, maskY + 2); Color houseCol = fallbackColor; if (placedObj.House != null && colorPick != null) { houseCol = Color.FromArgb(0x80, Globals.TheTeamColorManager.GetBaseColor(colorPick(placedObj.House))); } string id = "outline_" + typeof(T).Name + "_" + placedObj.TechnoType.Name + "_fr" + placedObj.DrawFrameCache + "_" + tileSize.Width + "x" + tileSize.Height; RegionData paintAreaRel = Globals.TheShapeCacheManager.GetShape(id); if (paintAreaRel == null) { // Clone without preview flag. T toRender = placedObj; if (placedObj.IsPreview) { toRender = (T)placedObj.Clone(); placedObj.IsPreview = false; if (placedObj is Building bld) { bld.BasePriority = 0; bld.IsPrebuilt = true; } } using (Bitmap bm = new Bitmap(tileSize.Width * cellSize.Width, tileSize.Height * cellSize.Width, PixelFormat.Format32bppArgb)) { using (Graphics ig = Graphics.FromImage(bm)) { RenderAction(ig, new Point(1, 1), toRender); } paintAreaRel = ImageUtils.GetOutline(tileSize, bm, outlineThickness, alphaThreshold, Globals.UseClassicFiles); Globals.TheShapeCacheManager.AddShape(id, paintAreaRel); } } int paintPosTopLeftX = (objLocation.X - 1) * tileSize.Width; int paintPosTopLeftY = (objLocation.Y - 1) * tileSize.Height; if (paintAreaRel != null) { using (Region paintArea = new Region(paintAreaRel)) using (Brush brush = new SolidBrush(houseCol)) { paintArea.Translate(paintPosTopLeftX, paintPosTopLeftY); g.FillRegion(brush, paintArea); } } } } /// /// Check if an object is considered overlapped by something on the map. /// /// Map to check on. /// Location to check for overlap. /// True to immediately return true if the cell is occupied by units. /// When filled in, the overlapper is treated as infantry on that location. /// Object for which overlap is being checked. This object is automatically ignored in the objects it loops over to check for overlaps. /// Cached paint order of the object, to easily check if it can be overlapped at all. /// true if the cell is considered filled enough to overlap things. private static bool IsOverlapped(Map map, Point location, bool unitsAlwaysOverlap, InfantryStoppingType? ist, ICellOverlapper objectToCheck, int objectPaintOrder) { ICellOccupier techno = map.Technos[location]; int subIndex = ist.HasValue ? (int)ist.Value : 0; // Single-cell occupier. Always pass. Since this is only used for single-cell objects, only map.Technos needs to be checked. if (unitsAlwaysOverlap && (techno is Unit || techno is InfantryGroup) && techno != objectToCheck) { if (!(techno is InfantryGroup ig) || ig.Infantry[subIndex] != null) { return true; } } // Logic for multi-cell occupiers; buildings and terrain. // Return true if either an occupied cell, or overlayed by graphics deemed opaque. ICellOverlapper[] technos = map.Overlappers.OverlappersAt(location).Where(ov => !ReferenceEquals(ov,objectToCheck)).ToArray(); if (technos.Length == 0) { return false; } // Overlappers list contains both buildings and other technos. foreach (ICellOverlapper ovl in technos) { ICellOccupier occ = ovl as ICellOccupier; if (occ == null) { continue; } bool[,][] opaqueMask = ovl.OpaqueMask; int maskY = opaqueMask == null ? 0 : opaqueMask.GetLength(0); int maskX = opaqueMask == null ? 0 : opaqueMask.GetLength(1); Point? pt = map.Technos[occ] ?? map.Buildings[occ]; if (!pt.HasValue) { continue; } // Object we're comparing with was drawn before the current one, so it can't possible overlap it. // This caching allows extremely easy checks on overlap without much processing. if (ovl is ITechno paintedObj && paintedObj.DrawOrderCache < objectPaintOrder) { continue; } if (ovl is InfantryGroup ig && (ig.Infantry[subIndex] == null || ig.Infantry[subIndex].DrawOrderCache < objectPaintOrder)) { continue; } // Get list of points, find current point in the list. Rectangle boundsRect = new Rectangle(pt.Value, new Size(maskX, maskY)); List pts = boundsRect.Points().OrderBy(p => p.Y * map.Metrics.Width + p.X).ToList(); int index = pts.IndexOf(location); if (index == -1) { continue; } // Trick to convert 2-dimensional arrays to linear format. bool[][] opaqueArr = opaqueMask.Cast().ToArray(); if (index < opaqueArr.Length && opaqueArr[index] != null && subIndex < opaqueArr[index].Length && opaqueArr[index][subIndex]) { // If obscured from view by graphics, return true. return true; } } return false; } public static void RenderAllFootballAreas(Graphics graphics, Map map, Rectangle visibleCells, Size tileSize, double tileScale, GameInfo gameInfo) { if (!gameInfo.SupportsMapLayer(MapLayerFlag.FootballArea)) { return; } HashSet footballPoints = new HashSet(); Rectangle renderArea = map.Metrics.Bounds; renderArea.Intersect(visibleCells); foreach (Waypoint waypoint in map.Waypoints) { if (!waypoint.Point.HasValue || Waypoint.GetMpIdFromFlag(waypoint.Flag) == -1) { continue; } Point[] roadPoints = new Rectangle(waypoint.Point.Value.X - 1, waypoint.Point.Value.Y - 1, 4, 3).Points().ToArray(); foreach (Point p in roadPoints.Where(p => renderArea.Contains(p))) { footballPoints.Add(p); } } foreach (Point p in footballPoints.OrderBy(p => p.Y * map.Metrics.Width + p.X)) { Overlay footballTerrain = new Overlay() { Type = SoleSurvivor.OverlayTypes.Road, IsPreview = true, }; RenderOverlay(gameInfo, p, null, tileSize, tileScale, footballTerrain, false).Item2(graphics); } } public static void RenderWaypointFlags(Graphics graphics, GameInfo gameInfo, Map map, Rectangle visibleCells, Size tileSize, ShapeCacheManager cacheManager) { // Re-render flags on top of football areas. List flagWayPoints = new List(); Dictionary flagOverlapMpCheck = new Dictionary(); Dictionary flagOffsets = new Dictionary(); // Get all waypoints. Ignore the preview if it is on the same cell as the same actual waypoint. // Preview waypoint is always only one, and added at the end, so a sequential run always works. foreach (Waypoint waypoint in map.Waypoints) { int mpId = Waypoint.GetMpIdFromFlag(waypoint.Flag); if (waypoint.Point.HasValue && mpId >= 0 && visibleCells.Contains(waypoint.Point.Value) && map.Metrics.GetCell(waypoint.Point.Value, out int cell)) { bool alreadyExists = flagOverlapMpCheck.TryGetValue(mpId, out int mpCell) && cell == mpCell; if (!alreadyExists) { flagWayPoints.Add(waypoint); flagOverlapMpCheck[mpId] = cell; } } } // Create offsets if multiple flags are on the same cell. flagWayPoints = flagWayPoints.OrderBy(w => Waypoint.GetMpIdFromFlag(w.Flag)).ToList(); Dictionary flagOverlapPoints = new Dictionary(); foreach (Waypoint waypoint in flagWayPoints) { if (map.Metrics.GetCell(waypoint.Point.Value, out int cell)) { if (!flagOverlapPoints.TryGetValue(cell, out int amount)) { flagOverlapPoints.Add(cell, 1); } else { flagOverlapPoints[cell] = amount + 1; } flagOffsets[waypoint] = amount * 2; } } // Paint the flags. ITeamColor[] flagColors = map.FlagColors; foreach (Waypoint wp in flagWayPoints) { flagOffsets.TryGetValue(wp, out int offset); RenderWaypoint(gameInfo, false, tileSize, flagColors, wp, wp.IsPreview ? Globals.PreviewAlphaFloat : 1.0f, offset, cacheManager).Item2(graphics); } } public static void RenderAllFakeBuildingLabels(Graphics graphics, GameInfo gameInfo, Map map, Rectangle visibleCells, Size tileSize) { RenderAllFakeBuildingLabels(graphics, gameInfo, map.Buildings.OfType(), visibleCells, tileSize); } public static void RenderAllFakeBuildingLabels(Graphics graphics, GameInfo gameInfo, IEnumerable<(Point topLeft, Building building)> buildings, Rectangle visibleCells, Size tileSize) { Color textColor = Color.White; Color backPaintColor = Color.Black; StringFormat stringFormat = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; string classicFont = null; bool cropClassicFont = false; TeamRemap remapClassicFont = null; if (Globals.TheTilesetManager is TilesetManagerClassic tsmc && Globals.TheTeamColorManager is TeamRemapManager trm) { classicFont = gameInfo.GetClassicFontInfo(ClassicFont.CellTriggers, tsmc, trm, textColor, out cropClassicFont, out remapClassicFont); } string fakeText = Globals.TheGameTextManager["TEXT_UI_FAKE"]; double tileScaleHor = tileSize.Width / 128.0; using (SolidBrush fakeBackgroundBrushPrev = new SolidBrush(Color.FromArgb(128 * 2 / 3, backPaintColor))) using (SolidBrush fakeBackgroundBrush = new SolidBrush(Color.FromArgb(256 * 2 / 3, backPaintColor))) using (ImageAttributes imageAttributes = new ImageAttributes()) { foreach ((Point topLeft, Building building) in buildings) { if (!building.Type.IsFake) { continue; } Rectangle buildingCellBounds = new Rectangle(topLeft, building.Type.Size); if (!visibleCells.IntersectsWith(buildingCellBounds)) { continue; } bool forPreview = building.IsPreview; Size maxSize = building.Type.Size; Rectangle buildingBounds = new Rectangle( new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height), new Size(maxSize.Width * tileSize.Width, maxSize.Height * tileSize.Height) ); if (classicFont == null) { using (SolidBrush fakeTextBrush = new SolidBrush(Color.FromArgb(forPreview ? Globals.PreviewAlphaInt : 255, textColor))) { using (Font font = graphics.GetAdjustedFont(fakeText, SystemFonts.DefaultFont, buildingBounds.Width, buildingBounds.Height, Math.Max(1, (int)Math.Round(12 * tileScaleHor)), Math.Max(1, (int)Math.Round(24 * tileScaleHor)), stringFormat, true)) { SizeF textBounds = graphics.MeasureString(fakeText, font, buildingBounds.Width, stringFormat); RectangleF backgroundBounds = new RectangleF(buildingBounds.Location, textBounds); graphics.FillRectangle(forPreview ? fakeBackgroundBrushPrev : fakeBackgroundBrush, backgroundBounds); graphics.DrawString(fakeText, font, fakeTextBrush, backgroundBounds, stringFormat); } } } else { Rectangle buildingRenderBounds = new Rectangle( new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height), new Size(maxSize.Width * tileSize.Width, maxSize.Height * tileSize.Height) ); string fkId = "fake_classic_" + maxSize.Width + "x" + maxSize.Height; Bitmap fkBm = Globals.TheShapeCacheManager.GetImage(fkId); if (fkBm == null) { Rectangle buildingBoundsClassic = new Rectangle(Point.Empty, new Size(maxSize.Width * Globals.OriginalTileWidth, maxSize.Height * Globals.OriginalTileHeight)); fkBm = new Bitmap(buildingBoundsClassic.Width, buildingBoundsClassic.Height); using (Graphics bmgr = Graphics.FromImage(fkBm)) { int[] indices = Encoding.ASCII.GetBytes(fakeText).Select(x => (int)x).ToArray(); using (Bitmap txt = RenderTextFromSprite(classicFont, remapClassicFont, Size.Empty, indices, false, cropClassicFont)) { int frameWidth = Math.Min(txt.Width + 2, buildingBoundsClassic.Width); int frameHeight = Math.Min(txt.Height + 2, buildingBoundsClassic.Height); Rectangle backgroundBounds = new Rectangle(Point.Empty, new Size(frameWidth, frameHeight)); Rectangle frameRect = new Rectangle(0, 0, frameWidth, frameHeight); Rectangle textRect = new Rectangle(1, 1, txt.Width, txt.Height); bmgr.FillRectangle(fakeBackgroundBrush, frameRect); bmgr.DrawImage(txt, textRect, 0, 0, txt.Width, txt.Height, GraphicsUnit.Pixel); } } Globals.TheShapeCacheManager.AddImage(fkId, fkBm); } imageAttributes.SetColorMatrix(GetColorMatrix(Color.White, 1.0f, building.IsPreview ? 0.5f : 1.0f)); graphics.DrawImage(fkBm, buildingRenderBounds, 0, 0, fkBm.Width, fkBm.Height, GraphicsUnit.Pixel, imageAttributes); } } } } public static void RenderAllRebuildPriorityLabels(Graphics graphics, GameInfo gameInfo, IEnumerable<(Point topLeft, Building building)> buildings, Rectangle visibleCells, Size tileSize, double tilescale) { Color textColor = Color.Red; Color backPaintColor = Color.Black; StringFormat stringFormat = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; string classicFont = null; bool cropClassicFont = false; TeamRemap remapClassicFont = null; if (Globals.TheTilesetManager is TilesetManagerClassic tsmc && Globals.TheTeamColorManager is TeamRemapManager trm) { classicFont = gameInfo.GetClassicFontInfo(ClassicFont.CellTriggers, tsmc, trm, textColor, out cropClassicFont, out remapClassicFont); } foreach ((Point topLeft, Building building) in buildings) { if (building.BasePriority < 0 || !visibleCells.IntersectsWith(new Rectangle(topLeft, building.Type.Size))) { continue; } Size maxSize = building.Type.Size; Rectangle buildingRenderBounds = new Rectangle( new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height), new Size(maxSize.Width * tileSize.Width, maxSize.Height * tileSize.Height) ); double tileScaleHor = tileSize.Width / 128.0; string priText = building.BasePriority.ToString(); using (SolidBrush baseBackgroundBrush = new SolidBrush(Color.FromArgb(256 * 2 / 3, backPaintColor))) using (ImageAttributes imageAttributes = new ImageAttributes()) { imageAttributes.SetColorMatrix(GetColorMatrix(Color.White, 1.0f, building.IsPreview ? 0.5f : 1.0f)); if (classicFont == null) { using (SolidBrush baseTextBrush = new SolidBrush(Color.FromArgb(255, textColor))) using (Font font = graphics.GetAdjustedFont(priText, SystemFonts.DefaultFont, buildingRenderBounds.Width, buildingRenderBounds.Height, Math.Max(1, (int)Math.Round(12 * tileScaleHor)), Math.Max(1, (int)Math.Round(24 * tileScaleHor)), stringFormat, true)) { SizeF textBounds = graphics.MeasureString(priText, font, buildingRenderBounds.Width, stringFormat); RectangleF backgroundBounds = new RectangleF(new PointF(buildingRenderBounds.X, buildingRenderBounds.Y), textBounds); backgroundBounds.Offset((buildingRenderBounds.Width - textBounds.Width) / 2.0f, buildingRenderBounds.Height - textBounds.Height); graphics.FillRectangle(baseBackgroundBrush, backgroundBounds); graphics.DrawString(priText, font, baseTextBrush, backgroundBounds, stringFormat); } } else { string priId = "priority_classic_" + maxSize.Width + "x" + maxSize.Height + "_" + priText; Bitmap priBm = Globals.TheShapeCacheManager.GetImage(priId); if (priBm == null) { Rectangle buildingBounds = new Rectangle(Point.Empty, new Size(maxSize.Width * Globals.OriginalTileWidth, maxSize.Height * Globals.OriginalTileHeight)); priBm = new Bitmap(buildingBounds.Width, buildingBounds.Height); using (Graphics bmgr = Graphics.FromImage(priBm)) { int[] indices = Encoding.ASCII.GetBytes(priText).Select(x => (int)x).ToArray(); using (Bitmap txt = RenderTextFromSprite(classicFont, remapClassicFont, Size.Empty, indices, false, cropClassicFont)) { int textOffsetX = (buildingBounds.Width - txt.Width) / 2; int textOffsetY = (buildingBounds.Height - txt.Height - 1); int frameOffsetX = Math.Max(textOffsetX, -tileSize.Width) - 1; int frameOffsetY = Math.Max(textOffsetY, -tileSize.Height) - 1; int frameWidth = Math.Min(txt.Width + 2, buildingBounds.Width); int frameHeight = Math.Min(txt.Height + 2, buildingBounds.Height); Rectangle frameRect = new Rectangle(frameOffsetX, frameOffsetY, frameWidth, frameHeight); Rectangle textRect = new Rectangle(textOffsetX, textOffsetY, txt.Width, txt.Height); bmgr.FillRectangle(baseBackgroundBrush, frameRect); bmgr.DrawImage(txt, textRect, 0, 0, txt.Width, txt.Height, GraphicsUnit.Pixel); } } Globals.TheShapeCacheManager.AddImage(priId, priBm); } imageAttributes.SetColorMatrix(GetColorMatrix(Color.White, 1.0f, building.IsPreview ? 0.5f : 1.0f)); graphics.DrawImage(priBm, buildingRenderBounds, 0, 0, priBm.Width, priBm.Height, GraphicsUnit.Pixel, imageAttributes); } } } } public static void RenderAllTechnoTriggers(Graphics graphics, GameInfo gameInfo, Map map, Rectangle visibleCells, Size tileSize, MapLayerFlag layersToRender) { RenderAllTechnoTriggers(graphics, gameInfo, map.Technos, map.Buildings, visibleCells, tileSize, layersToRender, Color.LimeGreen, null, false); } public static void RenderAllTechnoTriggers(Graphics graphics, GameInfo gameInfo, OccupierSet mapTechnos, OccupierSet mapBuildings, Rectangle visibleCells, Size tileSize, MapLayerFlag layersToRender, Color color, string toPick, bool excludePick) { string classicFontLarge = null; bool cropClassicFontLarge = false; string classicFontSmall = null; bool cropClassicFontSmall = false; TeamRemap remapClassicFontLarge = null; TeamRemap remapClassicFontSmall = null; if (Globals.TheTilesetManager is TilesetManagerClassic tsmc && Globals.TheTeamColorManager is TeamRemapManager trm) { classicFontLarge = gameInfo.GetClassicFontInfo(ClassicFont.TechnoTriggers, tsmc, trm, color, out cropClassicFontLarge, out remapClassicFontLarge); classicFontSmall = gameInfo.GetClassicFontInfo(ClassicFont.TechnoTriggersSmall, tsmc, trm, color, out cropClassicFontSmall, out remapClassicFontSmall); } double tileScaleHor = tileSize.Width / 128.0; float borderSize = Math.Max(0.5f, tileSize.Width / 60.0f); // The "bounds" in this are a bit weird; they have the real paint position, but the size is relative to Globals.OriginalTileSize List<(string trigger, Rectangle bounds, int alpha)> allTriggers = new List<(string trigger, Rectangle bounds, int alpha)>(); if (mapTechnos != null) { foreach ((Point topLeft, ICellOccupier techno) in mapTechnos) { Point location = new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height); (string trigger, Rectangle bounds, int alpha)[] triggers = null; if (techno is Terrain terrain && !Trigger.IsEmpty(terrain.Trigger)) { if (layersToRender.HasFlag(MapLayerFlag.Terrain)) { if (visibleCells.IntersectsWith(new Rectangle(topLeft, terrain.Type.Size))) { Size size = new Size(terrain.Type.Size.Width * Globals.OriginalTileWidth, terrain.Type.Size.Height * Globals.OriginalTileHeight); triggers = new (string, Rectangle, int)[] { (terrain.Trigger, new Rectangle(location, size), terrain.IsPreview ? Globals.PreviewAlphaInt : 256) }; } } } else if (techno is Unit unit && !Trigger.IsEmpty(unit.Trigger)) { if (layersToRender.HasFlag(MapLayerFlag.Units)) { if (visibleCells.Contains(topLeft)) { triggers = new (string, Rectangle, int)[] { (unit.Trigger, new Rectangle(location, Globals.OriginalTileSize), unit.IsPreview ? Globals.PreviewAlphaInt : 256) }; } } } else if (techno is InfantryGroup infantryGroup) { if (layersToRender.HasFlag(MapLayerFlag.Infantry)) { if (!visibleCells.Contains(topLeft)) { continue; } Size size = tileSize; Size boundSize = Globals.OriginalTileSize; List<(string, Rectangle, int)> infantryTriggers = new List<(string, Rectangle, int)>(); for (int i = 0; i < infantryGroup.Infantry.Length; ++i) { Infantry infantry = infantryGroup.Infantry[i]; if (infantry == null || Trigger.IsEmpty(infantry.Trigger)) { continue; } Size offset = Size.Empty; switch ((InfantryStoppingType)i) { case InfantryStoppingType.UpperLeft: offset.Width = -size.Width / 4; offset.Height = -size.Height / 4; break; case InfantryStoppingType.UpperRight: offset.Width = size.Width / 4; offset.Height = -size.Height / 4; break; case InfantryStoppingType.LowerLeft: offset.Width = -size.Width / 4; offset.Height = size.Height / 4; break; case InfantryStoppingType.LowerRight: offset.Width = size.Width / 4; offset.Height = size.Height / 4; break; } Rectangle bounds = new Rectangle(location + offset, boundSize); infantryTriggers.Add((infantry.Trigger, bounds, infantry.IsPreview ? Globals.PreviewAlphaInt : 256)); } triggers = infantryTriggers.ToArray(); } } if (triggers != null) { allTriggers.AddRange(triggers); } } } if (mapBuildings != null) { foreach ((Point topLeft, ICellOccupier techno) in mapBuildings) { Point location = new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height); if (techno is Building building && !Trigger.IsEmpty(building.Trigger)) { if (layersToRender.HasFlag(MapLayerFlag.Buildings)) { if (visibleCells.IntersectsWith(new Rectangle(topLeft, building.Type.Size))) { Size size = new Size(building.Type.Size.Width * Globals.OriginalTileWidth, building.Type.Size.Height * Globals.OriginalTileHeight); allTriggers.Add((building.Trigger, new Rectangle(location, size), building.IsPreview ? Globals.PreviewAlphaInt : 256)); } } } } } StringFormat stringFormat = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; foreach ((string trigger, Rectangle bounds, int alpha) in allTriggers.Where(x => toPick == null || (excludePick && !x.trigger.Equals(toPick, StringComparison.OrdinalIgnoreCase)) || (!excludePick && x.trigger.Equals(toPick, StringComparison.OrdinalIgnoreCase)))) { // Larger than a single cell. bool isLarge = bounds.Width > Globals.OriginalTileWidth; string classicFont = isLarge ? classicFontLarge : classicFontSmall; bool cropClassicFont = isLarge ? cropClassicFontLarge : cropClassicFontSmall; TeamRemap remapClassicFont = isLarge ? remapClassicFontLarge : remapClassicFontSmall; Color alphaColor = Color.FromArgb(alpha.Restrict(0,255), color); if (classicFont == null) { int width = bounds.Width * tileSize.Width / Globals.OriginalTileWidth; int height = bounds.Height * tileSize.Height / Globals.OriginalTileHeight; Rectangle realBounds = new Rectangle(bounds.Location, new Size(width, height)); using (SolidBrush technoTriggerBackgroundBrush = new SolidBrush(Color.FromArgb((96 * alpha / 256).Restrict(0, 255), Color.Black))) using (SolidBrush technoTriggerBrush = new SolidBrush(alphaColor)) using (Pen technoTriggerPen = new Pen(alphaColor, borderSize)) using (Font font = graphics.GetAdjustedFont(trigger, SystemFonts.DefaultFont, width, height, Math.Max(1, (int)Math.Round(12 * tileScaleHor)), Math.Max(1, (int)Math.Round(24 * tileScaleHor)), stringFormat, true)) { SizeF textBounds = graphics.MeasureString(trigger, font, width, stringFormat); RectangleF backgroundBounds = new RectangleF(bounds.Location, textBounds); backgroundBounds.Offset((width - textBounds.Width) / 2.0f, (height - textBounds.Height) / 2.0f); graphics.FillRectangle(technoTriggerBackgroundBrush, backgroundBounds); graphics.DrawRectangle(technoTriggerPen, Rectangle.Round(backgroundBounds)); graphics.DrawString(trigger, font, technoTriggerBrush, realBounds, stringFormat); } } else { int[] indices = Encoding.ASCII.GetBytes(trigger).Select(x => (int)x).ToArray(); using (SolidBrush technoTriggerBackgroundBrush = new SolidBrush(Color.FromArgb(96, Color.Black))) using (Pen technoTriggerPen = new Pen(color, 1)) using (Bitmap txt = RenderTextFromSprite(classicFont, remapClassicFont, Size.Empty, indices, false, cropClassicFont)) using (Bitmap txt2 = new Bitmap(txt.Width + 4, txt.Height + 4)) using (ImageAttributes imageAttributes = new ImageAttributes()) { txt2.SetResolution(96, 96); using (Graphics txt2g = Graphics.FromImage(txt2)) { txt2g.FillRectangle(technoTriggerBackgroundBrush, new Rectangle(1, 1, txt2.Width - 2, txt2.Height - 2)); txt2g.DrawRectangle(technoTriggerPen, new Rectangle(0, 0, txt2.Width - 1, txt2.Height - 1)); txt2g.DrawImage(txt, new Rectangle(2, 2, txt.Width, txt.Height)); } imageAttributes.SetColorMatrix(GetColorMatrix(Color.White, 1.0f, alpha / 256.0f)); int paintOffsX = (bounds.Width - txt2.Width) / 2 * tileSize.Width / Globals.OriginalTileWidth; int paintOffsY = (bounds.Height - txt2.Height) / 2 * tileSize.Width / Globals.OriginalTileWidth; int textWidth = txt2.Width * tileSize.Width / Globals.OriginalTileWidth; int textHeight = txt2.Height * tileSize.Width / Globals.OriginalTileWidth; Rectangle paintBounds = new Rectangle(bounds.Location, new Size(textWidth, textHeight)); paintBounds.Offset(new Point(paintOffsX, paintOffsY)); graphics.DrawImage(txt2, paintBounds, 0, 0, txt2.Width, txt2.Height, GraphicsUnit.Pixel, imageAttributes); } } } } public static void RenderWayPointIndicators(Graphics graphics, Map map, GameInfo gameInfo, Rectangle visibleCells, Size tileSize, Color textColor, bool forPreview, bool excludeSpecified, params Waypoint[] specified) { HashSet specifiedWaypoints = specified.ToHashSet(); Waypoint[] toPaint = excludeSpecified ? map.Waypoints : specified; string classicFontShort = null; bool cropClassicFontShort = false; TeamRemap remapClassicFontShort = null; string classicFontLong = null; bool cropClassicFontLong = false; TeamRemap remapClassicFontLong = null; if (Globals.TheTilesetManager is TilesetManagerClassic tsmc && Globals.TheTeamColorManager is TeamRemapManager trm) { classicFontShort = gameInfo.GetClassicFontInfo(ClassicFont.Waypoints, tsmc, trm, textColor, out cropClassicFontShort, out remapClassicFontShort); classicFontLong = gameInfo.GetClassicFontInfo(ClassicFont.WaypointsLong, tsmc, trm, textColor, out cropClassicFontLong, out remapClassicFontLong); } foreach (Waypoint waypoint in toPaint) { if ((excludeSpecified && specifiedWaypoints.Contains(waypoint)) || !waypoint.Cell.HasValue || !map.Metrics.GetLocation(waypoint.Cell.Value, out Point topLeft) || !visibleCells.Contains(topLeft)) { continue; } Rectangle paintBounds = new Rectangle(new Point(topLeft.X * tileSize.Width, topLeft.Y * tileSize.Height), tileSize); string wpText = waypoint.Name; bool isLong = wpText.Length > 3; string classicFont = isLong ? classicFontLong : classicFontShort; bool cropClassicFont = isLong ? cropClassicFontLong : cropClassicFontShort; TeamRemap remapClassicFont = isLong ? remapClassicFontLong : remapClassicFontShort; if (classicFont != null && isLong) { wpText = waypoint.ShortName; } Color backPaintColor = Color.FromArgb(128, Color.Black); // Adjust calcuations to tile size. The below adjustments are done assuming the tile is 128 wide. double tileScaleHor = tileSize.Width / 128.0; using (SolidBrush baseBackgroundBrush = new SolidBrush(backPaintColor)) using (ImageAttributes imageAttributes = new ImageAttributes()) { imageAttributes.SetColorMatrix(GetColorMatrix(Color.White, 1.0f, forPreview ? 0.5f : 1.0f)); if (classicFont == null) { StringFormat stringFormat = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; using (SolidBrush baseTextBrush = new SolidBrush(Color.FromArgb(forPreview ? 128 : 255, textColor))) using (Font font = graphics.GetAdjustedFont(wpText, SystemFonts.DefaultFont, paintBounds.Width, paintBounds.Height, Math.Max(1, (int)Math.Round(12 * tileScaleHor)), Math.Max(1, (int)Math.Round(55 * tileScaleHor)), stringFormat, true)) { SizeF textBounds = graphics.MeasureString(wpText, font, paintBounds.Width, stringFormat); RectangleF backgroundBounds = new RectangleF(paintBounds.Location, textBounds); backgroundBounds.Offset((paintBounds.Width - textBounds.Width) / 2.0f, (paintBounds.Height - textBounds.Height) / 2.0f); graphics.FillRectangle(baseBackgroundBrush, backgroundBounds); graphics.DrawString(wpText, font, baseTextBrush, backgroundBounds, stringFormat); } } else { string wpId = "waypoint_" + wpText + "_" + classicFont + "_" + remapClassicFont.Name; Bitmap wpBm = Globals.TheShapeCacheManager.GetImage(wpId); if (wpBm == null) { wpBm = new Bitmap(tileSize.Width, tileSize.Height); int[] indices = Encoding.ASCII.GetBytes(wpText).Select(x => (int)x).ToArray(); using (Graphics bmgr = Graphics.FromImage(wpBm)) using (Bitmap txt = RenderTextFromSprite(classicFont, remapClassicFont, Size.Empty, indices, false, cropClassicFont)) { int textOffsetX = (tileSize.Width - txt.Width) / 2; int textOffsetY = (tileSize.Height - txt.Height) / 2; int frameOffsetX = Math.Max(textOffsetX, -tileSize.Width) - 1; int frameOffsetY = Math.Max(textOffsetY, -tileSize.Height) - 1; int frameWidth = Math.Min(txt.Width + 2, tileSize.Width * 3); int frameHeight = Math.Min(txt.Height + 2, tileSize.Height * 3); Rectangle frameRect = new Rectangle(frameOffsetX, frameOffsetY, frameWidth, frameHeight); Rectangle textRect = new Rectangle(textOffsetX, textOffsetY, txt.Width, txt.Height); bmgr.FillRectangle(baseBackgroundBrush, frameRect); bmgr.DrawImage(txt, textRect, 0, 0, txt.Width, txt.Height, GraphicsUnit.Pixel); } } Globals.TheShapeCacheManager.AddImage(wpId, wpBm); Rectangle paintRect = new Rectangle(paintBounds.Location.X, paintBounds.Location.Y, wpBm.Width, wpBm.Height); graphics.DrawImage(wpBm, paintRect, 0, 0, wpBm.Width, wpBm.Height, GraphicsUnit.Pixel, imageAttributes); } } } } public static void RenderAllBuildingEffectRadiuses(Graphics graphics, Map map, Rectangle visibleCells, Size tileSize, int effectRadius, Building selected) { foreach ((Point topLeft, Building building) in map.Buildings.OfType() .Where(b => (b.Occupier.Type.Flag & BuildingTypeFlag.GapGenerator) != BuildingTypeFlag.None)) { RenderBuildingEffectRadius(graphics, visibleCells, tileSize, effectRadius, building, topLeft, selected); } } public static void RenderBuildingEffectRadius(Graphics graphics, Rectangle visibleCells, Size tileSize, int effectRadius, Building building, Point topLeft, Building selected) { if ((building.Type.Flag & BuildingTypeFlag.GapGenerator) != BuildingTypeFlag.GapGenerator) { return; } ITeamColor tc = Globals.TheTeamColorManager[building.House?.BuildingTeamColor]; Color circleColor = Globals.TheTeamColorManager.GetBaseColor(tc?.Name); bool[,] cells = building.Type.BaseOccupyMask; int maskY = cells.GetLength(0); int maskX = cells.GetLength(1); Rectangle circleCellBounds = GeneralUtils.GetBoxFromCenterCell(topLeft, maskX, maskY, effectRadius, effectRadius, new Size(1,1), out _); Rectangle circleBounds = GeneralUtils.GetBoxFromCenterCell(topLeft, maskX, maskY, effectRadius, effectRadius, tileSize, out Point center); if (visibleCells.IntersectsWith(circleCellBounds)) { float alphaFactor = 1.0f; if (building.IsPreview) { alphaFactor *= Globals.PreviewAlphaFloat; } if (!building.IsPrebuilt) { alphaFactor *= Globals.UnbuiltAlphaFloat; } int alphaFactorInt = (int)Math.Round(alphaFactor * 256).Restrict(0, 255); Color alphacorr = Color.FromArgb(alphaFactorInt, circleColor); RenderCircleDiagonals(graphics, tileSize, alphacorr, effectRadius, effectRadius, center); DrawDashesCircle(graphics, circleBounds, tileSize, alphacorr, true, -1.25f, 2.5f); } } public static void RenderAllUnitEffectRadiuses(Graphics graphics, Map map, Rectangle visibleCells, Size tileSize, int jamRadius, Unit selected) { foreach ((Point topLeft, Unit unit) in map.Technos.OfType() .Where(b => (b.Occupier.Type.Flag & (UnitTypeFlag.IsGapGenerator | UnitTypeFlag.IsJammer)) != UnitTypeFlag.None)) { RenderUnitEffectRadius(graphics, tileSize, jamRadius, unit, topLeft, visibleCells, selected); } } public static void RenderUnitEffectRadius(Graphics graphics, Size tileSize, int jamRadius, Unit unit, Point cell, Rectangle visibleCells, Unit selected) { bool isJammer = unit.Type.Flag.HasFlag(UnitTypeFlag.IsJammer); bool isGapGen = unit.Type.Flag.HasFlag(UnitTypeFlag.IsGapGenerator); if (!isJammer && !isGapGen) { return; } ITeamColor tc = Globals.TheTeamColorManager[unit.House?.BuildingTeamColor]; Color circleColor = Globals.TheTeamColorManager.GetBaseColor(tc?.Name); float alphaFactor = 1.0f; if (unit.IsPreview) { alphaFactor *= Globals.PreviewAlphaFloat; } int alphaFactorInt = (int)Math.Round(alphaFactor * 256).Restrict(0, 255); Color alphacorr = Color.FromArgb(alphaFactorInt, circleColor); if (isJammer) { // uses map's Gap Generator range. Rectangle circleCellBounds = GeneralUtils.GetBoxFromCenterCell(cell, 1, 1, jamRadius, jamRadius, new Size(1, 1), out _); Rectangle circleBounds = GeneralUtils.GetBoxFromCenterCell(cell, 1, 1, jamRadius, jamRadius, tileSize, out Point center); if (visibleCells.IntersectsWith(circleCellBounds)) { RenderCircleDiagonals(graphics, tileSize, alphacorr, jamRadius, jamRadius, center); DrawDashesCircle(graphics, circleBounds, tileSize, alphacorr, true, -1.25f, 2.5f); } } if (isGapGen) { // uses specific 5x7 circle around the unit cell int radiusX = 2; int radiusY = 3; Rectangle circleCellBounds = GeneralUtils.GetBoxFromCenterCell(cell, 1, 1, radiusX, radiusY, new Size(1, 1), out _); Rectangle circleBounds = GeneralUtils.GetBoxFromCenterCell(cell, 1, 1, radiusX, radiusY, tileSize, out Point center); if (visibleCells.IntersectsWith(circleCellBounds)) { RenderCircleDiagonals(graphics, tileSize, alphacorr, radiusX, radiusY, center); DrawDashesCircle(graphics, circleBounds, tileSize, alphacorr, true, -1.25f, 2.5f); } } } private static void RenderCircleDiagonals(Graphics graphics, Size tileSize, Color paintColor, double radiusX, double radiusY, Point center) { float penSize = Math.Max(1.0f, tileSize.Width / 16.0f); using (Pen linePen = new Pen(paintColor, penSize)) { linePen.DashPattern = new float[] { 1.0F, 4.0F, 6.0F, 4.0F }; linePen.DashCap = DashCap.Round; int diamX = (int)Math.Round((radiusX * 2 + 1) * tileSize.Width); int radX = diamX / 2; int diamY = (int)Math.Round((radiusY * 2 + 1) * tileSize.Height); int radY = diamY / 2; double sinDistance = Math.Sin(Math.PI * 45 / 180.0); int sinX = (int)Math.Round(radX * sinDistance); int sinY = (int)Math.Round(radY * sinDistance); graphics.DrawLine(linePen, center, new Point(center.X, center.Y - radY)); graphics.DrawLine(linePen, center, new Point(center.X - sinX, center.Y + sinY)); graphics.DrawLine(linePen, center, new Point(center.X + radX, center.Y)); graphics.DrawLine(linePen, center, new Point(center.X + sinX, center.Y + sinX)); graphics.DrawLine(linePen, center, new Point(center.X, center.Y + radY)); graphics.DrawLine(linePen, center, new Point(center.X + sinX, center.Y - sinY)); graphics.DrawLine(linePen, center, new Point(center.X - radX, center.Y)); graphics.DrawLine(linePen, center, new Point(center.X - sinX, center.Y - sinY)); } } public static void RenderAllWayPointRevealRadiuses(Graphics graphics, IGamePlugin plugin, Map map, Rectangle visibleCells, Size tileSize, Waypoint selectedItem) { RenderAllWayPointRevealRadiuses(graphics, plugin, map, visibleCells, tileSize, selectedItem, false); } public static void RenderAllWayPointRevealRadiuses(Graphics graphics, IGamePlugin plugin, Map map, Rectangle visibleCells, Size tileSize, Waypoint selectedItem, bool onlySelected) { int[] wpReveal1 = plugin.GetRevealRadiusForWaypoints(false); int[] wpReveal2 = plugin.GetRevealRadiusForWaypoints(true); Waypoint[] allWaypoints = map.Waypoints; for (int i = 0; i < allWaypoints.Length; i++) { Waypoint cur = allWaypoints[i]; bool isSelected = selectedItem != null && selectedItem == cur; if (onlySelected && !isSelected) { continue; } Point? p = cur?.Point; if (p.HasValue) { Color drawColor = isSelected ? Color.Yellow : Color.Orange; if (wpReveal1[i] != 0) { RenderWayPointRevealRadius(graphics, map.Metrics, visibleCells, tileSize, drawColor, isSelected, false, wpReveal1[i], cur); } if (wpReveal2[i] != 0) { RenderWayPointRevealRadius(graphics, map.Metrics, visibleCells, tileSize, drawColor, isSelected, false, wpReveal2[i], cur); } } } } public static void RenderWayPointRevealRadius(Graphics graphics, CellMetrics metrics, Rectangle visibleCells, Size tileSize, Color circleColor, bool isSelected, bool forPreview, double revealRadius, Waypoint waypoint) { if (waypoint.Cell.HasValue && metrics.GetLocation(waypoint.Cell.Value, out Point cellPoint)) { double diam = revealRadius * 2 + 1; Rectangle circleCellBounds = new Rectangle( (int)Math.Round(cellPoint.X - revealRadius), (int)Math.Round(cellPoint.Y - revealRadius), (int)Math.Round(diam), (int)Math.Round(diam)); Rectangle circleBounds = new Rectangle( (int)Math.Round(cellPoint.X * tileSize.Width - revealRadius * tileSize.Width), (int)Math.Round(cellPoint.Y * tileSize.Width - revealRadius * tileSize.Height), (int)Math.Round(diam * tileSize.Width), (int)Math.Round(diam * tileSize.Height)); if (visibleCells.IntersectsWith(circleCellBounds)) { DrawDashesCircle(graphics, circleBounds, tileSize, Color.FromArgb(isSelected && !forPreview ? 255 : 128, circleColor), isSelected, 1.25f, 2.5f); } } } public static void DrawCircle(Graphics graphics, Rectangle circleBounds, Size tileSize, Color circleColor, bool thickborder) { float penSize = Math.Max(1f, tileSize.Width / (thickborder ? 16.0f : 32.0f)); using (Pen circlePen = new Pen(circleColor, penSize)) { graphics.DrawEllipse(circlePen, circleBounds); } } public static void DrawDashesCircle(Graphics graphics, Rectangle circleBounds, Size tileSize, Color circleColor, bool thickborder, float startAngle, float drawAngle) { float penSize = Math.Max(1f, tileSize.Width / (thickborder ? 16.0f : 32.0f)); using (Pen circlePen = new Pen(circleColor, penSize)) { drawAngle = Math.Abs(drawAngle); float endPoint = 360f + startAngle - drawAngle; for (float i = startAngle; i <= endPoint; i += drawAngle * 2) { graphics.DrawArc(circlePen, circleBounds, i, drawAngle); } } } public static void RenderCellTriggersSoft(Graphics graphics, GameInfo gameInfo, Map map, Rectangle visibleCells, Size tileSize, params string[] specifiedToExclude) { RenderCellTriggers(graphics, gameInfo, map, visibleCells, tileSize, Color.Black, Color.Silver, Color.White, 0.75f, false, true, specifiedToExclude); } public static void RenderCellTriggersHard(Graphics graphics, GameInfo gameInfo, Map map, Rectangle visibleCells, Size tileSize, params string[] specifiedToExclude) { RenderCellTriggers(graphics, gameInfo, map, visibleCells, tileSize, Color.Black, Color.Silver, Color.White, 1, false, true, specifiedToExclude); } public static void RenderCellTriggersSelected(Graphics graphics, GameInfo gameInfo, Map map, Rectangle visibleCells, Size tileSize, params string[] specifiedToDraw) { RenderCellTriggers(graphics, gameInfo, map, visibleCells, tileSize, Color.Black, Color.FromArgb(0xFF, 0xC0, 0xC0, 0x00), Color.Yellow, 1, true, false, specifiedToDraw); } public static void RenderCellTriggers(Graphics graphics, GameInfo gameInfo, Map map, Rectangle visibleCells, Size tileSize, Color fillColor, Color borderColor, Color textColor, double alphaAdjust, bool thickborder, bool excludeSpecified, params string[] specified) { string classicFont = null; bool cropClassicFont = false; TeamRemap remapClassicFont = null; if (Globals.TheTilesetManager is TilesetManagerClassic tsmc && Globals.TheTeamColorManager is TeamRemapManager trm) { classicFont = gameInfo.GetClassicFontInfo(ClassicFont.CellTriggers, tsmc, trm, textColor, out cropClassicFont, out remapClassicFont); } // For bounds, add one more cell to get all borders showing. Rectangle boundRenderCells = visibleCells; boundRenderCells.Inflate(1, 1); boundRenderCells.Intersect(map.Metrics.Bounds); HashSet specifiedSet = new HashSet(specified, StringComparer.OrdinalIgnoreCase); List<(Point p, CellTrigger cellTrigger)> toRender = new List<(Point p, CellTrigger cellTrigger)>(); HashSet toRenderSet = new HashSet(); List<(Point p, CellTrigger cellTrigger)> boundsToDraw = new List<(Point p, CellTrigger cellTrigger)>(); foreach ((int cell, CellTrigger cellTrigger) in map.CellTriggers.OrderBy(c => c.Cell)) { int x = cell % map.Metrics.Width; int y = cell / map.Metrics.Width; if (!boundRenderCells.Contains(x, y)) { continue; } bool contains = specifiedSet.Contains(cellTrigger.Trigger); if (contains && excludeSpecified || !contains && !excludeSpecified) { continue; } // Allow better alpha control by detecting previews but not using that alpha. bool isPreview = cellTrigger.IsPreview; Point p = new Point(x, y); if (visibleCells.Contains(x, y)) { toRender.Add((p, cellTrigger)); toRenderSet.Add(cellTrigger.Trigger + "=" + (isPreview ? 'P' : 'N')); } boundsToDraw.Add((p, cellTrigger)); } if (boundsToDraw.Count == 0) { return; } Color ApplyAlpha(Color col, int baseAlpha, double alphaMul) { return Color.FromArgb(Math.Max(0, Math.Min(0xFF, (int)Math.Round(baseAlpha * alphaMul, MidpointRounding.AwayFromZero))), col); }; int sizeW = tileSize.Width; int sizeH = tileSize.Height; // Actual balance is fixed; border is 1, text is 1/2, background is 3/8. The original alpha inside the given colors is ignored. // Should probably rewrite this to paint text as opaque on 6/8 alpha background, then paint that as 50% alpha and add solid border, // and then paint that with the final adjusted alpha factor. fillColor = ApplyAlpha(fillColor, 0x60, alphaAdjust); borderColor = ApplyAlpha(borderColor, 0xFF, alphaAdjust); textColor = ApplyAlpha(textColor, 0x80, alphaAdjust); // for classic fonts Color adjustColor = ApplyAlpha(Color.White, 0x80, alphaAdjust); // Preview versions Color previewFillColor = ApplyAlpha(fillColor, 0x60, alphaAdjust / 2); Color previewBorderColor = ApplyAlpha(borderColor, 0xFF, alphaAdjust / 2); Color previewTextColor = ApplyAlpha(textColor, 0x80, alphaAdjust / 2); // for classic fonts Color previewAdjustColor = ApplyAlpha(Color.White, 0x80, alphaAdjust / 2); // Render each trigger once, and just paint the rendered image multiple times. Dictionary backRenders = new Dictionary(); Dictionary renders = new Dictionary(); double tileScaleHor = sizeW / 128.0; Rectangle tileBounds = new Rectangle(0, 0, sizeW, sizeH); using (SolidBrush prevCellTriggersBackgroundBrush = new SolidBrush(previewFillColor)) using (SolidBrush prevCellTriggersBrush = new SolidBrush(previewTextColor)) using (SolidBrush cellTriggersBackgroundBrush = new SolidBrush(fillColor)) using (SolidBrush cellTriggersBrush = new SolidBrush(textColor)) { foreach (string trigger in toRenderSet) { string[] trigPart = trigger.Split('='); if (trigPart.Length != 2) { continue; } string text = trigPart[0]; bool isPreview = trigPart[1] == "P"; Color textCol = isPreview ? previewTextColor : textColor; Rectangle textBounds = new Rectangle(Point.Empty, tileBounds.Size); string trId = "trigger_" + trigger + "_" + ((uint)(isPreview ? previewTextColor : textColor).ToArgb()).ToString("X4"); string bgId = "trig_bg_" + trigger + "_" + ((uint)(isPreview ? previewFillColor : fillColor).ToArgb()).ToString("X4"); Bitmap trigbm = Globals.TheShapeCacheManager.GetImage(trId); if (trigbm == null) { trigbm = new Bitmap(sizeW, sizeH); using (Graphics trigctg = Graphics.FromImage(trigbm)) { SetRenderSettings(trigctg, classicFont == null); StringFormat stringFormat = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; using (Bitmap textBm = new Bitmap(sizeW, sizeH)) { using (Graphics textGr = Graphics.FromImage(textBm)) { if (classicFont == null) { using (Font font = trigctg.GetAdjustedFont(text, SystemFonts.DefaultFont, textBounds.Width, textBounds.Height, Math.Max(1, (int)Math.Round(24 * tileScaleHor)), Math.Max(1, (int)Math.Round(48 * tileScaleHor)), stringFormat, true)) { SetRenderSettings(trigctg, true); // If not set, the text will have ugly black fades at the edges. textGr.TextRenderingHint = TextRenderingHint.SingleBitPerPixel; textGr.DrawString(text, font, isPreview ? prevCellTriggersBrush : cellTriggersBrush, textBounds, stringFormat); } } else { int[] indices = Encoding.ASCII.GetBytes(text).Select(x => (int)x).ToArray(); using (ImageAttributes imageAttributes = new ImageAttributes()) { imageAttributes.SetColorMatrix(GetColorMatrix(isPreview ? previewAdjustColor : adjustColor, 1.0f, 1.0f)); using (Bitmap txt = RenderTextFromSprite(classicFont, remapClassicFont, tileBounds.Size, indices, false, cropClassicFont)) { Rectangle paintBounds = new Rectangle(Point.Empty, tileSize); textGr.DrawImage(txt, textBounds, 0, 0, tileBounds.Width, tileBounds.Height, GraphicsUnit.Pixel, imageAttributes); } } } } trigctg.DrawImage(textBm, 0, 0); } } Globals.TheShapeCacheManager.AddImage(trId, trigbm); } Bitmap fillbm = Globals.TheShapeCacheManager.GetImage(bgId); if (fillbm == null) { fillbm = new Bitmap(sizeW, sizeH); using (Graphics fillctg = Graphics.FromImage(fillbm)) { SetRenderSettings(fillctg, classicFont == null); fillctg.FillRectangle(isPreview ? prevCellTriggersBackgroundBrush : cellTriggersBackgroundBrush, textBounds); // Clear background under text to make it more transparent. There are probably more elegant ways to do this, but this works. RegionData textInline = ImageUtils.GetOutline(new Size(sizeW, sizeH), trigbm, 0.00f, (byte)Math.Max(0, textCol.A - 1), false); using (Region clearArea = new Region(textInline)) using (Brush clear = new SolidBrush(Color.Transparent)) { fillctg.CompositingMode = CompositingMode.SourceCopy; fillctg.FillRegion(clear, clearArea); fillctg.CompositingMode = CompositingMode.SourceOver; } } Globals.TheShapeCacheManager.AddImage(bgId, fillbm); } backRenders.Add(trigger, fillbm); renders.Add(trigger, trigbm); } } var backupCompositingQuality = graphics.CompositingQuality; var backupInterpolationMode = graphics.InterpolationMode; var backupSmoothingMode = graphics.SmoothingMode; var backupPixelOffsetMode = graphics.PixelOffsetMode; SetRenderSettings(graphics, classicFont == null); foreach ((Point p, CellTrigger cellTrigger) in toRender) { bool isPreview = cellTrigger.IsPreview; string requestName = cellTrigger.Trigger + "=" + (isPreview ? 'P' : 'N'); if (backRenders.TryGetValue(requestName, out Bitmap fillCtBm)) { graphics.DrawImage(fillCtBm, p.X * tileSize.Width, p.Y * tileSize.Height); } } float borderSize = Math.Max(0.5f, tileSize.Width / 60.0f); float thickBorderSize = Math.Max(1f, tileSize.Width / 20.0f); using (Pen prevBorderPen = new Pen(previewBorderColor, thickborder ? thickBorderSize : borderSize)) using (Pen borderPen = new Pen(borderColor, thickborder ? thickBorderSize : borderSize)) { foreach ((Point p, CellTrigger cellTrigger) in boundsToDraw) { bool isPreview = cellTrigger.IsPreview; Rectangle bounds = new Rectangle(new Point(p.X * tileSize.Width, p.Y * tileSize.Height), tileSize); graphics.DrawRectangle(isPreview ? prevBorderPen : borderPen, bounds); } } foreach ((Point p, CellTrigger cellTrigger) in toRender) { bool isPreview = cellTrigger.IsPreview; string requestName = cellTrigger.Trigger + "=" + (isPreview ? 'P' : 'N'); if (renders.TryGetValue(requestName, out Bitmap ctBm)) { graphics.DrawImage(ctBm, p.X * tileSize.Width, p.Y * tileSize.Height); } } graphics.CompositingQuality = backupCompositingQuality; graphics.InterpolationMode = backupInterpolationMode; graphics.SmoothingMode = backupSmoothingMode; graphics.PixelOffsetMode = backupPixelOffsetMode; } public static void RenderMapBoundaries(Graphics graphics, Map map, Rectangle visibleCells, Size tileSize) { RenderMapBoundaries(graphics, map.Bounds, visibleCells, tileSize, Color.Cyan); } public static void RenderMapBoundaries(Graphics graphics, Rectangle bounds, Rectangle visibleCells, Size tileSize, Color color) { // Inflate so you'd see the indicator if it's at the edge. visibleCells.Inflate(1, 1); Rectangle cropped = bounds; cropped.Intersect(visibleCells); // If these two are identical, that means all map borders are at least one cell outside the visible cells area. if (visibleCells == cropped) { return; } Rectangle boundsRect = Rectangle.FromLTRB( bounds.Left * tileSize.Width, bounds.Top * tileSize.Height, bounds.Right * tileSize.Width, bounds.Bottom * tileSize.Height ); using (Pen boundsPen = new Pen(color, Math.Max(1f, tileSize.Width / 8.0f))) { graphics.DrawRectangle(boundsPen, boundsRect); } } public static void RenderMapSymmetry(Graphics graphics, Rectangle bounds, Size tileSize, Color color) { Rectangle boundsRect = Rectangle.FromLTRB( bounds.Left * tileSize.Width, bounds.Top * tileSize.Height, bounds.Right * tileSize.Width, bounds.Bottom * tileSize.Height ); using (Pen boundsPen = new Pen(color, Math.Max(1f, tileSize.Width / 8.0f))) { graphics.DrawLine(boundsPen, new Point(boundsRect.X, boundsRect.Y), new Point(boundsRect.Right, boundsRect.Bottom)); graphics.DrawLine(boundsPen, new Point(boundsRect.Right, boundsRect.Y), new Point(boundsRect.X, boundsRect.Bottom)); int halfX = boundsRect.X + boundsRect.Width / 2; int halfY = boundsRect.Y + boundsRect.Height / 2; graphics.DrawLine(boundsPen, new Point(halfX, boundsRect.Y), new Point(halfX, boundsRect.Bottom)); graphics.DrawLine(boundsPen, new Point(boundsRect.X, halfY), new Point(boundsRect.Right, halfY)); } } public static void RenderMapGrid(Graphics graphics, Rectangle renderBounds, Rectangle mapBounds, bool mapBoundsRendered, Size tileSize, Color color) { Rectangle boundvisibleCells = mapBounds; boundvisibleCells.Intersect(renderBounds); int startY = boundvisibleCells.Y; int startX = boundvisibleCells.X; int endRight = boundvisibleCells.Right; int endBottom = boundvisibleCells.Bottom; if (mapBoundsRendered) { if (boundvisibleCells.Y == mapBounds.Y) { startY++; } if (boundvisibleCells.Bottom == mapBounds.Bottom) { endBottom--; } if (boundvisibleCells.X == mapBounds.X) { startX++; } if (boundvisibleCells.Right == mapBounds.Right) { endRight--; } } using (Pen gridPen = new Pen(color, Math.Max(1f, tileSize.Width / 16.0f))) { int leftBound = boundvisibleCells.X * tileSize.Width; int rightBound = boundvisibleCells.Right * tileSize.Width; for (int y = startY; y <= endBottom; ++y) { int ymul = y * tileSize.Height; graphics.DrawLine(gridPen, new Point(leftBound, ymul), new Point(rightBound, ymul)); } //*/ int topBound = boundvisibleCells.Y * tileSize.Height; int bottomBound = boundvisibleCells.Bottom * tileSize.Height; for (int x = startX; x <= endRight; ++x) { int xmul = x * tileSize.Height; graphics.DrawLine(gridPen, new Point(xmul, topBound), new Point(xmul, bottomBound)); } //*/ } } /// /// Paints the passability grid onto the map. /// /// Graphics to paint on. /// Game plugin /// The map data itself /// If given, draws a green grid on the locations of the technos in the given set. /// If given, draws a green grid on the locations of the buildings in the given set. /// Tile size /// If given, only cells in the given area are marked. /// Cells to completely ignore during the drawing operation. /// Indicates this is painted for placement preview purposes, meaning colours with their alpha set to 0 are restored and also handled. /// True to paint the hashing with only 25% alpha instead of the usual 50%. public static void RenderHashAreas(Graphics graphics, IGamePlugin plugin, CellGrid