#region Copyright & License Information /* * Copyright 2007-2021 The OpenRA Developers (see AUTHORS) * 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 { [Desc("Renders the Tiberian Sun Vein resources.", "Attach this to the world actor")] public class TSVeinsRendererInfo : TraitInfo, Requires { [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] [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 VeinholeActors = new HashSet { }; public override object Create(ActorInitializer init) { return new TSVeinsRenderer(init.Self, this); } } public class TSVeinsRenderer : IResourceRenderer, IWorldLoaded, IRenderOverlay, ITickRender, INotifyActorDisposing { [Flags] enum Adjacency : byte { None = 0x0, MinusX = 0x1, PlusX = 0x2, MinusY = 0x4, PlusY = 0x8, } static readonly Dictionary BorderIndices = new Dictionary() { { 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 renderIndices; readonly CellLayer borders; readonly HashSet dirty = new HashSet(); readonly Queue cleanDirty = new Queue(); readonly HashSet veinholeCells = new HashSet(); readonly int maxDensity; ISpriteSequence veinSequence; PaletteReference veinPalette; TerrainSpriteLayer spriteLayer; public TSVeinsRenderer(Actor self, TSVeinsRendererInfo info) { this.info = info; world = self.World; resourceLayer = self.Trait(); resourceLayer.CellChanged += AddDirtyCell; maxDensity = resourceLayer.GetMaxDensity(info.ResourceType); renderIndices = new CellLayer(world.Map); borders = new CellLayer(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.Rules.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 '{0}'".F(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 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) return info.Name; return borders[cell] != Adjacency.None ? info.Name : null; } IEnumerable IResourceRenderer.RenderUIPreview(WorldRenderer wr, string resourceType, int2 origin, float scale) { if (resourceType != info.ResourceType) yield break; var sprite = veinSequence.GetSprite(HeavyIndices.First()); var palette = wr.Palette(info.Palette); yield return new UISpriteRenderable(sprite, WPos.Zero, origin, 0, palette, scale); } IEnumerable IResourceRenderer.RenderPreview(WorldRenderer wr, string resourceType, WPos origin) { if (resourceType != info.ResourceType) yield break; var frame = HeavyIndices.First(); 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); } } }