Files
OpenRA/OpenRA.Mods.Cnc/Traits/World/TSVeinsRenderer.cs
RoosterDragon 9e548fd0f0 Freeze non-mutable readonly static dictionaries.
Since these will live for the lifetime of the process, it is worth freezing them on creation to improve lookup performance. If the dictionaries were iterated, change to an immutable array instead to preserve the order.
2025-12-12 09:51:27 +02:00

431 lines
13 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.Frozen;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Cnc.Traits
{
[TraitLocation(SystemActors.World | SystemActors.EditorWorld)]
[Desc("Renders the Tiberian Sun Vein resources.", "Attach this to the world actor")]
public class TSVeinsRendererInfo : TraitInfo, Requires<IResourceLayerInfo>, IMapPreviewSignatureInfo
{
[FieldLoader.Require]
[Desc("Resource type used for veins.")]
public readonly string ResourceType = null;
[Desc("Sequence image that holds the different variants.")]
public readonly string Image = "resources";
[SequenceReference(nameof(Image))]
[Desc("Vein sequence name.")]
public readonly string Sequence = "veins";
[PaletteReference]
[Desc("Palette used for rendering the resource sprites.")]
public readonly string Palette = TileSet.TerrainPaletteInternalName;
[FieldLoader.Require]
[FluentReference]
[Desc("Resource name used by tooltips.")]
public readonly string Name = null;
[ActorReference]
[Desc("Actor types that should be treated as veins for adjacency.")]
public readonly FrozenSet<string> VeinholeActors = FrozenSet<string>.Empty;
void IMapPreviewSignatureInfo.PopulateMapPreviewSignatureCells(Map map, ActorInfo ai, ActorReference s, List<(MPos Uv, Color Color)> destinationBuffer)
{
var resourceLayer = ai.TraitInfoOrDefault<IResourceLayerInfo>();
if (resourceLayer == null)
return;
if (!resourceLayer.TryGetResourceIndex(ResourceType, out var resourceIndex) || !resourceLayer.TryGetTerrainType(ResourceType, out var terrainType))
return;
var veinholeCells = new HashSet<CPos>();
foreach (var kv in map.ActorDefinitions)
{
var type = kv.Value.Value;
if (!VeinholeActors.Contains(type))
continue;
var actorReference = new ActorReference(type, kv.Value.ToDictionary());
var location = actorReference.Get<LocationInit>();
var veinholeInfo = map.Rules.Actors[actorReference.Type];
foreach (var cell in veinholeInfo.TraitInfo<IOccupySpaceInfo>().OccupiedCells(veinholeInfo, location.Value))
veinholeCells.Add(cell.Key);
}
var terrainInfo = map.Rules.TerrainInfo;
var info = terrainInfo.TerrainTypes[terrainInfo.GetTerrainIndex(terrainType)];
for (var i = 0; i < map.MapSize.Width; i++)
{
for (var j = 0; j < map.MapSize.Height; j++)
{
var uv = new MPos(i, j);
// Cell contains veins
if (map.Resources[uv].Type == resourceIndex)
{
destinationBuffer.Add((uv, info.Color));
continue;
}
// Cell is a vein border if it is flat and adjacent to at least one cell
// that is also flat and contains veins (borders are not drawn next to slope vein cells)
var isBorder = map.Ramp[uv] == 0 && Common.Util.ExpandFootprint(uv.ToCPos(map), false).Any(c =>
{
if (!map.Resources.Contains(c))
return false;
if (veinholeCells.Contains(c))
return true;
return map.Resources[c].Type == resourceIndex && map.Ramp[c] == 0;
});
if (isBorder)
destinationBuffer.Add((uv, info.Color));
}
}
}
public override object Create(ActorInitializer init) { return new TSVeinsRenderer(init.Self, this); }
}
public class TSVeinsRenderer : IResourceRenderer, IWorldLoaded, IRenderOverlay, ITickRender, INotifyActorDisposing, IRadarTerrainLayer
{
[Flags]
enum Adjacency : byte
{
None = 0x0,
MinusX = 0x1,
PlusX = 0x2,
MinusY = 0x4,
PlusY = 0x8,
}
static readonly FrozenDictionary<Adjacency, int[]> BorderIndices = new Dictionary<Adjacency, int[]>
{
{ Adjacency.MinusY, new[] { 3, 4, 5 } },
{ Adjacency.PlusX, new[] { 6, 7, 8 } },
{ Adjacency.MinusY | Adjacency.PlusX, new[] { 9, 10, 11 } },
{ Adjacency.PlusY, new[] { 12, 13, 14 } },
{ Adjacency.MinusY | Adjacency.PlusY, new[] { 15, 16, 17 } },
{ Adjacency.PlusY | Adjacency.PlusX, new[] { 18, 19, 20 } },
{ Adjacency.MinusY | Adjacency.PlusY | Adjacency.PlusX, new[] { 21, 22, 23 } },
{ Adjacency.MinusX, new[] { 24, 25, 26 } },
{ Adjacency.MinusX | Adjacency.MinusY, new[] { 27, 28, 29 } },
{ Adjacency.MinusX | Adjacency.PlusX, new[] { 30, 31, 32 } },
{ Adjacency.MinusX | Adjacency.PlusX | Adjacency.MinusY, new[] { 33, 34, 35 } },
{ Adjacency.MinusX | Adjacency.PlusY, new[] { 36, 37, 38 } },
{ Adjacency.MinusX | Adjacency.MinusY | Adjacency.PlusY, new[] { 39, 40, 41 } },
{ Adjacency.MinusX | Adjacency.PlusX | Adjacency.PlusY, new[] { 42, 43, 44 } },
{ Adjacency.MinusX | Adjacency.PlusX | Adjacency.MinusY | Adjacency.PlusY, new[] { 45, 46, 47 } },
}.ToFrozenDictionary();
static readonly int[] HeavyIndices = [48, 49, 50, 51];
static readonly int[] LightIndices = [52];
static readonly int[] Ramp1Indices = [53, 54];
static readonly int[] Ramp2Indices = [55, 56];
static readonly int[] Ramp3Indices = [57, 58];
static readonly int[] Ramp4Indices = [59, 60];
readonly TSVeinsRendererInfo info;
readonly World world;
readonly IResourceLayer resourceLayer;
readonly CellLayer<int[]> renderIndices;
readonly CellLayer<Adjacency> borders;
readonly HashSet<CPos> dirty = [];
readonly Queue<CPos> cleanDirty = [];
readonly HashSet<CPos> veinholeCells = [];
readonly byte maxDensity;
readonly Color veinRadarColor;
readonly ISpriteSequence veinSequence;
PaletteReference veinPalette;
TerrainSpriteLayer spriteLayer;
public TSVeinsRenderer(Actor self, TSVeinsRendererInfo info)
{
this.info = info;
world = self.World;
resourceLayer = self.Trait<IResourceLayer>();
resourceLayer.CellChanged += AddDirtyCell;
maxDensity = resourceLayer.GetMaxDensity(info.ResourceType);
var terrainInfo = self.World.Map.Rules.TerrainInfo;
resourceLayer.Info.TryGetTerrainType(info.ResourceType, out var terrainType);
veinRadarColor = terrainInfo.TerrainTypes[terrainInfo.GetTerrainIndex(terrainType)].Color;
renderIndices = new CellLayer<int[]>(world.Map);
borders = new CellLayer<Adjacency>(world.Map);
veinSequence = self.World.Map.Sequences.GetSequence(info.Image, info.Sequence);
}
void AddDirtyCell(CPos cell, string resourceType)
{
if (resourceType == null || resourceType == info.ResourceType)
dirty.Add(cell);
}
void IWorldLoaded.WorldLoaded(World w, WorldRenderer wr)
{
// Cache locations of veinhole actors
// TODO: Add support for monitoring actors placed in the map editor!
w.ActorAdded += ActorAddedToWorld;
w.ActorRemoved += ActorRemovedFromWorld;
foreach (var a in w.Actors)
ActorAddedToWorld(a);
veinPalette = wr.Palette(info.Palette);
var first = veinSequence.GetSprite(0);
var emptySprite = new Sprite(first.Sheet, Rectangle.Empty, TextureChannel.Alpha);
spriteLayer = new TerrainSpriteLayer(w, wr, emptySprite, first.BlendMode, wr.World.Type != WorldType.Editor);
// Initialize the renderIndices with the initial map state so it is visible
// through the fog with the Explored Map option enabled
foreach (var cell in w.Map.AllCells)
{
var resource = resourceLayer.GetResource(cell);
var cellIndices = CalculateCellIndices(resource, cell);
if (cellIndices != null)
{
renderIndices[cell] = cellIndices;
UpdateRenderedSprite(cell, cellIndices);
}
}
}
int[] CalculateCellIndices(ResourceLayerContents contents, CPos cell)
{
if (contents.Type != info.ResourceType || contents.Density == 0)
return null;
var ramp = world.Map.Ramp[cell];
switch (ramp)
{
case 1: return Ramp1Indices;
case 2: return Ramp2Indices;
case 3: return Ramp3Indices;
case 4: return Ramp4Indices;
default: return contents.Density == maxDensity ? HeavyIndices : LightIndices;
}
}
void IRenderOverlay.Render(WorldRenderer wr)
{
spriteLayer.Draw(wr.Viewport);
}
void ITickRender.TickRender(WorldRenderer wr, Actor self)
{
foreach (var cell in dirty)
{
if (!resourceLayer.IsVisible(cell))
continue;
var contents = resourceLayer.GetResource(cell);
var cellIndices = CalculateCellIndices(contents, cell);
if (cellIndices != renderIndices[cell])
{
renderIndices[cell] = cellIndices;
UpdateRenderedSprite(cell, cellIndices);
}
cleanDirty.Enqueue(cell);
}
while (cleanDirty.Count > 0)
dirty.Remove(cleanDirty.Dequeue());
}
bool HasBorder(CPos cell)
{
if (!renderIndices.Contains(cell))
return false;
// Draw the vein border if this is a flat cell with veins, or a veinhole
return (world.Map.Ramp[cell] == 0 && renderIndices[cell] != null) || veinholeCells.Contains(cell);
}
Adjacency CalculateBorders(CPos cell)
{
// Borders are only valid on flat cells
if (world.Map.Ramp[cell] != 0)
return Adjacency.None;
var ret = Adjacency.None;
if (HasBorder(cell + new CVec(0, -1)))
ret |= Adjacency.MinusY;
if (HasBorder(cell + new CVec(-1, 0)))
ret |= Adjacency.MinusX;
if (HasBorder(cell + new CVec(1, 0)))
ret |= Adjacency.PlusX;
if (HasBorder(cell + new CVec(0, 1)))
ret |= Adjacency.PlusY;
return ret;
}
void UpdateRenderedSprite(CPos cell, int[] indices)
{
borders[cell] = Adjacency.None;
UpdateSpriteLayers(cell, indices);
foreach (var c in Common.Util.ExpandFootprint(cell, false))
UpdateBorderSprite(c);
}
void UpdateBorderSprite(CPos cell)
{
// Borders are never drawn on ramps or in cells that contain resources
if (HasBorder(cell) || world.Map.Ramp[cell] != 0)
return;
var adjacency = CalculateBorders(cell);
if (borders[cell] == adjacency)
return;
borders[cell] = adjacency;
if (adjacency == Adjacency.None)
UpdateSpriteLayers(cell, null);
else if (BorderIndices.TryGetValue(adjacency, out var indices))
UpdateSpriteLayers(cell, indices);
else
throw new InvalidOperationException($"SpriteMap does not contain an index for Adjacency type '{adjacency}'");
}
void UpdateSpriteLayers(CPos cell, int[] indices)
{
if (indices != null)
spriteLayer.Update(cell, veinSequence, veinPalette, indices.Random(world.LocalRandom));
else
spriteLayer.Clear(cell);
}
void ActorAddedToWorld(Actor a)
{
if (info.VeinholeActors.Contains(a.Info.Name))
{
foreach (var cell in a.OccupiesSpace.OccupiedCells())
{
veinholeCells.Add(cell.Cell);
AddDirtyCell(cell.Cell, info.ResourceType);
}
}
}
void ActorRemovedFromWorld(Actor a)
{
if (info.VeinholeActors.Contains(a.Info.Name))
{
foreach (var cell in a.OccupiesSpace.OccupiedCells())
{
veinholeCells.Remove(cell.Cell);
AddDirtyCell(cell.Cell, null);
}
}
}
void INotifyActorDisposing.Disposing(Actor self)
{
resourceLayer.CellChanged -= AddDirtyCell;
world.ActorAdded -= ActorAddedToWorld;
world.ActorRemoved -= ActorRemovedFromWorld;
}
IEnumerable<string> IResourceRenderer.ResourceTypes { get { yield return info.ResourceType; } }
string IResourceRenderer.GetRenderedResourceType(CPos cell)
{
if (renderIndices[cell] != null)
return info.ResourceType;
// This makes sure harvesters will get a harvest cursor but then move to the next actual resource cell to start harvesting
return borders[cell] != Adjacency.None ? info.ResourceType : null;
}
string IResourceRenderer.GetRenderedResourceTooltip(CPos cell)
{
if (renderIndices[cell] != null || borders[cell] != Adjacency.None)
return FluentProvider.GetMessage(info.Name);
return null;
}
IEnumerable<IRenderable> IResourceRenderer.RenderUIPreview(WorldRenderer wr, string resourceType, int2 origin, float scale)
{
if (resourceType != info.ResourceType)
yield break;
var sprite = veinSequence.GetSprite(HeavyIndices[0]);
var palette = wr.Palette(info.Palette);
yield return new UISpriteRenderable(sprite, WPos.Zero, origin, 0, palette, scale);
}
IEnumerable<IRenderable> IResourceRenderer.RenderPreview(WorldRenderer wr, string resourceType, WPos origin)
{
if (resourceType != info.ResourceType)
yield break;
var frame = HeavyIndices[0];
var sprite = veinSequence.GetSprite(frame);
var alpha = veinSequence.GetAlpha(frame);
var palette = wr.Palette(info.Palette);
var tintModifiers = veinSequence.IgnoreWorldTint ? TintModifiers.IgnoreWorldTint : TintModifiers.None;
yield return new SpriteRenderable(sprite, origin, WVec.Zero, 0, palette, veinSequence.Scale, alpha, float3.Ones, tintModifiers, false);
}
event Action<CPos> IRadarTerrainLayer.CellEntryChanged
{
add
{
renderIndices.CellEntryChanged += value;
borders.CellEntryChanged += value;
}
remove
{
renderIndices.CellEntryChanged -= value;
borders.CellEntryChanged -= value;
}
}
bool IRadarTerrainLayer.TryGetTerrainColorPair(MPos uv, out (Color Left, Color Right) value)
{
value = default;
if (borders[uv] == Adjacency.None && renderIndices[uv] == null)
return false;
value = (veinRadarColor, veinRadarColor);
return true;
}
}
}