This rule recommends use of primary constructors. Apply the suggestions on simple POCOs, adjusting some to readonly/structs/records at the same time. For more complex classes, the use of primary constructors is more distracting than helpful, so silence the rule. IDEs can still offer fixes for using primary constructors, but it will not show up as a build issue.
462 lines
12 KiB
C#
462 lines
12 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright (c) The OpenRA Developers and Contributors
|
|
* This file is part of OpenRA, which is free software. It is made
|
|
* available to you under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation, either version 3 of
|
|
* the License, or (at your option) any later version. For more
|
|
* information, see COPYING.
|
|
*/
|
|
#endregion
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Newtonsoft.Json;
|
|
using OpenRA.Graphics;
|
|
using OpenRA.Mods.Common.Graphics;
|
|
using OpenRA.Traits;
|
|
using Color = OpenRA.Primitives.Color;
|
|
|
|
namespace OpenRA.Mods.Common.Traits
|
|
{
|
|
[TraitLocation(SystemActors.EditorWorld)]
|
|
public class MarkerLayerOverlayInfo : TraitInfo
|
|
{
|
|
[Desc("A list of colors to be used for drawing.")]
|
|
public readonly Color[] Colors =
|
|
[
|
|
Color.FromArgb(255, 0, 0),
|
|
Color.FromArgb(255, 127, 0),
|
|
Color.FromArgb(255, 238, 70),
|
|
Color.FromArgb(0, 255, 33),
|
|
Color.FromArgb(0, 255, 255),
|
|
Color.FromArgb(0, 42, 255),
|
|
Color.FromArgb(165, 0, 255),
|
|
Color.FromArgb(255, 0, 220),
|
|
];
|
|
|
|
[Desc("Default alpha blend.")]
|
|
public readonly int Alpha = 85;
|
|
|
|
[Desc("Color of the axis angle display.")]
|
|
public readonly Color AxisAngleColor = Color.Crimson;
|
|
|
|
public override object Create(ActorInitializer init)
|
|
{
|
|
return new MarkerLayerOverlay(init.Self, this);
|
|
}
|
|
}
|
|
|
|
public class MarkerLayerOverlay : IRenderAnnotations, INotifyActorDisposing, IWorldLoaded
|
|
{
|
|
public class MarkerLayerFile
|
|
{
|
|
public Dictionary<int, List<int>> Tiles { get; set; }
|
|
public MarkerTileMirrorMode MirrorMode { get; set; }
|
|
public int NumSides { get; set; }
|
|
public int AxisAngle { get; set; }
|
|
public int TileAlpha { get; set; }
|
|
}
|
|
|
|
const double DegreesToRadians = Math.PI / 180;
|
|
|
|
readonly int[] validFlipModeSides = [2, 4];
|
|
|
|
public enum MarkerTileMirrorMode
|
|
{
|
|
None,
|
|
Flip,
|
|
Rotate
|
|
}
|
|
|
|
readonly World world;
|
|
readonly WPos mapCenter;
|
|
readonly Color[] alphaBlendColors;
|
|
|
|
public readonly CellLayer<int?> CellLayer;
|
|
public readonly Dictionary<int, HashSet<CPos>> Tiles = [];
|
|
|
|
public bool Enabled = true;
|
|
public MarkerTileMirrorMode MirrorMode { get; private set; } = MarkerTileMirrorMode.None;
|
|
public MarkerLayerOverlayInfo Info { get; }
|
|
public int NumSides = 2;
|
|
public int AxisAngle;
|
|
public int TileAlpha
|
|
{
|
|
get => tileAlpha;
|
|
set
|
|
{
|
|
tileAlpha = value;
|
|
UpdateTileAlpha();
|
|
}
|
|
}
|
|
|
|
int tileAlpha;
|
|
bool disposed;
|
|
|
|
public MarkerLayerOverlay(Actor self, MarkerLayerOverlayInfo info)
|
|
{
|
|
Info = info;
|
|
world = self.World;
|
|
var map = self.World.Map;
|
|
|
|
tileAlpha = info.Alpha;
|
|
alphaBlendColors = new Color[info.Colors.Length];
|
|
UpdateTileAlpha();
|
|
|
|
CellLayer = new CellLayer<int?>(map);
|
|
|
|
mapCenter = GetMapCenterWPos();
|
|
}
|
|
|
|
public void WorldLoaded(World w, WorldRenderer wr)
|
|
{
|
|
try
|
|
{
|
|
var modData = Game.ModData;
|
|
var mod = modData.Manifest.Metadata;
|
|
var directory = Path.Combine(Platform.SupportDir, "Editor", modData.Manifest.Id, mod.Version, "MarkerTiles");
|
|
if (!Directory.Exists(directory))
|
|
return;
|
|
|
|
if (string.IsNullOrWhiteSpace(world.Map.Package.Name))
|
|
return;
|
|
|
|
var markerTileFilename = $"{Path.GetFileNameWithoutExtension(world.Map.Package.Name)}.json";
|
|
var markerTilePath = Path.Combine(directory, markerTileFilename);
|
|
if (!File.Exists(markerTilePath))
|
|
return;
|
|
|
|
using (var streamReader = new StreamReader(markerTilePath))
|
|
{
|
|
var content = streamReader.ReadToEnd();
|
|
var file = JsonConvert.DeserializeObject<MarkerLayerFile>(content);
|
|
|
|
TileAlpha = file.TileAlpha;
|
|
MirrorMode = file.MirrorMode;
|
|
NumSides = file.NumSides;
|
|
AxisAngle = file.AxisAngle;
|
|
|
|
var savedTilesHashSetDictionary = file.Tiles.ToDictionary(x => x.Key, x => x.Value.Select(bits => new CPos(bits)).ToHashSet());
|
|
SetAll(savedTilesHashSetDictionary);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Write("debug", "Failed to load map editor marker tiles.");
|
|
Log.Write("debug", e);
|
|
}
|
|
}
|
|
|
|
public MarkerLayerFile ToFile()
|
|
{
|
|
var tilesBitsDictionary = Tiles.ToDictionary(x => x.Key, x => x.Value.Select(cpos => cpos.Bits).ToList());
|
|
return new MarkerLayerFile
|
|
{
|
|
Tiles = tilesBitsDictionary,
|
|
TileAlpha = TileAlpha,
|
|
MirrorMode = MirrorMode,
|
|
NumSides = NumSides,
|
|
AxisAngle = AxisAngle,
|
|
};
|
|
}
|
|
|
|
void UpdateTileAlpha()
|
|
{
|
|
for (var i = 0; i < Info.Colors.Length; i++)
|
|
alphaBlendColors[i] = Color.FromArgb(tileAlpha, Info.Colors[i]);
|
|
}
|
|
|
|
public void ClearSelected(int tileType)
|
|
{
|
|
if (Tiles.TryGetValue(tileType, out var set))
|
|
foreach (var pos in set)
|
|
SetTile(pos, null);
|
|
}
|
|
|
|
public void ClearAll()
|
|
{
|
|
foreach (var position in Tiles.SelectMany(x => x.Value))
|
|
CellLayer[position] = null;
|
|
|
|
Tiles.Clear();
|
|
}
|
|
|
|
public void SetAll(Dictionary<int, HashSet<CPos>> newTiles)
|
|
{
|
|
ClearAll();
|
|
|
|
foreach (var type in newTiles)
|
|
{
|
|
var set = new HashSet<CPos>();
|
|
Tiles.Add(type.Key, set);
|
|
|
|
foreach (var position in type.Value)
|
|
{
|
|
if (!world.Map.Contains(position))
|
|
continue;
|
|
|
|
set.Add(position);
|
|
CellLayer[position] = type.Key;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void SetSelected(int tile, HashSet<CPos> newTiles)
|
|
{
|
|
var type = Tiles[tile];
|
|
foreach (var pos in type)
|
|
SetTile(pos, null);
|
|
|
|
type.Clear();
|
|
|
|
foreach (var pos in newTiles)
|
|
{
|
|
type.Add(pos);
|
|
CellLayer[pos] = tile;
|
|
}
|
|
}
|
|
|
|
public void SetMirrorMode(MarkerTileMirrorMode mirrorMode)
|
|
{
|
|
MirrorMode = mirrorMode;
|
|
|
|
if (mirrorMode == MarkerTileMirrorMode.Flip && !validFlipModeSides.Contains(NumSides))
|
|
NumSides = validFlipModeSides[0];
|
|
}
|
|
|
|
WPos GetMapCenterWPos()
|
|
{
|
|
var map = world.Map;
|
|
|
|
var boundsWidth = map.AllCells.BottomRight.X - map.AllCells.TopLeft.X;
|
|
var boundsHeight = map.AllCells.BottomRight.Y - map.AllCells.TopLeft.Y;
|
|
|
|
var xIsOdd = boundsWidth % 2 != 0;
|
|
var yIsOdd = boundsHeight % 2 != 0;
|
|
|
|
var xCenter = boundsWidth / 2;
|
|
var yCenter = boundsHeight / 2;
|
|
|
|
var centerWpos = map.CenterOfCell(new CPos(xCenter, yCenter));
|
|
if (xIsOdd)
|
|
centerWpos += new WVec(512, 0, 0);
|
|
|
|
if (yIsOdd)
|
|
centerWpos += new WVec(0, 512, 0);
|
|
|
|
return centerWpos;
|
|
}
|
|
|
|
public CPos[] CalculateMirrorPositions(CPos cell)
|
|
{
|
|
const int DegreesInCircle = 360;
|
|
|
|
var map = world.Map;
|
|
|
|
var wpos = map.CenterOfCell(cell);
|
|
var wposVec = wpos - mapCenter;
|
|
var angle = DegreesInCircle / NumSides;
|
|
|
|
var targets = new List<CPos>();
|
|
|
|
if (map.Contains(cell))
|
|
targets.Add(cell);
|
|
|
|
if (MirrorMode == MarkerTileMirrorMode.Flip)
|
|
{
|
|
var startAxis = new WVec(1024, 0, 0);
|
|
var axes = new List<WVec>();
|
|
for (var i = 0; i < NumSides / 2; i++)
|
|
{
|
|
var targetAngle = (i * angle + AxisAngle) * DegreesToRadians;
|
|
var point = new WVec((int)(startAxis.X * Math.Cos(targetAngle) - startAxis.Y * Math.Sin(targetAngle)),
|
|
(int)(startAxis.X * Math.Sin(targetAngle) + startAxis.Y * Math.Cos(targetAngle)),
|
|
wpos.Z);
|
|
|
|
axes.Add(point);
|
|
}
|
|
|
|
foreach (var axis in axes)
|
|
{
|
|
var point = GetAxisMirrorPoint(mapCenter, axis, wpos);
|
|
var cellPoint = map.CellContaining(point);
|
|
|
|
if (map.Contains(cellPoint))
|
|
targets.Add(cellPoint);
|
|
}
|
|
|
|
// Mirror twice for both
|
|
if (axes.Count == 2)
|
|
{
|
|
var point = GetAxisMirrorPoint(mapCenter, axes[0], wpos);
|
|
point = GetAxisMirrorPoint(mapCenter, axes[1], point);
|
|
var cellPoint = map.CellContaining(point);
|
|
|
|
if (map.Contains(cellPoint))
|
|
targets.Add(cellPoint);
|
|
}
|
|
|
|
///////////////
|
|
|
|
static WPos GetAxisMirrorPoint(WPos center, WVec axis, WPos point)
|
|
{
|
|
var testPoint = center - new WVec(point.X, point.Y, 0);
|
|
var a = axis.Y;
|
|
var b = -axis.X;
|
|
var c = -a * 0 - b * 0;
|
|
|
|
var m = Math.Sqrt(a * a + b * b);
|
|
var aDash = a / m;
|
|
var bDash = b / m;
|
|
var cDash = c / m;
|
|
|
|
var d = aDash * testPoint.X + bDash * testPoint.Y + cDash;
|
|
var pxDash = testPoint.X - 2 * aDash * d;
|
|
var pyDash = testPoint.Y - 2 * bDash * d;
|
|
|
|
return new WPos((int)pxDash + center.X, (int)pyDash + center.Y, 0);
|
|
}
|
|
}
|
|
else if (MirrorMode == MarkerTileMirrorMode.Rotate)
|
|
{
|
|
// Rotate
|
|
var flipAngleRadians = DegreesToRadians * angle;
|
|
|
|
var sidesAreEven = NumSides % 2 == 0;
|
|
var oddSideStartIndex = (int)Math.Floor((double)NumSides / 2);
|
|
var startIndex = sidesAreEven ? 0 : -oddSideStartIndex;
|
|
var count = sidesAreEven ? NumSides : oddSideStartIndex + 1;
|
|
|
|
for (var i = startIndex; i < count; i++)
|
|
{
|
|
var targetAngle = i * flipAngleRadians;
|
|
var point = new WPos((int)(wposVec.X * Math.Cos(targetAngle) - wposVec.Y * Math.Sin(targetAngle)),
|
|
(int)(wposVec.X * Math.Sin(targetAngle) + wposVec.Y * Math.Cos(targetAngle)),
|
|
wpos.Z);
|
|
|
|
var cellPoint = map.CellContaining(point + new WVec(mapCenter.X, mapCenter.Y, 0));
|
|
|
|
if (map.Contains(cellPoint))
|
|
targets.Add(cellPoint);
|
|
}
|
|
}
|
|
|
|
return targets.ToArray();
|
|
}
|
|
|
|
public void SetTile(CPos target, int? tileType)
|
|
{
|
|
if (!world.Map.Contains(target))
|
|
return;
|
|
|
|
// Maintain map of tile types for selective clearing
|
|
var prevTile = CellLayer[target];
|
|
if (prevTile.HasValue && Tiles.TryGetValue(prevTile.Value, out var set))
|
|
set.Remove(target);
|
|
|
|
if (tileType.HasValue)
|
|
{
|
|
if (Tiles.TryGetValue(tileType.Value, out set))
|
|
set.Add(target);
|
|
else
|
|
Tiles.Add(tileType.Value, [target]);
|
|
|
|
CellLayer[target] = tileType;
|
|
}
|
|
else
|
|
CellLayer[target] = null;
|
|
}
|
|
|
|
void INotifyActorDisposing.Disposing(Actor self)
|
|
{
|
|
if (disposed)
|
|
return;
|
|
|
|
disposed = true;
|
|
}
|
|
|
|
readonly record struct MapLine(float2 Start, float2 End);
|
|
|
|
IEnumerable<IRenderable> IRenderAnnotations.RenderAnnotations(Actor self, WorldRenderer wr)
|
|
{
|
|
if (!Enabled)
|
|
yield break;
|
|
|
|
foreach (var cellPair in Tiles)
|
|
foreach (var cellPos in cellPair.Value)
|
|
yield return new MarkerTileRenderable(cellPos, alphaBlendColors[cellPair.Key]);
|
|
|
|
if (MirrorMode != MarkerTileMirrorMode.Flip)
|
|
yield break;
|
|
|
|
const int LineWidth = 1;
|
|
|
|
var color = Info.AxisAngleColor;
|
|
var targetAngle = AxisAngle * DegreesToRadians;
|
|
|
|
var mapCenterFloat = new float2(mapCenter.X, mapCenter.Y);
|
|
var mapBoundsWorldSize = mapCenterFloat * 2;
|
|
|
|
// Create our axis lines
|
|
var horizontalVec = new float2(1, 0);
|
|
var verticalVec = new float2(0, 1);
|
|
var edges = new[]
|
|
{
|
|
new MapLine(mapCenterFloat, mapCenterFloat + horizontalVec),
|
|
new MapLine(mapCenterFloat, mapCenterFloat + verticalVec),
|
|
};
|
|
|
|
var sourceAxes = new[] { verticalVec, -verticalVec, horizontalVec, -horizontalVec };
|
|
for (var i = 0; i < NumSides; i++)
|
|
{
|
|
var isOpposite = i % 2 != 0;
|
|
var sourceAxis = sourceAxes[i];
|
|
var rotatedAxis = new float2(
|
|
(float)(sourceAxis.X * Math.Cos(targetAngle) - sourceAxis.Y * Math.Sin(targetAngle)),
|
|
(float)(sourceAxis.X * Math.Sin(targetAngle) + sourceAxis.Y * Math.Cos(targetAngle)));
|
|
|
|
var axisLine = new MapLine(float2.Zero, rotatedAxis);
|
|
var collisionPoints = FindEdgeCollisionPoints(edges, axisLine);
|
|
|
|
var closestCollisionPoint = collisionPoints.OrderBy(x => x.LengthSquared).First();
|
|
if (isOpposite)
|
|
closestCollisionPoint *= -1;
|
|
|
|
var resultPos = new WVec((int)closestCollisionPoint.X, (int)closestCollisionPoint.Y, 0);
|
|
yield return new LineAnnotationRenderable(mapCenter, mapCenter + resultPos, LineWidth, color, color);
|
|
}
|
|
}
|
|
|
|
static float2[] FindEdgeCollisionPoints(MapLine[] mapEdges, MapLine axis)
|
|
{
|
|
var collisionResults = new List<float2>();
|
|
foreach (var mapEdge in mapEdges)
|
|
if (FindIntersection(axis.Start, axis.End, mapEdge.Start, mapEdge.End, out var collisionVec))
|
|
collisionResults.Add(collisionVec);
|
|
|
|
return collisionResults.ToArray();
|
|
}
|
|
|
|
static bool FindIntersection(float2 a1, float2 a2, float2 b1, float2 b2, out float2 result)
|
|
{
|
|
result = float2.Zero;
|
|
var d = (a1.X - a2.X) * (b1.Y - b2.Y) - (a1.Y - a2.Y) * (b1.X - b2.X);
|
|
|
|
// check if lines are parallel
|
|
if (d == 0)
|
|
return false;
|
|
|
|
var px = (a1.X * a2.Y - a1.Y * a2.X) * (b1.X - b2.X) - (a1.X - a2.X) * (b1.X * b2.Y - b1.Y * b2.X);
|
|
var py = (a1.X * a2.Y - a1.Y * a2.X) * (b1.Y - b2.Y) - (a1.Y - a2.Y) * (b1.X * b2.Y - b1.Y * b2.X);
|
|
|
|
result = new float2(px, py) / d;
|
|
return true;
|
|
}
|
|
|
|
bool IRenderAnnotations.SpatiallyPartitionable => false;
|
|
}
|
|
}
|