Files
OpenRA/OpenRA.Mods.Common/Traits/World/ShroudRenderer.cs
RoosterDragon 36660b89e9 Silence IDE0290.
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.
2025-04-02 10:18:32 +03:00

390 lines
14 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 OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
{
[TraitLocation(SystemActors.World | SystemActors.EditorWorld)]
public class ShroudRendererInfo : TraitInfo
{
public readonly string Sequence = "shroud";
[SequenceReference(nameof(Sequence))]
public readonly string[] ShroudVariants = ["shroud"];
[SequenceReference(nameof(Sequence))]
public readonly string[] FogVariants = ["fog"];
[PaletteReference]
public readonly string ShroudPalette = "shroud";
[PaletteReference]
public readonly string FogPalette = "fog";
[Desc("Bitfield of shroud directions for each frame. Lower four bits are",
"corners clockwise from TL; upper four are edges clockwise from top")]
public readonly int[] Index = [12, 9, 8, 3, 1, 6, 4, 2, 13, 11, 7, 14];
[Desc("Use the upper four bits when calculating frame")]
public readonly bool UseExtendedIndex = false;
[SequenceReference(nameof(Sequence))]
[Desc("Override for source art that doesn't define a fully shrouded tile")]
public readonly string OverrideFullShroud = null;
public readonly int OverrideShroudIndex = 15;
[SequenceReference(nameof(Sequence))]
[Desc("Override for source art that doesn't define a fully fogged tile")]
public readonly string OverrideFullFog = null;
public readonly int OverrideFogIndex = 15;
public readonly BlendMode ShroudBlend = BlendMode.Alpha;
public override object Create(ActorInitializer init) { return new ShroudRenderer(init.World, this); }
}
public sealed class ShroudRenderer : IRenderShroud, IWorldLoaded, INotifyActorDisposing
{
[Flags]
enum Edges : byte
{
None = 0,
TopLeft = 0x01,
TopRight = 0x02,
BottomRight = 0x04,
BottomLeft = 0x08,
AllCorners = TopLeft | TopRight | BottomRight | BottomLeft,
TopSide = 0x10,
RightSide = 0x20,
BottomSide = 0x40,
LeftSide = 0x80,
AllSides = TopSide | RightSide | BottomSide | LeftSide,
Top = TopSide | TopLeft | TopRight,
Right = RightSide | TopRight | BottomRight,
Bottom = BottomSide | BottomRight | BottomLeft,
Left = LeftSide | TopLeft | BottomLeft,
All = Top | Right | Bottom | Left
}
// Index into neighbors array.
enum Neighbor
{
Top = 0,
Right,
Bottom,
Left,
TopLeft,
TopRight,
BottomRight,
BottomLeft
}
readonly struct TileInfo(in float3 screenPosition, byte variant)
{
public readonly float3 ScreenPosition = screenPosition;
public readonly byte Variant = variant;
}
readonly ShroudRendererInfo info;
readonly World world;
readonly Map map;
readonly (Edges, Edges) notVisibleEdgesPair;
readonly byte variantStride;
readonly byte[] edgesToSpriteIndexOffset;
// PERF: Allocate once.
readonly Shroud.CellVisibility[] neighbors = new Shroud.CellVisibility[8];
readonly CellLayer<TileInfo> tileInfos;
readonly CellLayer<bool> cellsDirty;
bool anyCellDirty;
readonly (Sprite Sprite, float Scale, float Alpha)[] fogSprites, shroudSprites;
Shroud shroud;
Func<PPos, Shroud.CellVisibility> cellVisibility;
TerrainSpriteLayer shroudLayer, fogLayer;
PaletteReference shroudPaletteReference, fogPaletteReference;
bool disposed;
public ShroudRenderer(World world, ShroudRendererInfo info)
{
if (info.ShroudVariants.Length != info.FogVariants.Length)
throw new ArgumentException("ShroudRenderer must define the same number of shroud and fog variants!", nameof(info));
if ((info.OverrideFullFog == null) ^ (info.OverrideFullShroud == null))
throw new ArgumentException("ShroudRenderer cannot define overrides for only one of shroud or fog!", nameof(info));
if (info.ShroudVariants.Length > byte.MaxValue)
throw new ArgumentException("ShroudRenderer cannot define this many shroud and fog variants.", nameof(info));
if (info.Index.Length >= byte.MaxValue)
throw new ArgumentException("ShroudRenderer cannot define this many indexes for shroud directions.", nameof(info));
this.info = info;
this.world = world;
map = world.Map;
tileInfos = new CellLayer<TileInfo>(map);
cellsDirty = new CellLayer<bool>(map);
anyCellDirty = true;
// Load sprite variants
var variantCount = info.ShroudVariants.Length;
variantStride = (byte)(info.Index.Length + (info.OverrideFullShroud != null ? 1 : 0));
shroudSprites = new (Sprite, float, float)[variantCount * variantStride];
fogSprites = new (Sprite, float, float)[variantCount * variantStride];
var sequences = map.Sequences;
for (var j = 0; j < variantCount; j++)
{
var shroudSequence = sequences.GetSequence(info.Sequence, info.ShroudVariants[j]);
var fogSequence = sequences.GetSequence(info.Sequence, info.FogVariants[j]);
for (var i = 0; i < info.Index.Length; i++)
{
shroudSprites[j * variantStride + i] = (shroudSequence.GetSprite(i), shroudSequence.Scale, shroudSequence.GetAlpha(i));
fogSprites[j * variantStride + i] = (fogSequence.GetSprite(i), fogSequence.Scale, fogSequence.GetAlpha(i));
}
if (info.OverrideFullShroud != null)
{
var i = (j + 1) * variantStride - 1;
shroudSequence = sequences.GetSequence(info.Sequence, info.OverrideFullShroud);
fogSequence = sequences.GetSequence(info.Sequence, info.OverrideFullFog);
shroudSprites[i] = (shroudSequence.GetSprite(0), shroudSequence.Scale, shroudSequence.GetAlpha(0));
fogSprites[i] = (fogSequence.GetSprite(0), fogSequence.Scale, fogSequence.GetAlpha(0));
}
}
int spriteCount;
if (info.UseExtendedIndex)
{
notVisibleEdgesPair = (Edges.AllSides, Edges.AllSides);
spriteCount = (int)Edges.All;
}
else
{
notVisibleEdgesPair = (Edges.AllCorners, Edges.AllCorners);
spriteCount = (int)Edges.AllCorners;
}
// Mapping of shrouded directions -> sprite index
edgesToSpriteIndexOffset = new byte[spriteCount + 1];
for (var i = 0; i < info.Index.Length; i++)
edgesToSpriteIndexOffset[info.Index[i]] = (byte)i;
if (info.OverrideFullShroud != null)
edgesToSpriteIndexOffset[info.OverrideShroudIndex] = (byte)(variantStride - 1);
world.RenderPlayerChanged += WorldOnRenderPlayerChanged;
}
void IWorldLoaded.WorldLoaded(World w, WorldRenderer wr)
{
// Initialize tile cache
// This includes the region outside the visible area to cover any sprites peeking outside the map
foreach (var uv in w.Map.AllCells.MapCoords)
{
var pos = w.Map.CenterOfCell(uv.ToCPos(map));
var screen = wr.Screen3DPosition(pos - new WVec(0, 0, pos.Z));
var variant = (byte)Game.CosmeticRandom.Next(info.ShroudVariants.Length);
tileInfos[uv] = new TileInfo(screen, variant);
}
// All tiles are visible in the editor
if (w.Type == WorldType.Editor)
cellVisibility = puv => map.Contains(puv) ? Shroud.CellVisibility.Visible | Shroud.CellVisibility.Explored : Shroud.CellVisibility.Explored;
else
cellVisibility = puv => map.Contains(puv) ? Shroud.CellVisibility.Visible | Shroud.CellVisibility.Explored : Shroud.CellVisibility.Hidden;
var shroudBlend = shroudSprites[0].Sprite.BlendMode;
if (shroudSprites.Any(s => s.Sprite.BlendMode != shroudBlend))
throw new InvalidDataException("Shroud sprites must all use the same blend mode.");
var fogBlend = fogSprites[0].Sprite.BlendMode;
if (fogSprites.Any(s => s.Sprite.BlendMode != fogBlend))
throw new InvalidDataException("Fog sprites must all use the same blend mode.");
var emptySprite = new Sprite(shroudSprites[0].Sprite.Sheet, Rectangle.Empty, TextureChannel.Alpha);
shroudPaletteReference = wr.Palette(info.ShroudPalette);
fogPaletteReference = wr.Palette(info.FogPalette);
shroudLayer = new TerrainSpriteLayer(w, wr, emptySprite, shroudBlend, false);
fogLayer = new TerrainSpriteLayer(w, wr, emptySprite, fogBlend, false);
WorldOnRenderPlayerChanged(world.RenderPlayer);
}
Shroud.CellVisibility[] GetNeighborsVisbility(PPos puv)
{
var cell = ((MPos)puv).ToCPos(map);
neighbors[(int)Neighbor.Top] = cellVisibility((PPos)(cell + new CVec(0, -1)).ToMPos(map));
neighbors[(int)Neighbor.Right] = cellVisibility((PPos)(cell + new CVec(1, 0)).ToMPos(map));
neighbors[(int)Neighbor.Bottom] = cellVisibility((PPos)(cell + new CVec(0, 1)).ToMPos(map));
neighbors[(int)Neighbor.Left] = cellVisibility((PPos)(cell + new CVec(-1, 0)).ToMPos(map));
neighbors[(int)Neighbor.TopLeft] = cellVisibility((PPos)(cell + new CVec(-1, -1)).ToMPos(map));
neighbors[(int)Neighbor.TopRight] = cellVisibility((PPos)(cell + new CVec(1, -1)).ToMPos(map));
neighbors[(int)Neighbor.BottomRight] = cellVisibility((PPos)(cell + new CVec(1, 1)).ToMPos(map));
neighbors[(int)Neighbor.BottomLeft] = cellVisibility((PPos)(cell + new CVec(-1, 1)).ToMPos(map));
return neighbors;
}
Edges GetEdges(Shroud.CellVisibility[] neighbors, Shroud.CellVisibility visibleMask)
{
// If a side is shrouded then we also count the corners.
var edges = Edges.None;
if ((neighbors[(int)Neighbor.Top] & visibleMask) == 0) edges |= Edges.Top;
if ((neighbors[(int)Neighbor.Right] & visibleMask) == 0) edges |= Edges.Right;
if ((neighbors[(int)Neighbor.Bottom] & visibleMask) == 0) edges |= Edges.Bottom;
if ((neighbors[(int)Neighbor.Left] & visibleMask) == 0) edges |= Edges.Left;
var ucorner = edges & Edges.AllCorners;
if ((neighbors[(int)Neighbor.TopLeft] & visibleMask) == 0) edges |= Edges.TopLeft;
if ((neighbors[(int)Neighbor.TopRight] & visibleMask) == 0) edges |= Edges.TopRight;
if ((neighbors[(int)Neighbor.BottomRight] & visibleMask) == 0) edges |= Edges.BottomRight;
if ((neighbors[(int)Neighbor.BottomLeft] & visibleMask) == 0) edges |= Edges.BottomLeft;
// RA provides a set of frames for tiles with shrouded
// corners but unshrouded edges. We want to detect this
// situation without breaking the edge -> corner enabling
// in other combinations. The XOR turns off the corner
// bits that are enabled twice, which gives the sprite offset
// we want here.
return info.UseExtendedIndex ? edges ^ ucorner : edges & Edges.AllCorners;
}
(Edges EdgesShroud, Edges EdgesFog) GetEdges(PPos puv)
{
var cv = cellVisibility(puv);
// If a cell is covered by shroud, then all neighbors are covered by shroud and fog.
if (!cv.HasFlag(Shroud.CellVisibility.Explored))
return notVisibleEdgesPair;
var ncv = GetNeighborsVisbility(puv);
// If a cell is covered by fog, then all neighbors are as well.
var edgesFog = cv.HasFlag(Shroud.CellVisibility.Visible) ? GetEdges(ncv, Shroud.CellVisibility.Visible) : notVisibleEdgesPair.Item2;
var edgesShroud = GetEdges(ncv, Shroud.CellVisibility.Explored);
return (edgesShroud, edgesFog);
}
void WorldOnRenderPlayerChanged(Player player)
{
var newShroud = player?.Shroud;
if (shroud != newShroud)
{
if (shroud != null)
shroud.OnShroudChanged -= UpdateShroudCell;
if (newShroud != null)
{
cellVisibility = newShroud.GetVisibility;
newShroud.OnShroudChanged += UpdateShroudCell;
}
else
{
// Visible under shroud: Explored. Visible under fog: Visible.
cellVisibility = puv => map.Contains(puv) ? Shroud.CellVisibility.Visible | Shroud.CellVisibility.Explored : Shroud.CellVisibility.Hidden;
}
shroud = newShroud;
}
// Dirty the full projected space so the cells outside
// the map bounds can be initialized as fully shrouded.
cellsDirty.Clear(true);
anyCellDirty = true;
var tl = new PPos(0, 0);
var br = new PPos(map.MapSize.X - 1, map.MapSize.Y - 1);
UpdateShroud(new ProjectedCellRegion(map, tl, br));
}
void UpdateShroud(IEnumerable<PPos> region)
{
if (!anyCellDirty)
return;
foreach (var puv in region)
{
var uv = (MPos)puv;
if (!cellsDirty[uv] || !tileInfos.Contains(uv))
continue;
cellsDirty[uv] = false;
var tileInfo = tileInfos[uv];
var (edgesShroud, edgesFog) = GetEdges(puv);
var shroudSprite = GetSprite(shroudSprites, edgesShroud, tileInfo.Variant);
var shroudPos = tileInfo.ScreenPosition;
if (shroudSprite.Sprite != null)
shroudPos += shroudSprite.Sprite.Offset - 0.5f * shroudSprite.Sprite.Size;
var fogSprite = GetSprite(fogSprites, edgesFog, tileInfo.Variant);
var fogPos = tileInfo.ScreenPosition;
if (fogSprite.Sprite != null)
fogPos += fogSprite.Sprite.Offset - 0.5f * fogSprite.Sprite.Size;
shroudLayer.Update(uv, shroudSprite.Sprite, shroudPaletteReference, shroudPos, shroudSprite.Scale, shroudSprite.Alpha, true);
fogLayer.Update(uv, fogSprite.Sprite, fogPaletteReference, fogPos, fogSprite.Scale, fogSprite.Alpha, true);
}
anyCellDirty = false;
}
void IRenderShroud.RenderShroud(WorldRenderer wr)
{
UpdateShroud(map.ProjectedCells);
fogLayer.Draw(wr.Viewport);
shroudLayer.Draw(wr.Viewport);
}
void UpdateShroudCell(PPos puv)
{
var uv = (MPos)puv;
cellsDirty[uv] = true;
anyCellDirty = true;
var cell = uv.ToCPos(map);
foreach (var direction in CVec.Directions)
if (map.Contains((PPos)(cell + direction).ToMPos(map)))
cellsDirty[cell + direction] = true;
}
(Sprite Sprite, float Scale, float Alpha) GetSprite((Sprite, float, float)[] sprites, Edges edges, int variant)
{
if (edges == Edges.None)
return (null, 1f, 1f);
return sprites[variant * variantStride + edgesToSpriteIndexOffset[(byte)edges]];
}
void INotifyActorDisposing.Disposing(Actor self)
{
if (disposed)
return;
shroudLayer.Dispose();
fogLayer.Dispose();
disposed = true;
}
}
}