Files
OpenRA/OpenRA.Mods.Cnc/Traits/World/TSVeinsRenderer.cs
2024-11-03 16:52:47 +02:00

430 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.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 HashSet<string> VeinholeActors = new();
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.X; i++)
{
for (var j = 0; j < map.MapSize.Y; 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 Dictionary<Adjacency, int[]> BorderIndices = new()
{
{ 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 } },
};
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 = new();
readonly Queue<CPos> cleanDirty = new();
readonly HashSet<CPos> veinholeCells = new();
readonly int maxDensity;
readonly Color veinRadarColor;
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);
}
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);
veinSequence = w.Map.Sequences.GetSequence(info.Image, info.Sequence);
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;
}
}
}