From ac200f6173287380e4d179ddb4694fa70d34ce91 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Mon, 9 Mar 2020 19:56:31 +0000 Subject: [PATCH] Rework decoration renderable traits: - Removed implicit pip definitions and IPips interface. New decoration traits have been added to render them. Pip types are no longer hardcoded in OpenRA.Game. - Decoration rendering is now managed by SelectionDecorations(Base), which allows us to remove assumptions about the selection box geometry from the decoration traits. - RenderNameTag has been replaced by WithNameTagDecoration, which is an otherwise normal decoration trait. - Unify the configuration and reduce duplication between traits. - Removed hardcoded references to specific selection box renderables. - Remove legacy cruft. --- OpenRA.Game/Primitives/Rectangle.cs | 5 + OpenRA.Game/Traits/TraitsInterfaces.cs | 42 +-- .../Traits/SupportPowers/ChronoshiftPower.cs | 10 +- .../Graphics/UITextRenderable.cs | 72 +++++ OpenRA.Mods.Common/Traits/AmmoPool.cs | 20 +- OpenRA.Mods.Common/Traits/Cargo.cs | 29 +- OpenRA.Mods.Common/Traits/Harvester.cs | 32 +- OpenRA.Mods.Common/Traits/Interactable.cs | 6 +- OpenRA.Mods.Common/Traits/Passenger.cs | 5 +- .../Traits/Render/RenderNameTag.cs | 65 ----- .../Traits/Render/SelectionDecorations.cs | 157 ++-------- .../Traits/Render/SelectionDecorationsBase.cs | 145 ++++++++++ .../Render/VeteranProductionIconOverlay.cs | 18 +- .../Traits/Render/WithAmmoPipsDecoration.cs | 83 ++++++ .../Traits/Render/WithCargoPipsDecoration.cs | 101 +++++++ .../Traits/Render/WithDecoration.cs | 166 +---------- .../Traits/Render/WithDecorationBase.cs | 147 ++++++++++ .../Render/WithHarvesterPipsDecoration.cs | 99 +++++++ .../Traits/Render/WithNameTagDecoration.cs | 84 ++++++ .../WithResourceStoragePipsDecoration.cs | 80 +++++ .../WithSpriteControlGroupDecoration.cs | 63 ++-- .../Render/WithTextControlGroupDecoration.cs | 81 ++---- .../Traits/Render/WithTextDecoration.cs | 84 +----- OpenRA.Mods.Common/Traits/StoresResources.cs | 15 +- .../GrantExternalConditionPower.cs | 5 +- .../Traits/World/ResourceType.cs | 3 - OpenRA.Mods.Common/TraitsInterfaces.cs | 37 +++ .../Rules/AddPipDecorationTraits.cs | 273 ++++++++++++++++++ .../Rules/ModernizeDecorationTraits.cs | 123 ++++++++ OpenRA.Mods.Common/UpdateRules/UpdatePath.cs | 10 +- .../WorldInteractionControllerWidget.cs | 3 +- 31 files changed, 1377 insertions(+), 686 deletions(-) create mode 100644 OpenRA.Mods.Common/Graphics/UITextRenderable.cs delete mode 100644 OpenRA.Mods.Common/Traits/Render/RenderNameTag.cs create mode 100644 OpenRA.Mods.Common/Traits/Render/SelectionDecorationsBase.cs create mode 100644 OpenRA.Mods.Common/Traits/Render/WithAmmoPipsDecoration.cs create mode 100644 OpenRA.Mods.Common/Traits/Render/WithCargoPipsDecoration.cs create mode 100644 OpenRA.Mods.Common/Traits/Render/WithDecorationBase.cs create mode 100644 OpenRA.Mods.Common/Traits/Render/WithHarvesterPipsDecoration.cs create mode 100644 OpenRA.Mods.Common/Traits/Render/WithNameTagDecoration.cs create mode 100644 OpenRA.Mods.Common/Traits/Render/WithResourceStoragePipsDecoration.cs create mode 100644 OpenRA.Mods.Common/UpdateRules/Rules/AddPipDecorationTraits.cs create mode 100644 OpenRA.Mods.Common/UpdateRules/Rules/ModernizeDecorationTraits.cs diff --git a/OpenRA.Game/Primitives/Rectangle.cs b/OpenRA.Game/Primitives/Rectangle.cs index 4e016f81af..1d35bbc347 100644 --- a/OpenRA.Game/Primitives/Rectangle.cs +++ b/OpenRA.Game/Primitives/Rectangle.cs @@ -66,6 +66,11 @@ namespace OpenRA.Primitives public int2 Location { get { return new int2(X, Y); } } public Size Size { get { return new Size(Width, Height); } } + public int2 TopLeft { get { return Location; } } + public int2 TopRight { get { return new int2(X + Width, Y); } } + public int2 BottomLeft { get { return new int2(X, Y + Height); } } + public int2 BottomRight { get { return new int2(X + Width, Y + Height); } } + public bool Contains(int x, int y) { return x >= Left && x < Right && y >= Top && y < Bottom; diff --git a/OpenRA.Game/Traits/TraitsInterfaces.cs b/OpenRA.Game/Traits/TraitsInterfaces.cs index 356072b9a6..2b31676d2a 100644 --- a/OpenRA.Game/Traits/TraitsInterfaces.cs +++ b/OpenRA.Game/Traits/TraitsInterfaces.cs @@ -66,9 +66,6 @@ namespace OpenRA.Traits void Kill(Actor self, Actor attacker, BitSet damageTypes); } - // depends on the order of pips in WorldRenderer.cs! - public enum PipType { Transparent, Green, Yellow, Red, Gray, Blue, Ammo, AmmoEmpty } - [Flags] public enum Stance { @@ -128,38 +125,6 @@ namespace OpenRA.Traits public interface IMouseBoundsInfo : ITraitInfoInterface { } public interface IAutoMouseBounds { Rectangle AutoMouseoverBounds(Actor self, WorldRenderer wr); } - // HACK: This provides a shim for legacy code until it can be rewritten - public interface IDecorationBounds { Rectangle DecorationBounds(Actor self, WorldRenderer wr); } - public interface IDecorationBoundsInfo : ITraitInfoInterface { } - public static class DecorationBoundsExtensions - { - public static Rectangle FirstNonEmptyBounds(this IEnumerable decorationBounds, Actor self, WorldRenderer wr) - { - // PERF: Avoid LINQ. - foreach (var decoration in decorationBounds) - { - var bounds = decoration.DecorationBounds(self, wr); - if (!bounds.IsEmpty) - return bounds; - } - - return Rectangle.Empty; - } - - public static Rectangle FirstNonEmptyBounds(this IDecorationBounds[] decorationBounds, Actor self, WorldRenderer wr) - { - // PERF: Avoid LINQ. - foreach (var decoration in decorationBounds) - { - var bounds = decoration.DecorationBounds(self, wr); - if (!bounds.IsEmpty) - return bounds; - } - - return Rectangle.Empty; - } - } - public interface IIssueOrder { IEnumerable Orders { get; } @@ -296,12 +261,15 @@ namespace OpenRA.Traits public interface ILoadsPalettes { void LoadPalettes(WorldRenderer wr); } public interface ILoadsPlayerPalettes { void LoadPlayerPalettes(WorldRenderer wr, string playerName, Color playerColor, bool replaceExisting); } public interface IPaletteModifier { void AdjustPalette(IReadOnlyDictionary b); } - public interface IPips { IEnumerable GetPips(Actor self); } [RequireExplicitImplementation] public interface ISelectionBar { float GetValue(); Color GetColor(); bool DisplayWhenEmpty { get; } } - public interface ISelectionDecorations { void DrawRollover(Actor self, WorldRenderer worldRenderer); } + public interface ISelectionDecorations + { + IEnumerable RenderRolloverAnnotations(Actor self, WorldRenderer worldRenderer); + IEnumerable RenderSelectionAnnotations(Actor self, WorldRenderer worldRenderer, Color color); + } public interface IMapPreviewSignatureInfo : ITraitInfoInterface { diff --git a/OpenRA.Mods.Cnc/Traits/SupportPowers/ChronoshiftPower.cs b/OpenRA.Mods.Cnc/Traits/SupportPowers/ChronoshiftPower.cs index 2f64981fc9..3551fefdc9 100644 --- a/OpenRA.Mods.Cnc/Traits/SupportPowers/ChronoshiftPower.cs +++ b/OpenRA.Mods.Cnc/Traits/SupportPowers/ChronoshiftPower.cs @@ -179,8 +179,9 @@ namespace OpenRA.Mods.Cnc.Traits { if (unit.CanBeViewedByPlayer(manager.Self.Owner)) { - var bounds = unit.TraitsImplementing().FirstNonEmptyBounds(unit, wr); - yield return new SelectionBoxAnnotationRenderable(unit, bounds, Color.Red); + var decorations = unit.TraitsImplementing().FirstEnabledTraitOrDefault(); + foreach (var d in decorations.RenderSelectionAnnotations(unit, wr, Color.Red)) + yield return d; } } } @@ -302,8 +303,9 @@ namespace OpenRA.Mods.Cnc.Traits { if (unit.CanBeViewedByPlayer(manager.Self.Owner)) { - var bounds = unit.TraitsImplementing().FirstNonEmptyBounds(unit, wr); - yield return new SelectionBoxAnnotationRenderable(unit, bounds, Color.Red); + var decorations = unit.TraitsImplementing().FirstEnabledTraitOrDefault(); + foreach (var d in decorations.RenderSelectionAnnotations(unit, wr, Color.Red)) + yield return d; } } } diff --git a/OpenRA.Mods.Common/Graphics/UITextRenderable.cs b/OpenRA.Mods.Common/Graphics/UITextRenderable.cs new file mode 100644 index 0000000000..cbbfc25482 --- /dev/null +++ b/OpenRA.Mods.Common/Graphics/UITextRenderable.cs @@ -0,0 +1,72 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 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 OpenRA.Graphics; +using OpenRA.Primitives; +using OpenRA.Widgets; + +namespace OpenRA.Mods.Common.Graphics +{ + public struct UITextRenderable : IRenderable, IFinalizedRenderable + { + readonly SpriteFont font; + readonly WPos effectiveWorldPos; + readonly int2 screenPos; + readonly int zOffset; + readonly Color color; + readonly Color bgDark; + readonly Color bgLight; + readonly string text; + + public UITextRenderable(SpriteFont font, WPos effectiveWorldPos, int2 screenPos, int zOffset, Color color, Color bgDark, Color bgLight, string text) + { + this.font = font; + this.effectiveWorldPos = effectiveWorldPos; + this.screenPos = screenPos; + this.zOffset = zOffset; + this.color = color; + this.bgDark = bgDark; + this.bgLight = bgLight; + this.text = text; + } + + public UITextRenderable(SpriteFont font, WPos effectiveWorldPos, int2 screenPos, int zOffset, Color color, string text) + : this(font, effectiveWorldPos, screenPos, zOffset, color, + ChromeMetrics.Get("TextContrastColorDark"), + ChromeMetrics.Get("TextContrastColorLight"), + text) { } + + public WPos Pos { get { return effectiveWorldPos; } } + public PaletteReference Palette { get { return null; } } + public int ZOffset { get { return zOffset; } } + public bool IsDecoration { get { return true; } } + + public IRenderable WithPalette(PaletteReference newPalette) { return new UITextRenderable(font, effectiveWorldPos, screenPos, zOffset, color, text); } + public IRenderable WithZOffset(int newOffset) { return new UITextRenderable(font, effectiveWorldPos, screenPos, zOffset, color, text); } + public IRenderable OffsetBy(WVec vec) { return new UITextRenderable(font, effectiveWorldPos + vec, screenPos, zOffset, color, text); } + public IRenderable AsDecoration() { return this; } + + public IFinalizedRenderable PrepareRender(WorldRenderer wr) { return this; } + public void Render(WorldRenderer wr) + { + font.DrawTextWithContrast(text, screenPos, color, bgDark, bgLight, 1); + } + + public void RenderDebugGeometry(WorldRenderer wr) + { + var size = font.Measure(text).ToFloat2(); + Game.Renderer.RgbaColorRenderer.DrawRect(screenPos - 0.5f * size, screenPos + 0.5f * size, 1, Color.Red); + } + + public Rectangle ScreenBounds(WorldRenderer wr) { return Rectangle.Empty; } + } +} diff --git a/OpenRA.Mods.Common/Traits/AmmoPool.cs b/OpenRA.Mods.Common/Traits/AmmoPool.cs index 15aff5495e..a7002765f2 100644 --- a/OpenRA.Mods.Common/Traits/AmmoPool.cs +++ b/OpenRA.Mods.Common/Traits/AmmoPool.cs @@ -30,15 +30,6 @@ namespace OpenRA.Mods.Common.Traits [Desc("Initial ammo the actor is created with. Defaults to Ammo.")] public readonly int InitialAmmo = -1; - [Desc("Defaults to value in Ammo. 0 means no visible pips.")] - public readonly int PipCount = -1; - - [Desc("PipType to use for loaded ammo.")] - public readonly PipType PipType = PipType.Green; - - [Desc("PipType to use for empty ammo.")] - public readonly PipType PipTypeEmpty = PipType.Transparent; - [Desc("How much ammo is reloaded after a certain period.")] public readonly int ReloadCount = 1; @@ -56,7 +47,7 @@ namespace OpenRA.Mods.Common.Traits public object Create(ActorInitializer init) { return new AmmoPool(init.Self, this); } } - public class AmmoPool : INotifyCreated, INotifyAttack, IPips, ISync + public class AmmoPool : INotifyCreated, INotifyAttack, ISync { public readonly AmmoPoolInfo Info; readonly Stack tokens = new Stack(); @@ -126,14 +117,5 @@ namespace OpenRA.Mods.Common.Traits while (CurrentAmmoCount < tokens.Count && tokens.Count > 0) conditionManager.RevokeCondition(self, tokens.Pop()); } - - public IEnumerable GetPips(Actor self) - { - var pips = Info.PipCount >= 0 ? Info.PipCount : Info.Ammo; - - return Enumerable.Range(0, pips).Select(i => - (CurrentAmmoCount * pips) / Info.Ammo > i ? - Info.PipType : Info.PipTypeEmpty); - } } } diff --git a/OpenRA.Mods.Common/Traits/Cargo.cs b/OpenRA.Mods.Common/Traits/Cargo.cs index d44d4334d8..13429ecb34 100644 --- a/OpenRA.Mods.Common/Traits/Cargo.cs +++ b/OpenRA.Mods.Common/Traits/Cargo.cs @@ -26,9 +26,6 @@ namespace OpenRA.Mods.Common.Traits [Desc("The maximum sum of Passenger.Weight that this actor can support.")] public readonly int MaxWeight = 0; - [Desc("Number of pips to display when this actor is selected.")] - public readonly int PipCount = 0; - [Desc("`Passenger.CargoType`s that can be loaded into this actor.")] public readonly HashSet Types = new HashSet(); @@ -88,7 +85,7 @@ namespace OpenRA.Mods.Common.Traits public object Create(ActorInitializer init) { return new Cargo(init, this); } } - public class Cargo : IPips, IIssueOrder, IResolveOrder, IOrderVoice, INotifyCreated, INotifyKilled, + public class Cargo : IIssueOrder, IResolveOrder, IOrderVoice, INotifyCreated, INotifyKilled, INotifyOwnerChanged, INotifySold, INotifyActorDisposing, IIssueDeployOrder, ITransformActorInitModifier { @@ -379,30 +376,6 @@ namespace OpenRA.Mods.Common.Traits t.TurretFacing = facing.Value.Facing + Info.PassengerFacing; } - public IEnumerable GetPips(Actor self) - { - var numPips = Info.PipCount; - - for (var i = 0; i < numPips; i++) - yield return GetPipAt(i); - } - - PipType GetPipAt(int i) - { - var n = i * Info.MaxWeight / Info.PipCount; - - foreach (var c in cargo) - { - var pi = c.Info.TraitInfo(); - if (n < pi.Weight) - return pi.PipType; - else - n -= pi.Weight; - } - - return PipType.Transparent; - } - public void Load(Actor self, Actor a) { cargo.Add(a); diff --git a/OpenRA.Mods.Common/Traits/Harvester.cs b/OpenRA.Mods.Common/Traits/Harvester.cs index 890a858551..646adf2c89 100644 --- a/OpenRA.Mods.Common/Traits/Harvester.cs +++ b/OpenRA.Mods.Common/Traits/Harvester.cs @@ -42,9 +42,6 @@ namespace OpenRA.Mods.Common.Traits [Desc("How many bales can it dump at once.")] public readonly int BaleUnloadAmount = 1; - [Desc("How many squares to show the fill level.")] - public readonly int PipCount = 7; - public readonly int HarvestFacings = 0; [Desc("Which resources it can harvest.")] @@ -90,15 +87,16 @@ namespace OpenRA.Mods.Common.Traits public object Create(ActorInitializer init) { return new Harvester(init.Self, this); } } - public class Harvester : IIssueOrder, IResolveOrder, IPips, IOrderVoice, + public class Harvester : IIssueOrder, IResolveOrder, IOrderVoice, ISpeedModifier, ISync, INotifyCreated { public readonly HarvesterInfo Info; + public readonly IReadOnlyDictionary Contents; + readonly Mobile mobile; readonly ResourceLayer resLayer; readonly ResourceClaimLayer claimLayer; readonly Dictionary contents = new Dictionary(); - INotifyHarvesterAction[] notifyHarvesterAction; ConditionManager conditionManager; int conditionToken = ConditionManager.InvalidConditionToken; HarvesterResourceMultiplier[] resourceMultipliers; @@ -127,6 +125,8 @@ namespace OpenRA.Mods.Common.Traits public Harvester(Actor self, HarvesterInfo info) { Info = info; + Contents = new ReadOnlyDictionary(contents); + mobile = self.Trait(); resLayer = self.World.WorldActor.Trait(); claimLayer = self.World.WorldActor.Trait(); @@ -134,7 +134,6 @@ namespace OpenRA.Mods.Common.Traits void INotifyCreated.Created(Actor self) { - notifyHarvesterAction = self.TraitsImplementing().ToArray(); resourceMultipliers = self.TraitsImplementing().ToArray(); conditionManager = self.TraitOrDefault(); UpdateCondition(self); @@ -348,27 +347,6 @@ namespace OpenRA.Mods.Common.Traits } } - PipType GetPipAt(int i) - { - var n = i * Info.Capacity / Info.PipCount; - - foreach (var rt in contents) - if (n < rt.Value) - return rt.Key.PipColor; - else - n -= rt.Value; - - return PipType.Transparent; - } - - IEnumerable IPips.GetPips(Actor self) - { - var numPips = Info.PipCount; - - for (var i = 0; i < numPips; i++) - yield return GetPipAt(i); - } - int ISpeedModifier.GetSpeedModifier() { return 100 - (100 - Info.FullyLoadedSpeed) * contents.Values.Sum() / Info.Capacity; diff --git a/OpenRA.Mods.Common/Traits/Interactable.cs b/OpenRA.Mods.Common/Traits/Interactable.cs index 017daa17fd..a22674d18b 100644 --- a/OpenRA.Mods.Common/Traits/Interactable.cs +++ b/OpenRA.Mods.Common/Traits/Interactable.cs @@ -17,7 +17,7 @@ using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { [Desc("Used to enable mouse interaction on actors that are not Selectable.")] - public class InteractableInfo : ITraitInfo, IMouseBoundsInfo, IDecorationBoundsInfo + public class InteractableInfo : ITraitInfo, IMouseBoundsInfo { [Desc("Defines a custom rectangle for mouse interaction with the actor.", "If null, the engine will guess an appropriate size based on the With*Body trait.", @@ -32,7 +32,7 @@ namespace OpenRA.Mods.Common.Traits public virtual object Create(ActorInitializer init) { return new Interactable(this); } } - public class Interactable : INotifyCreated, IMouseBounds, IDecorationBounds + public class Interactable : INotifyCreated, IMouseBounds { readonly InteractableInfo info; IAutoMouseBounds[] autoBounds; @@ -72,7 +72,7 @@ namespace OpenRA.Mods.Common.Traits return Bounds(self, wr, info.Bounds); } - Rectangle IDecorationBounds.DecorationBounds(Actor self, WorldRenderer wr) + public Rectangle DecorationBounds(Actor self, WorldRenderer wr) { return Bounds(self, wr, info.DecorationBounds ?? info.Bounds); } diff --git a/OpenRA.Mods.Common/Traits/Passenger.cs b/OpenRA.Mods.Common/Traits/Passenger.cs index bbe5b22dd6..3f4e74688a 100644 --- a/OpenRA.Mods.Common/Traits/Passenger.cs +++ b/OpenRA.Mods.Common/Traits/Passenger.cs @@ -23,7 +23,10 @@ namespace OpenRA.Mods.Common.Traits public class PassengerInfo : ITraitInfo, IObservesVariablesInfo { public readonly string CargoType = null; - public readonly PipType PipType = PipType.Green; + + [Desc("If defined, use a custom pip type defined on the transport's WithCargoPipsDecoration.CustomPipSequences list.")] + public readonly string CustomPipType = null; + public readonly int Weight = 1; [GrantedConditionReference] diff --git a/OpenRA.Mods.Common/Traits/Render/RenderNameTag.cs b/OpenRA.Mods.Common/Traits/Render/RenderNameTag.cs deleted file mode 100644 index 8bd39d21b8..0000000000 --- a/OpenRA.Mods.Common/Traits/Render/RenderNameTag.cs +++ /dev/null @@ -1,65 +0,0 @@ -#region Copyright & License Information -/* - * Copyright 2007-2020 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.Collections.Generic; -using System.Linq; -using OpenRA.Graphics; -using OpenRA.Mods.Common.Graphics; -using OpenRA.Primitives; -using OpenRA.Traits; - -namespace OpenRA.Mods.Common.Traits.Render -{ - [Desc("Displays the player name above the unit")] - class RenderNameTagInfo : ITraitInfo, Requires - { - public readonly int MaxLength = 10; - - public readonly string Font = "TinyBold"; - - public object Create(ActorInitializer init) { return new RenderNameTag(init.Self, this); } - } - - class RenderNameTag : IRenderAnnotations - { - readonly SpriteFont font; - readonly Color color; - readonly string name; - readonly IDecorationBounds[] decorationBounds; - - public RenderNameTag(Actor self, RenderNameTagInfo info) - { - font = Game.Renderer.Fonts[info.Font]; - color = self.Owner.Color; - - if (self.Owner.PlayerName.Length > info.MaxLength) - name = self.Owner.PlayerName.Substring(0, info.MaxLength); - else - name = self.Owner.PlayerName; - - decorationBounds = self.TraitsImplementing().ToArray(); - } - - IEnumerable IRenderAnnotations.RenderAnnotations(Actor self, WorldRenderer wr) - { - if (self.World.FogObscures(self)) - yield break; - - var bounds = decorationBounds.FirstNonEmptyBounds(self, wr); - var spaceBuffer = (int)(10 / wr.Viewport.Zoom); - var effectPos = wr.ProjectedPosition(new int2((bounds.Left + bounds.Right) / 2, bounds.Y - spaceBuffer)); - - yield return new TextAnnotationRenderable(font, effectPos, 0, color, name); - } - - bool IRenderAnnotations.SpatiallyPartitionable { get { return false; } } - } -} diff --git a/OpenRA.Mods.Common/Traits/Render/SelectionDecorations.cs b/OpenRA.Mods.Common/Traits/Render/SelectionDecorations.cs index 179660414b..d632b489c9 100644 --- a/OpenRA.Mods.Common/Traits/Render/SelectionDecorations.cs +++ b/OpenRA.Mods.Common/Traits/Render/SelectionDecorations.cs @@ -18,160 +18,49 @@ using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits.Render { - public class SelectionDecorationsInfo : ITraitInfo, Requires + public class SelectionDecorationsInfo : SelectionDecorationsBaseInfo, Requires { - [PaletteReference] - public readonly string Palette = "chrome"; - - [Desc("Health bar, production progress bar etc.")] - public readonly bool RenderSelectionBars = true; - - public readonly bool RenderSelectionBox = true; - - public readonly Color SelectionBoxColor = Color.White; - - public readonly string Image = "pips"; - - public object Create(ActorInitializer init) { return new SelectionDecorations(init.Self, this); } + public override object Create(ActorInitializer init) { return new SelectionDecorations(init.Self, this); } } - public class SelectionDecorations : ISelectionDecorations, IRenderAnnotations, INotifyCreated, ITick + public class SelectionDecorations : SelectionDecorationsBase { - // depends on the order of pips in TraitsInterfaces.cs! - static readonly string[] PipStrings = { "pip-empty", "pip-green", "pip-yellow", "pip-red", "pip-gray", "pip-blue", "pip-ammo", "pip-ammoempty" }; - - public readonly SelectionDecorationsInfo Info; - - readonly IDecorationBounds[] decorationBounds; - readonly Animation pipImages; - IPips[] pipSources; + readonly Interactable interactable; public SelectionDecorations(Actor self, SelectionDecorationsInfo info) + : base(info) { - Info = info; - - decorationBounds = self.TraitsImplementing().ToArray(); - pipImages = new Animation(self.World, Info.Image); + interactable = self.Trait(); } - void INotifyCreated.Created(Actor self) + protected override int2 GetDecorationPosition(Actor self, WorldRenderer wr, DecorationPosition pos) { - pipSources = self.TraitsImplementing().ToArray(); - } - - IEnumerable ActivityTargetPath(Actor self) - { - if (!self.IsInWorld || self.IsDead) - yield break; - - var activity = self.CurrentActivity; - if (activity != null) + var bounds = interactable.DecorationBounds(self, wr); + switch (pos) { - var targets = activity.GetTargets(self); - yield return self.CenterPosition; - - foreach (var t in targets.Where(t => t.Type != TargetType.Invalid)) - yield return t.CenterPosition; + case DecorationPosition.TopLeft: return bounds.TopLeft; + case DecorationPosition.TopRight: return bounds.TopRight; + case DecorationPosition.BottomLeft: return bounds.BottomLeft; + case DecorationPosition.BottomRight: return bounds.BottomRight; + case DecorationPosition.Top: return new int2(bounds.Left + bounds.Size.Width / 2, bounds.Top); + default: return bounds.TopLeft + new int2(bounds.Size.Width / 2, bounds.Size.Height / 2); } } - IEnumerable IRenderAnnotations.RenderAnnotations(Actor self, WorldRenderer wr) + protected override IEnumerable RenderSelectionBox(Actor self, WorldRenderer wr, Color color) { - if (self.World.FogObscures(self)) - return Enumerable.Empty(); - - return DrawDecorations(self, wr); + var bounds = interactable.DecorationBounds(self, wr); + yield return new SelectionBoxAnnotationRenderable(self, bounds, color); } - bool IRenderAnnotations.SpatiallyPartitionable { get { return true; } } - - IEnumerable DrawDecorations(Actor self, WorldRenderer wr) + protected override IEnumerable RenderSelectionBars(Actor self, WorldRenderer wr, bool displayHealth, bool displayExtra) { - var selected = self.World.Selection.Contains(self); - var regularWorld = self.World.Type == WorldType.Regular; - var statusBars = Game.Settings.Game.StatusBars; - var bounds = decorationBounds.FirstNonEmptyBounds(self, wr); - - // Health bars are shown when: - // * actor is selected - // * status bar preference is set to "always show" - // * status bar preference is set to "when damaged" and actor is damaged - var displayHealth = selected || (regularWorld && statusBars == StatusBarsType.AlwaysShow) - || (regularWorld && statusBars == StatusBarsType.DamageShow && self.GetDamageState() != DamageState.Undamaged); - - // Extra bars are shown when: - // * actor is selected - // * status bar preference is set to "always show" - // * status bar preference is set to "when damaged" - var displayExtra = selected || (regularWorld && statusBars != StatusBarsType.Standard); - - if (Info.RenderSelectionBox && selected) - yield return new SelectionBoxAnnotationRenderable(self, bounds, Info.SelectionBoxColor); - - if (Info.RenderSelectionBars && (displayHealth || displayExtra)) - yield return new SelectionBarsAnnotationRenderable(self, bounds, displayHealth, displayExtra); - - // Target lines and pips are always only displayed for selected allied actors - if (!selected || !self.Owner.IsAlliedWith(wr.World.RenderPlayer)) + // Don't render the selection bars for non-selectable actors + if (!(interactable is Selectable) || (!displayHealth && !displayExtra)) yield break; - if (self.World.LocalPlayer != null && self.World.LocalPlayer.PlayerActor.Trait().PathDebug) - yield return new TargetLineRenderable(ActivityTargetPath(self), Color.Green); - - foreach (var r in DrawPips(self, bounds, wr)) - yield return r; - } - - public void DrawRollover(Actor self, WorldRenderer worldRenderer) - { - var bounds = decorationBounds.FirstNonEmptyBounds(self, worldRenderer); - new SelectionBarsAnnotationRenderable(self, bounds, true, true).Render(worldRenderer); - } - - IEnumerable DrawPips(Actor self, Rectangle bounds, WorldRenderer wr) - { - if (pipSources.Length == 0) - return Enumerable.Empty(); - - return DrawPipsInner(self, bounds, wr); - } - - IEnumerable DrawPipsInner(Actor self, Rectangle bounds, WorldRenderer wr) - { - pipImages.PlayRepeating(PipStrings[0]); - - var palette = wr.Palette(Info.Palette); - var basePosition = wr.Viewport.WorldToViewPx(new int2(bounds.Left, bounds.Bottom)); - var pipSize = pipImages.Image.Size.XY.ToInt2(); - var pipxyBase = basePosition + new int2(1 - pipSize.X / 2, -(3 + pipSize.Y / 2)); - var pipxyOffset = new int2(0, 0); - var width = bounds.Width; - - foreach (var pips in pipSources) - { - var thisRow = pips.GetPips(self); - if (thisRow == null) - continue; - - foreach (var pip in thisRow) - { - if (pipxyOffset.X + pipSize.X >= width) - pipxyOffset = new int2(0, pipxyOffset.Y - pipSize.Y); - - pipImages.PlayRepeating(PipStrings[(int)pip]); - pipxyOffset += new int2(pipSize.X, 0); - - yield return new UISpriteRenderable(pipImages.Image, self.CenterPosition, pipxyBase + pipxyOffset, 0, palette, 1f); - } - - // Increment row - pipxyOffset = new int2(0, pipxyOffset.Y - (pipSize.Y + 1)); - } - } - - void ITick.Tick(Actor self) - { - pipImages.Tick(); + var bounds = interactable.DecorationBounds(self, wr); + yield return new SelectionBarsAnnotationRenderable(self, bounds, displayHealth, displayExtra); } } } diff --git a/OpenRA.Mods.Common/Traits/Render/SelectionDecorationsBase.cs b/OpenRA.Mods.Common/Traits/Render/SelectionDecorationsBase.cs new file mode 100644 index 0000000000..61118efdf6 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Render/SelectionDecorationsBase.cs @@ -0,0 +1,145 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 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.Collections.Generic; +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits.Render +{ + public abstract class SelectionDecorationsBaseInfo : ITraitInfo + { + public readonly Color SelectionBoxColor = Color.White; + + public abstract object Create(ActorInitializer init); + } + + public abstract class SelectionDecorationsBase : ISelectionDecorations, IRenderAnnotations, INotifyCreated + { + Dictionary decorations; + Dictionary selectedDecorations; + + protected readonly SelectionDecorationsBaseInfo info; + + public SelectionDecorationsBase(SelectionDecorationsBaseInfo info) + { + this.info = info; + } + + void INotifyCreated.Created(Actor self) + { + var groupedDecorations = new Dictionary>(); + var groupedSelectionDecorations = new Dictionary>(); + foreach (var d in self.TraitsImplementing()) + { + groupedSelectionDecorations.GetOrAdd(d.Position).Add(d); + if (!d.RequiresSelection) + groupedDecorations.GetOrAdd(d.Position).Add(d); + } + + decorations = groupedDecorations.ToDictionary( + d => d.Key, + d => d.Value.ToArray()); + + selectedDecorations = groupedSelectionDecorations.ToDictionary( + d => d.Key, + d => d.Value.ToArray()); + } + + IEnumerable ActivityTargetPath(Actor self) + { + if (!self.IsInWorld || self.IsDead) + yield break; + + var activity = self.CurrentActivity; + if (activity != null) + { + var targets = activity.GetTargets(self); + yield return self.CenterPosition; + + foreach (var t in targets.Where(t => t.Type != TargetType.Invalid)) + yield return t.CenterPosition; + } + } + + IEnumerable IRenderAnnotations.RenderAnnotations(Actor self, WorldRenderer wr) + { + if (self.World.FogObscures(self)) + return Enumerable.Empty(); + + return DrawDecorations(self, wr); + } + + bool IRenderAnnotations.SpatiallyPartitionable { get { return true; } } + + IEnumerable DrawDecorations(Actor self, WorldRenderer wr) + { + var selected = self.World.Selection.Contains(self); + var regularWorld = self.World.Type == WorldType.Regular; + var statusBars = Game.Settings.Game.StatusBars; + + // Health bars are shown when: + // * actor is selected + // * status bar preference is set to "always show" + // * status bar preference is set to "when damaged" and actor is damaged + var displayHealth = selected || (regularWorld && statusBars == StatusBarsType.AlwaysShow) + || (regularWorld && statusBars == StatusBarsType.DamageShow && self.GetDamageState() != DamageState.Undamaged); + + // Extra bars are shown when: + // * actor is selected + // * status bar preference is set to "always show" or "when damaged" + var displayExtra = selected || (regularWorld && statusBars != StatusBarsType.Standard); + + if (selected) + foreach (var r in RenderSelectionBox(self, wr, info.SelectionBoxColor)) + yield return r; + + if (displayHealth || displayExtra) + foreach (var r in RenderSelectionBars(self, wr, displayHealth, displayExtra)) + yield return r; + + var renderDecorations = self.World.Selection.Contains(self) ? selectedDecorations : decorations; + foreach (var kv in renderDecorations) + { + var pos = GetDecorationPosition(self, wr, kv.Key); + foreach (var r in kv.Value) + foreach (var rr in r.RenderDecoration(self, wr, pos)) + yield return rr; + } + + // Target lines and pips are always only displayed for selected allied actors + if (!selected || !self.Owner.IsAlliedWith(wr.World.RenderPlayer)) + yield break; + + if (self.World.LocalPlayer != null && self.World.LocalPlayer.PlayerActor.Trait().PathDebug) + yield return new TargetLineRenderable(ActivityTargetPath(self), Color.Green); + } + + IEnumerable ISelectionDecorations.RenderRolloverAnnotations(Actor self, WorldRenderer worldRenderer) + { + if (self.World.Selection.Contains(self)) + return Enumerable.Empty(); + + return RenderSelectionBars(self, worldRenderer, true, true); + } + + IEnumerable ISelectionDecorations.RenderSelectionAnnotations(Actor self, WorldRenderer worldRenderer, Color color) + { + return RenderSelectionBox(self, worldRenderer, color); + } + + protected abstract int2 GetDecorationPosition(Actor self, WorldRenderer wr, DecorationPosition pos); + protected abstract IEnumerable RenderSelectionBox(Actor self, WorldRenderer wr, Color color); + protected abstract IEnumerable RenderSelectionBars(Actor self, WorldRenderer wr, bool displayHealth, bool displayExtra); + } +} diff --git a/OpenRA.Mods.Common/Traits/Render/VeteranProductionIconOverlay.cs b/OpenRA.Mods.Common/Traits/Render/VeteranProductionIconOverlay.cs index cb45396d03..565f1abb90 100644 --- a/OpenRA.Mods.Common/Traits/Render/VeteranProductionIconOverlay.cs +++ b/OpenRA.Mods.Common/Traits/Render/VeteranProductionIconOverlay.cs @@ -32,10 +32,6 @@ namespace OpenRA.Mods.Common.Traits.Render [Desc("Palette to render the sprite in. Reference the world actor's PaletteFrom* traits.")] public readonly string Palette = "chrome"; - [Desc("Point on the production icon's used as reference for offsetting the overlay. ", - "Possible values are combinations of Center, Top, Bottom, Left, Right.")] - public readonly ReferencePoints ReferencePoint = ReferencePoints.Top | ReferencePoints.Left; - public object Create(ActorInitializer init) { return new VeteranProductionIconOverlay(init, this); } } @@ -77,18 +73,8 @@ namespace OpenRA.Mods.Common.Traits.Render string IProductionIconOverlay.Palette { get { return info.Palette; } } float2 IProductionIconOverlay.Offset(float2 iconSize) { - float x = 0; - float y = 0; - if (info.ReferencePoint.HasFlag(ReferencePoints.Top)) - y -= iconSize.Y / 2 - sprite.Size.Y / 2; - else if (info.ReferencePoint.HasFlag(ReferencePoints.Bottom)) - y += iconSize.Y / 2 - sprite.Size.Y / 2; - - if (info.ReferencePoint.HasFlag(ReferencePoints.Left)) - x -= iconSize.X / 2 - sprite.Size.X / 2; - else if (info.ReferencePoint.HasFlag(ReferencePoints.Right)) - x += iconSize.X / 2 - sprite.Size.X / 2; - + var x = (sprite.Size.X - iconSize.X) / 2; + var y = (sprite.Size.Y - iconSize.Y) / 2; return new float2(x, y); } diff --git a/OpenRA.Mods.Common/Traits/Render/WithAmmoPipsDecoration.cs b/OpenRA.Mods.Common/Traits/Render/WithAmmoPipsDecoration.cs new file mode 100644 index 0000000000..cfd40c322e --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Render/WithAmmoPipsDecoration.cs @@ -0,0 +1,83 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 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.Collections.Generic; +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits.Render +{ + public class WithAmmoPipsDecorationInfo : WithDecorationBaseInfo, Requires + { + [Desc("Number of pips to display. Defaults to the sum of the enabled AmmoPool.Ammo.")] + public readonly int PipCount = -1; + + [Desc("If non-zero, override the spacing between adjacent pips.")] + public readonly int2 PipStride = int2.Zero; + + [Desc("Image that defines the pip sequences.")] + public readonly string Image = "pips"; + + [SequenceReference("Image")] + [Desc("Sequence used for empty pips.")] + public readonly string EmptySequence = "pip-empty"; + + [SequenceReference("Image")] + [Desc("Sequence used for full pips.")] + public readonly string FullSequence = "pip-green"; + + [PaletteReference] + public readonly string Palette = "chrome"; + + public override object Create(ActorInitializer init) { return new WithAmmoPipsDecoration(init.Self, this); } + } + + public class WithAmmoPipsDecoration : WithDecorationBase + { + readonly AmmoPool[] ammo; + readonly Animation pips; + + public WithAmmoPipsDecoration(Actor self, WithAmmoPipsDecorationInfo info) + : base(self, info) + { + ammo = self.TraitsImplementing().ToArray(); + pips = new Animation(self.World, info.Image); + } + + protected override IEnumerable RenderDecoration(Actor self, WorldRenderer wr, int2 screenPos) + { + pips.PlayRepeating(Info.EmptySequence); + + var palette = wr.Palette(Info.Palette); + var pipSize = pips.Image.Size.XY.ToInt2(); + var pipStride = Info.PipStride != int2.Zero ? Info.PipStride : new int2(pipSize.X, 0); + screenPos -= pipSize / 2; + + var currentAmmo = 0; + var totalAmmo = 0; + foreach (var a in ammo) + { + currentAmmo += a.CurrentAmmoCount; + totalAmmo += a.Info.Ammo; + } + + var pipCount = Info.PipCount > 0 ? Info.PipCount : totalAmmo; + for (var i = 0; i < pipCount; i++) + { + pips.PlayRepeating(currentAmmo * pipCount > i * totalAmmo ? Info.FullSequence : Info.EmptySequence); + yield return new UISpriteRenderable(pips.Image, self.CenterPosition, screenPos, 0, palette, 1f); + + screenPos += pipStride; + } + } + } +} diff --git a/OpenRA.Mods.Common/Traits/Render/WithCargoPipsDecoration.cs b/OpenRA.Mods.Common/Traits/Render/WithCargoPipsDecoration.cs new file mode 100644 index 0000000000..bd10e022bf --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Render/WithCargoPipsDecoration.cs @@ -0,0 +1,101 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 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.Collections.Generic; +using OpenRA.Graphics; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits.Render +{ + public class WithCargoPipsDecorationInfo : WithDecorationBaseInfo, Requires + { + [Desc("Number of pips to display. Defaults to Cargo.MaxWeight.")] + public readonly int PipCount = -1; + + [Desc("If non-zero, override the spacing between adjacent pips.")] + public readonly int2 PipStride = int2.Zero; + + [Desc("Image that defines the pip sequences.")] + public readonly string Image = "pips"; + + [SequenceReference("Image")] + [Desc("Sequence used for empty pips.")] + public readonly string EmptySequence = "pip-empty"; + + [SequenceReference("Image")] + [Desc("Sequence used for full pips that aren't defined in CustomPipSequences.")] + public readonly string FullSequence = "pip-green"; + + // TODO: [SequenceReference] isn't smart enough to use Dictionaries. + [Desc("Pip sequence to use for specific passenger actors.")] + public readonly Dictionary CustomPipSequences = new Dictionary(); + + [PaletteReference] + public readonly string Palette = "chrome"; + + public override object Create(ActorInitializer init) { return new WithCargoPipsDecoration(init.Self, this); } + } + + public class WithCargoPipsDecoration : WithDecorationBase + { + readonly Cargo cargo; + readonly Animation pips; + readonly int pipCount; + + public WithCargoPipsDecoration(Actor self, WithCargoPipsDecorationInfo info) + : base(self, info) + { + cargo = self.Trait(); + pipCount = info.PipCount > 0 ? info.PipCount : cargo.Info.MaxWeight; + pips = new Animation(self.World, info.Image); + } + + string GetPipSequence(int i) + { + var n = i * cargo.Info.MaxWeight / pipCount; + + foreach (var c in cargo.Passengers) + { + var pi = c.Info.TraitInfo(); + if (n < pi.Weight) + { + var sequence = Info.FullSequence; + if (pi.CustomPipType != null && !Info.CustomPipSequences.TryGetValue(pi.CustomPipType, out sequence)) + Log.Write("debug", "Actor type {0} defines a custom pip type {1} that is not defined for actor type {2}".F(c.Info.Name, pi.CustomPipType, self.Info.Name)); + + return sequence; + } + + n -= pi.Weight; + } + + return Info.EmptySequence; + } + + protected override IEnumerable RenderDecoration(Actor self, WorldRenderer wr, int2 screenPos) + { + pips.PlayRepeating(Info.EmptySequence); + + var palette = wr.Palette(Info.Palette); + var pipSize = pips.Image.Size.XY.ToInt2(); + var pipStride = Info.PipStride != int2.Zero ? Info.PipStride : new int2(pipSize.X, 0); + + screenPos -= pipSize / 2; + for (var i = 0; i < pipCount; i++) + { + pips.PlayRepeating(GetPipSequence(i)); + yield return new UISpriteRenderable(pips.Image, self.CenterPosition, screenPos, 0, palette, 1f); + + screenPos += pipStride; + } + } + } +} diff --git a/OpenRA.Mods.Common/Traits/Render/WithDecoration.cs b/OpenRA.Mods.Common/Traits/Render/WithDecoration.cs index 0a36a9014c..bf52283938 100644 --- a/OpenRA.Mods.Common/Traits/Render/WithDecoration.cs +++ b/OpenRA.Mods.Common/Traits/Render/WithDecoration.cs @@ -18,24 +18,15 @@ using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits.Render { - [Flags] - public enum ReferencePoints - { - Center = 0, - Top = 1, - Bottom = 2, - Left = 4, - Right = 8, - } - - public enum BlinkState { Off, On } - [Desc("Displays a custom UI overlay relative to the actor's mouseover bounds.")] - public class WithDecorationInfo : ConditionalTraitInfo, Requires + public class WithDecorationInfo : WithDecorationBaseInfo { + [FieldLoader.Require] [Desc("Image used for this decoration. Defaults to the actor's type.")] public readonly string Image = null; + [FieldLoader.Require] + [SequenceReference("Image")] [Desc("Sequence used for this decoration (can be animated).")] public readonly string Sequence = null; @@ -46,77 +37,20 @@ namespace OpenRA.Mods.Common.Traits.Render [Desc("Custom palette is a player palette BaseName")] public readonly bool IsPlayerPalette = false; - [Desc("Point in the actor's selection box used as reference for offsetting the decoration image. " + - "Possible values are combinations of Center, Top, Bottom, Left, Right.")] - public readonly ReferencePoints ReferencePoint = ReferencePoints.Top | ReferencePoints.Left; - - [Desc("The Z offset to apply when rendering this decoration.")] - public readonly int ZOffset = 1; - - [Desc("Player stances who can view the decoration.")] - public readonly Stance ValidStances = Stance.Ally; - - [Desc("Should this be visible only when selected?")] - public readonly bool RequiresSelection = false; - - [Desc("Screen-space offsets to apply when defined conditions are enabled.", - "A dictionary of [condition string]: [x, y offset].")] - public readonly Dictionary Offsets = new Dictionary(); - - [Desc("The number of ticks that each step in the blink pattern in active.")] - public readonly int BlinkInterval = 5; - - [Desc("A pattern of ticks (BlinkInterval long) where the decoration is visible or hidden.")] - public readonly BlinkState[] BlinkPattern = { }; - - [Desc("Override blink conditions to use when defined conditions are enabled.", - "A dictionary of [condition string]: [pattern].")] - public readonly Dictionary BlinkPatterns = new Dictionary(); - - [ConsumedConditionReference] - public IEnumerable ConsumedConditions - { - get { return Offsets.Keys.Concat(BlinkPatterns.Keys).SelectMany(r => r.Variables).Distinct(); } - } - public override object Create(ActorInitializer init) { return new WithDecoration(init.Self, this); } } - public class WithDecoration : ConditionalTrait, ITick, IRenderAnnotations, IRenderAnnotationsWhenSelected + public class WithDecoration : WithDecorationBase, ITick { protected Animation anim; - readonly IDecorationBounds[] decorationBounds; readonly string image; - int2 conditionalOffset; - BlinkState[] blinkPattern; public WithDecoration(Actor self, WithDecorationInfo info) - : base(info) + : base(self, info) { image = info.Image ?? self.Info.Name; anim = new Animation(self.World, image, () => self.World.Paused); anim.PlayRepeating(info.Sequence); - decorationBounds = self.TraitsImplementing().ToArray(); - blinkPattern = info.BlinkPattern; - } - - protected virtual bool ShouldRender(Actor self) - { - if (blinkPattern != null && blinkPattern.Any()) - { - var i = (self.World.WorldTick / Info.BlinkInterval) % blinkPattern.Length; - if (blinkPattern[i] != BlinkState.On) - return false; - } - - if (self.World.RenderPlayer != null) - { - var stance = self.Owner.Stances[self.World.RenderPlayer]; - if (!Info.ValidStances.HasStance(stance)) - return false; - } - - return true; } protected virtual PaletteReference GetPalette(Actor self, WorldRenderer wr) @@ -124,99 +58,17 @@ namespace OpenRA.Mods.Common.Traits.Render return wr.Palette(Info.Palette + (Info.IsPlayerPalette ? self.Owner.InternalName : "")); } - IEnumerable IRenderAnnotations.RenderAnnotations(Actor self, WorldRenderer wr) + protected override IEnumerable RenderDecoration(Actor self, WorldRenderer wr, int2 screenPos) { - return !Info.RequiresSelection ? RenderInner(self, wr) : SpriteRenderable.None; - } - - IEnumerable IRenderAnnotationsWhenSelected.RenderAnnotations(Actor self, WorldRenderer wr) - { - return Info.RequiresSelection ? RenderInner(self, wr) : SpriteRenderable.None; - } - - bool IRenderAnnotations.SpatiallyPartitionable { get { return true; } } - bool IRenderAnnotationsWhenSelected.SpatiallyPartitionable { get { return true; } } - - IEnumerable RenderInner(Actor self, WorldRenderer wr) - { - if (IsTraitDisabled || self.IsDead || !self.IsInWorld || anim == null) + if (anim == null) return Enumerable.Empty(); - if (!ShouldRender(self) || self.World.FogObscures(self)) - return Enumerable.Empty(); - - var bounds = decorationBounds.FirstNonEmptyBounds(self, wr); - var halfSize = (0.5f * anim.Image.Size.XY).ToInt2(); - - var boundsOffset = new int2(bounds.Left + bounds.Right, bounds.Top + bounds.Bottom) / 2; - var sizeOffset = -halfSize; - if (Info.ReferencePoint.HasFlag(ReferencePoints.Top)) - { - boundsOffset -= new int2(0, bounds.Height / 2); - sizeOffset += new int2(0, halfSize.Y); - } - else if (Info.ReferencePoint.HasFlag(ReferencePoints.Bottom)) - { - boundsOffset += new int2(0, bounds.Height / 2); - sizeOffset -= new int2(0, halfSize.Y); - } - - if (Info.ReferencePoint.HasFlag(ReferencePoints.Left)) - { - boundsOffset -= new int2(bounds.Width / 2, 0); - sizeOffset += new int2(halfSize.X, 0); - } - else if (Info.ReferencePoint.HasFlag(ReferencePoints.Right)) - { - boundsOffset += new int2(bounds.Width / 2, 0); - sizeOffset -= new int2(halfSize.X, 0); - } - - var pxPos = wr.Viewport.WorldToViewPx(boundsOffset) + sizeOffset + conditionalOffset; return new IRenderable[] { - new UISpriteRenderable(anim.Image, self.CenterPosition, pxPos, Info.ZOffset, GetPalette(self, wr), 1f) + new UISpriteRenderable(anim.Image, self.CenterPosition, screenPos - (0.5f * anim.Image.Size.XY).ToInt2(), 0, GetPalette(self, wr), 1f) }; } void ITick.Tick(Actor self) { anim.Tick(); } - - public override IEnumerable GetVariableObservers() - { - foreach (var observer in base.GetVariableObservers()) - yield return observer; - - foreach (var condition in Info.Offsets.Keys) - yield return new VariableObserver(OffsetConditionChanged, condition.Variables); - - foreach (var condition in Info.BlinkPatterns.Keys) - yield return new VariableObserver(BlinkConditionsChanged, condition.Variables); - } - - void OffsetConditionChanged(Actor self, IReadOnlyDictionary conditions) - { - conditionalOffset = int2.Zero; - foreach (var kv in Info.Offsets) - { - if (kv.Key.Evaluate(conditions)) - { - conditionalOffset = kv.Value; - break; - } - } - } - - void BlinkConditionsChanged(Actor self, IReadOnlyDictionary conditions) - { - blinkPattern = Info.BlinkPattern; - foreach (var kv in Info.BlinkPatterns) - { - if (kv.Key.Evaluate(conditions)) - { - blinkPattern = kv.Value; - return; - } - } - } } } diff --git a/OpenRA.Mods.Common/Traits/Render/WithDecorationBase.cs b/OpenRA.Mods.Common/Traits/Render/WithDecorationBase.cs new file mode 100644 index 0000000000..4081dfbca7 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Render/WithDecorationBase.cs @@ -0,0 +1,147 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 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.Collections.Generic; +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Support; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits.Render +{ + public enum BlinkState { Off, On } + + public abstract class WithDecorationBaseInfo : ConditionalTraitInfo + { + [Desc("Position in the actor's selection box to draw the decoration.")] + public readonly DecorationPosition Position = DecorationPosition.TopLeft; + + [Desc("Player stances who can view the decoration.")] + public readonly Stance ValidStances = Stance.Ally; + + [Desc("Should this be visible only when selected?")] + public readonly bool RequiresSelection = false; + + [Desc("Offset sprite center position from the selection box edge.")] + public readonly int2 Margin = int2.Zero; + + [Desc("Screen-space offsets to apply when defined conditions are enabled.", + "A dictionary of [condition string]: [x, y offset].")] + public readonly Dictionary Offsets = new Dictionary(); + + [Desc("The number of ticks that each step in the blink pattern in active.")] + public readonly int BlinkInterval = 5; + + [Desc("A pattern of ticks (BlinkInterval long) where the decoration is visible or hidden.")] + public readonly BlinkState[] BlinkPattern = { }; + + [Desc("Override blink conditions to use when defined conditions are enabled.", + "A dictionary of [condition string]: [pattern].")] + public readonly Dictionary BlinkPatterns = new Dictionary(); + + [ConsumedConditionReference] + public IEnumerable ConsumedConditions + { + get { return Offsets.Keys.Concat(BlinkPatterns.Keys).SelectMany(r => r.Variables).Distinct(); } + } + } + + public abstract class WithDecorationBase : ConditionalTrait, IDecoration where InfoType : WithDecorationBaseInfo + { + protected readonly Actor self; + int2 conditionalOffset; + BlinkState[] blinkPattern; + + public WithDecorationBase(Actor self, InfoType info) + : base(info) + { + this.self = self; + blinkPattern = info.BlinkPattern; + } + + protected virtual bool ShouldRender(Actor self) + { + if (self.World.FogObscures(self)) + return false; + + if (blinkPattern != null && blinkPattern.Any()) + { + var i = (self.World.WorldTick / Info.BlinkInterval) % blinkPattern.Length; + if (blinkPattern[i] != BlinkState.On) + return false; + } + + if (self.World.RenderPlayer != null) + { + var stance = self.Owner.Stances[self.World.RenderPlayer]; + if (!Info.ValidStances.HasStance(stance)) + return false; + } + + return true; + } + + DecorationPosition IDecoration.Position { get { return Info.Position; } } + + bool IDecoration.Enabled { get { return !IsTraitDisabled && self.IsInWorld && ShouldRender(self); } } + + bool IDecoration.RequiresSelection { get { return Info.RequiresSelection; } } + + protected abstract IEnumerable RenderDecoration(Actor self, WorldRenderer wr, int2 pos); + + IEnumerable IDecoration.RenderDecoration(Actor self, WorldRenderer wr, int2 pos) + { + if (IsTraitDisabled || self.IsDead || !self.IsInWorld || !ShouldRender(self)) + return Enumerable.Empty(); + + var screenPos = wr.Viewport.WorldToViewPx(pos) + Info.Position.CreateMargin(Info.Margin) + conditionalOffset; + return RenderDecoration(self, wr, screenPos); + } + + public override IEnumerable GetVariableObservers() + { + foreach (var observer in base.GetVariableObservers()) + yield return observer; + + foreach (var condition in Info.Offsets.Keys) + yield return new VariableObserver(OffsetConditionChanged, condition.Variables); + + foreach (var condition in Info.BlinkPatterns.Keys) + yield return new VariableObserver(BlinkConditionsChanged, condition.Variables); + } + + void OffsetConditionChanged(Actor self, IReadOnlyDictionary conditions) + { + conditionalOffset = int2.Zero; + foreach (var kv in Info.Offsets) + { + if (kv.Key.Evaluate(conditions)) + { + conditionalOffset = kv.Value; + break; + } + } + } + + void BlinkConditionsChanged(Actor self, IReadOnlyDictionary conditions) + { + blinkPattern = Info.BlinkPattern; + foreach (var kv in Info.BlinkPatterns) + { + if (kv.Key.Evaluate(conditions)) + { + blinkPattern = kv.Value; + return; + } + } + } + } +} diff --git a/OpenRA.Mods.Common/Traits/Render/WithHarvesterPipsDecoration.cs b/OpenRA.Mods.Common/Traits/Render/WithHarvesterPipsDecoration.cs new file mode 100644 index 0000000000..e1351ea111 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Render/WithHarvesterPipsDecoration.cs @@ -0,0 +1,99 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 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.Collections.Generic; +using OpenRA.Graphics; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits.Render +{ + public class WithHarvesterPipsDecorationInfo : WithDecorationBaseInfo, Requires + { + [FieldLoader.Require] + [Desc("Number of pips to display how filled unit is.")] + public readonly int PipCount = 0; + + [Desc("If non-zero, override the spacing between adjacent pips.")] + public readonly int2 PipStride = int2.Zero; + + [Desc("Image that defines the pip sequences.")] + public readonly string Image = "pips"; + + [SequenceReference("Image")] + [Desc("Sequence used for empty pips.")] + public readonly string EmptySequence = "pip-empty"; + + [SequenceReference("Image")] + [Desc("Sequence used for full pips that aren't defined in ResourceSequences.")] + public readonly string FullSequence = "pip-green"; + + // TODO: [SequenceReference] isn't smart enough to use Dictionaries. + [Desc("Pip sequence to use for specific resource types.")] + public readonly Dictionary ResourceSequences = new Dictionary(); + + [PaletteReference] + public readonly string Palette = "chrome"; + + public override object Create(ActorInitializer init) { return new WithHarvesterPipsDecoration(init.Self, this); } + } + + public class WithHarvesterPipsDecoration : WithDecorationBase + { + readonly Harvester harvester; + readonly Animation pips; + + public WithHarvesterPipsDecoration(Actor self, WithHarvesterPipsDecorationInfo info) + : base(self, info) + { + harvester = self.Trait(); + pips = new Animation(self.World, info.Image); + } + + string GetPipSequence(int i) + { + var n = i * harvester.Info.Capacity / Info.PipCount; + + string sequence; + foreach (var rt in harvester.Contents) + { + if (n < rt.Value) + { + if (!Info.ResourceSequences.TryGetValue(rt.Key.Type, out sequence)) + sequence = Info.FullSequence; + + return sequence; + } + + n -= rt.Value; + } + + return Info.EmptySequence; + } + + protected override IEnumerable RenderDecoration(Actor self, WorldRenderer wr, int2 screenPos) + { + pips.PlayRepeating(Info.EmptySequence); + + var palette = wr.Palette(Info.Palette); + var pipSize = pips.Image.Size.XY.ToInt2(); + var pipStride = Info.PipStride != int2.Zero ? Info.PipStride : new int2(pipSize.X, 0); + + screenPos -= pipSize / 2; + for (var i = 0; i < Info.PipCount; i++) + { + pips.PlayRepeating(GetPipSequence(i)); + yield return new UISpriteRenderable(pips.Image, self.CenterPosition, screenPos, 0, palette, 1f); + + screenPos += pipStride; + } + } + } +} diff --git a/OpenRA.Mods.Common/Traits/Render/WithNameTagDecoration.cs b/OpenRA.Mods.Common/Traits/Render/WithNameTagDecoration.cs new file mode 100644 index 0000000000..f978dc2e1d --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Render/WithNameTagDecoration.cs @@ -0,0 +1,84 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 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.Collections.Generic; +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Graphics; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits.Render +{ + [Desc("Displays the player name above the unit")] + public class WithNameTagDecorationInfo : WithDecorationBaseInfo + { + public readonly int MaxLength = 10; + + public readonly string Font = "TinyBold"; + + [Desc("Display in this color when not using the player color.")] + public readonly Color Color = Color.White; + + [Desc("Use the player color of the current owner.")] + public readonly bool UsePlayerColor = false; + + public override object Create(ActorInitializer init) { return new WithNameTagDecoration(init.Self, this); } + + public override void RulesetLoaded(Ruleset rules, ActorInfo ai) + { + if (!Game.ModData.Manifest.Get().FontList.ContainsKey(Font)) + throw new YamlException("Font '{0}' is not listed in the mod.yaml's Fonts section".F(Font)); + + base.RulesetLoaded(rules, ai); + } + } + + public class WithNameTagDecoration : WithDecorationBase, INotifyOwnerChanged + { + readonly SpriteFont font; + string name; + Color color; + + public WithNameTagDecoration(Actor self, WithNameTagDecorationInfo info) + : base(self, info) + { + font = Game.Renderer.Fonts[info.Font]; + color = info.UsePlayerColor ? self.Owner.Color : info.Color; + + name = self.Owner.PlayerName; + if (name.Length > info.MaxLength) + name = name.Substring(0, info.MaxLength); + } + + protected override IEnumerable RenderDecoration(Actor self, WorldRenderer wr, int2 screenPos) + { + if (IsTraitDisabled || self.IsDead || !self.IsInWorld || !ShouldRender(self)) + return Enumerable.Empty(); + + var size = font.Measure(name); + return new IRenderable[] + { + new UITextRenderable(font, self.CenterPosition, screenPos - size / 2, 0, color, name) + }; + } + + void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + if (Info.UsePlayerColor) + color = newOwner.Color; + + name = self.Owner.PlayerName; + if (name.Length > Info.MaxLength) + name = name.Substring(0, Info.MaxLength); + } + } +} diff --git a/OpenRA.Mods.Common/Traits/Render/WithResourceStoragePipsDecoration.cs b/OpenRA.Mods.Common/Traits/Render/WithResourceStoragePipsDecoration.cs new file mode 100644 index 0000000000..07618e1ff4 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Render/WithResourceStoragePipsDecoration.cs @@ -0,0 +1,80 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 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.Collections.Generic; +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits.Render +{ + public class WithResourceStoragePipsDecorationInfo : WithDecorationBaseInfo + { + [FieldLoader.Require] + [Desc("Number of pips to display how filled unit is.")] + public readonly int PipCount = 0; + + [Desc("If non-zero, override the spacing between adjacing pips.")] + public readonly int2 PipStride = int2.Zero; + + [Desc("Image that defines the pip sequences.")] + public readonly string Image = "pips"; + + [SequenceReference("Image")] + [Desc("Sequence used for empty pips.")] + public readonly string EmptySequence = "pip-empty"; + + [SequenceReference("Image")] + [Desc("Sequence used for full pips.")] + public readonly string FullSequence = "pip-green"; + + [PaletteReference] + public readonly string Palette = "chrome"; + + public override object Create(ActorInitializer init) { return new WithResourceStoragePipsDecoration(init.Self, this); } + } + + public class WithResourceStoragePipsDecoration : WithDecorationBase, INotifyOwnerChanged + { + readonly Animation pips; + PlayerResources player; + + public WithResourceStoragePipsDecoration(Actor self, WithResourceStoragePipsDecorationInfo info) + : base(self, info) + { + player = self.Owner.PlayerActor.Trait(); + pips = new Animation(self.World, info.Image); + } + + protected override IEnumerable RenderDecoration(Actor self, WorldRenderer wr, int2 screenPos) + { + pips.PlayRepeating(Info.EmptySequence); + + var palette = wr.Palette(Info.Palette); + var pipSize = pips.Image.Size.XY.ToInt2(); + var pipStride = Info.PipStride != int2.Zero ? Info.PipStride : new int2(pipSize.X, 0); + + screenPos -= pipSize / 2; + for (var i = 0; i < Info.PipCount; i++) + { + pips.PlayRepeating(player.Resources * Info.PipCount > i * player.ResourceCapacity ? Info.FullSequence : Info.EmptySequence); + yield return new UISpriteRenderable(pips.Image, self.CenterPosition, screenPos, 0, palette, 1f); + + screenPos += pipStride; + } + } + + void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + player = newOwner.PlayerActor.Trait(); + } + } +} diff --git a/OpenRA.Mods.Common/Traits/Render/WithSpriteControlGroupDecoration.cs b/OpenRA.Mods.Common/Traits/Render/WithSpriteControlGroupDecoration.cs index 48e30697b1..cc7c19efb7 100644 --- a/OpenRA.Mods.Common/Traits/Render/WithSpriteControlGroupDecoration.cs +++ b/OpenRA.Mods.Common/Traits/Render/WithSpriteControlGroupDecoration.cs @@ -17,7 +17,7 @@ using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits.Render { [Desc("Renders Ctrl groups using pixel art.")] - public class WithSpriteControlGroupDecorationInfo : ITraitInfo, Requires + public class WithSpriteControlGroupDecorationInfo : ITraitInfo { [PaletteReference] public readonly string Palette = "chrome"; @@ -28,66 +28,49 @@ namespace OpenRA.Mods.Common.Traits.Render [Desc("Sprite sequence used to render the control group 0-9 numbers.")] public readonly string GroupSequence = "groups"; - [Desc("Point in the actor's selection box used as reference for offsetting the decoration image. " + - "Possible values are combinations of Center, Top, Bottom, Left, Right.")] - public readonly ReferencePoints ReferencePoint = ReferencePoints.Top | ReferencePoints.Left; + [Desc("Position in the actor's selection box to draw the decoration.")] + public readonly DecorationPosition Position = DecorationPosition.TopLeft; + + [Desc("Offset sprite center position from the selection box edge.")] + public readonly int2 Margin = int2.Zero; public object Create(ActorInitializer init) { return new WithSpriteControlGroupDecoration(init.Self, this); } } - public class WithSpriteControlGroupDecoration : IRenderAnnotationsWhenSelected + public class WithSpriteControlGroupDecoration : IDecoration { public readonly WithSpriteControlGroupDecorationInfo Info; - readonly IDecorationBounds[] decorationBounds; - readonly Animation pipImages; + readonly Actor self; + readonly Animation anim; public WithSpriteControlGroupDecoration(Actor self, WithSpriteControlGroupDecorationInfo info) { Info = info; + this.self = self; - decorationBounds = self.TraitsImplementing().ToArray(); - pipImages = new Animation(self.World, Info.Image); + anim = new Animation(self.World, Info.Image); } - IEnumerable IRenderAnnotationsWhenSelected.RenderAnnotations(Actor self, WorldRenderer wr) - { - if (self.Owner != wr.World.LocalPlayer) - yield break; + DecorationPosition IDecoration.Position { get { return Info.Position; } } - if (self.World.FogObscures(self)) - yield break; + bool IDecoration.Enabled { get { return self.Owner == self.World.LocalPlayer && self.World.Selection.GetControlGroupForActor(self) != null; } } - var pal = wr.Palette(Info.Palette); - foreach (var r in DrawControlGroup(self, wr, pal)) - yield return r; - } + bool IDecoration.RequiresSelection { get { return true; } } - bool IRenderAnnotationsWhenSelected.SpatiallyPartitionable { get { return true; } } - - IEnumerable DrawControlGroup(Actor self, WorldRenderer wr, PaletteReference palette) + IEnumerable IDecoration.RenderDecoration(Actor self, WorldRenderer wr, int2 pos) { var group = self.World.Selection.GetControlGroupForActor(self); if (group == null) - yield break; + return Enumerable.Empty(); - pipImages.PlayFetchIndex(Info.GroupSequence, () => (int)group); + anim.PlayFetchIndex(Info.GroupSequence, () => (int)group); - var bounds = decorationBounds.FirstNonEmptyBounds(self, wr); - var boundsOffset = 0.5f * new float2(bounds.Left + bounds.Right, bounds.Top + bounds.Bottom); - if (Info.ReferencePoint.HasFlag(ReferencePoints.Top)) - boundsOffset -= new float2(0, 0.5f * bounds.Height); - - if (Info.ReferencePoint.HasFlag(ReferencePoints.Bottom)) - boundsOffset += new float2(0, 0.5f * bounds.Height); - - if (Info.ReferencePoint.HasFlag(ReferencePoints.Left)) - boundsOffset -= new float2(0.5f * bounds.Width, 0); - - if (Info.ReferencePoint.HasFlag(ReferencePoints.Right)) - boundsOffset += new float2(0.5f * bounds.Width, 0); - - var pxPos = wr.Viewport.WorldToViewPx(boundsOffset.ToInt2()) - (0.5f * pipImages.Image.Size.XY).ToInt2(); - yield return new UISpriteRenderable(pipImages.Image, self.CenterPosition, pxPos, 0, palette, 1f); + var screenPos = wr.Viewport.WorldToViewPx(pos) + Info.Position.CreateMargin(Info.Margin) - (0.5f * anim.Image.Size.XY).ToInt2(); + var palette = wr.Palette(Info.Palette); + return new IRenderable[] + { + new UISpriteRenderable(anim.Image, self.CenterPosition, screenPos, 0, palette, 1f) + }; } } } diff --git a/OpenRA.Mods.Common/Traits/Render/WithTextControlGroupDecoration.cs b/OpenRA.Mods.Common/Traits/Render/WithTextControlGroupDecoration.cs index 35dbf52e48..3e24eb65a7 100644 --- a/OpenRA.Mods.Common/Traits/Render/WithTextControlGroupDecoration.cs +++ b/OpenRA.Mods.Common/Traits/Render/WithTextControlGroupDecoration.cs @@ -13,13 +13,14 @@ using System.Collections.Generic; using System.Linq; using OpenRA.Graphics; using OpenRA.Mods.Common.Graphics; +using OpenRA.Mods.Common.Widgets; using OpenRA.Primitives; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits.Render { [Desc("Renders Ctrl groups using typeface.")] - public class WithTextControlGroupDecorationInfo : ITraitInfo, IRulesetLoaded, Requires + public class WithTextControlGroupDecorationInfo : ITraitInfo, IRulesetLoaded { public readonly string Font = "TinyBold"; @@ -29,15 +30,11 @@ namespace OpenRA.Mods.Common.Traits.Render [Desc("Use the player color of the current owner.")] public readonly bool UsePlayerColor = false; - [Desc("The Z offset to apply when rendering this decoration.")] - public readonly int ZOffset = 1; + [Desc("Position in the actor's selection box to draw the decoration.")] + public readonly DecorationPosition Position = DecorationPosition.TopLeft; - [Desc("Point in the actor's selection box used as reference for offsetting the decoration image. " + - "Possible values are combinations of Center, Top, Bottom, Left, Right.")] - public readonly ReferencePoints ReferencePoint = ReferencePoints.Bottom | ReferencePoints.Left; - - [Desc("Manual offset in screen pixel.")] - public readonly int2 ScreenOffset = new int2(2, -2); + [Desc("Offset text center position from the selection box edge.")] + public readonly int2 Margin = int2.Zero; void IRulesetLoaded.RulesetLoaded(Ruleset rules, ActorInfo info) { @@ -48,76 +45,42 @@ namespace OpenRA.Mods.Common.Traits.Render public object Create(ActorInitializer init) { return new WithTextControlGroupDecoration(init.Self, this); } } - public class WithTextControlGroupDecoration : IRenderAnnotationsWhenSelected, INotifyOwnerChanged + public class WithTextControlGroupDecoration : IDecoration, INotifyOwnerChanged { readonly WithTextControlGroupDecorationInfo info; - readonly IDecorationBounds[] decorationBounds; readonly SpriteFont font; + readonly Actor self; + readonly CachedTransform label; Color color; public WithTextControlGroupDecoration(Actor self, WithTextControlGroupDecorationInfo info) { this.info = info; - - if (!Game.Renderer.Fonts.TryGetValue(info.Font, out font)) - throw new YamlException("Font '{0}' is not listed in the mod.yaml's Fonts section".F(info.Font)); - - decorationBounds = self.TraitsImplementing().ToArray(); + this.self = self; + font = Game.Renderer.Fonts[info.Font]; color = info.UsePlayerColor ? self.Owner.Color : info.Color; + label = new CachedTransform(g => g.ToString()); } - IEnumerable IRenderAnnotationsWhenSelected.RenderAnnotations(Actor self, WorldRenderer wr) - { - if (self.Owner != wr.World.LocalPlayer) - yield break; + DecorationPosition IDecoration.Position { get { return info.Position; } } - if (self.World.FogObscures(self)) - yield break; + bool IDecoration.Enabled { get { return self.Owner == self.World.LocalPlayer && self.World.Selection.GetControlGroupForActor(self) != null; } } - foreach (var r in DrawControlGroup(self, wr)) - yield return r; - } + bool IDecoration.RequiresSelection { get { return true; } } - bool IRenderAnnotationsWhenSelected.SpatiallyPartitionable { get { return true; } } - - IEnumerable DrawControlGroup(Actor self, WorldRenderer wr) + IEnumerable IDecoration.RenderDecoration(Actor self, WorldRenderer wr, int2 pos) { var group = self.World.Selection.GetControlGroupForActor(self); if (group == null) - yield break; + return Enumerable.Empty(); - var bounds = decorationBounds.FirstNonEmptyBounds(self, wr); - var number = group.Value.ToString(); - var halfSize = font.Measure(number) / 2; - - var boundsOffset = new int2(bounds.Left + bounds.Right, bounds.Top + bounds.Bottom) / 2; - var sizeOffset = int2.Zero; - if (info.ReferencePoint.HasFlag(ReferencePoints.Top)) + var text = label.Update(group.Value); + var screenPos = wr.Viewport.WorldToViewPx(pos) + info.Position.CreateMargin(info.Margin); + return new IRenderable[] { - boundsOffset -= new int2(0, bounds.Height / 2); - sizeOffset += new int2(0, halfSize.Y); - } - else if (info.ReferencePoint.HasFlag(ReferencePoints.Bottom)) - { - boundsOffset += new int2(0, bounds.Height / 2); - sizeOffset -= new int2(0, halfSize.Y); - } - - if (info.ReferencePoint.HasFlag(ReferencePoints.Left)) - { - boundsOffset -= new int2(bounds.Width / 2, 0); - sizeOffset += new int2(halfSize.X, 0); - } - else if (info.ReferencePoint.HasFlag(ReferencePoints.Right)) - { - boundsOffset += new int2(bounds.Width / 2, 0); - sizeOffset -= new int2(halfSize.X, 0); - } - - var screenPos = boundsOffset + sizeOffset + info.ScreenOffset; - - yield return new TextAnnotationRenderable(font, wr.ProjectedPosition(screenPos), info.ZOffset, color, number); + new UITextRenderable(font, self.CenterPosition, screenPos, 0, color, text) + }; } void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) diff --git a/OpenRA.Mods.Common/Traits/Render/WithTextDecoration.cs b/OpenRA.Mods.Common/Traits/Render/WithTextDecoration.cs index 00cb8bfce7..10bccd8f9e 100644 --- a/OpenRA.Mods.Common/Traits/Render/WithTextDecoration.cs +++ b/OpenRA.Mods.Common/Traits/Render/WithTextDecoration.cs @@ -19,7 +19,7 @@ using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits.Render { [Desc("Displays a text overlay relative to the selection box.")] - public class WithTextDecorationInfo : ConditionalTraitInfo, Requires + public class WithTextDecorationInfo : WithDecorationBaseInfo { [Translate] [FieldLoader.Require] @@ -33,19 +33,6 @@ namespace OpenRA.Mods.Common.Traits.Render [Desc("Use the player color of the current owner.")] public readonly bool UsePlayerColor = false; - [Desc("Point in the actor's selection box used as reference for offsetting the decoration image. " + - "Possible values are combinations of Center, Top, Bottom, Left, Right.")] - public readonly ReferencePoints ReferencePoint = ReferencePoints.Top | ReferencePoints.Left; - - [Desc("The Z offset to apply when rendering this decoration.")] - public readonly int ZOffset = 1; - - [Desc("Player stances who can view the decoration.")] - public readonly Stance ValidStances = Stance.Ally; - - [Desc("Should this be visible only when selected?")] - public readonly bool RequiresSelection = false; - public override object Create(ActorInitializer init) { return new WithTextDecoration(init.Self, this); } public override void RulesetLoaded(Ruleset rules, ActorInfo ai) @@ -57,79 +44,28 @@ namespace OpenRA.Mods.Common.Traits.Render } } - public class WithTextDecoration : ConditionalTrait, IRenderAnnotations, IRenderAnnotationsWhenSelected, INotifyOwnerChanged + public class WithTextDecoration : WithDecorationBase, INotifyOwnerChanged { readonly SpriteFont font; - readonly IDecorationBounds[] decorationBounds; Color color; public WithTextDecoration(Actor self, WithTextDecorationInfo info) - : base(info) + : base(self, info) { font = Game.Renderer.Fonts[info.Font]; - decorationBounds = self.TraitsImplementing().ToArray(); - color = Info.UsePlayerColor ? self.Owner.Color : Info.Color; + color = info.UsePlayerColor ? self.Owner.Color : info.Color; } - public virtual bool ShouldRender(Actor self) { return true; } - - IEnumerable IRenderAnnotations.RenderAnnotations(Actor self, WorldRenderer wr) + protected override IEnumerable RenderDecoration(Actor self, WorldRenderer wr, int2 screenPos) { - return !Info.RequiresSelection ? RenderInner(self, wr) : SpriteRenderable.None; - } - - public bool SpatiallyPartitionable { get { return true; } } - - IEnumerable IRenderAnnotationsWhenSelected.RenderAnnotations(Actor self, WorldRenderer wr) - { - return Info.RequiresSelection ? RenderInner(self, wr) : SpriteRenderable.None; - } - - bool IRenderAnnotationsWhenSelected.SpatiallyPartitionable { get { return true; } } - - IEnumerable RenderInner(Actor self, WorldRenderer wr) - { - if (IsTraitDisabled || self.IsDead || !self.IsInWorld) + if (IsTraitDisabled || self.IsDead || !self.IsInWorld || !ShouldRender(self)) return Enumerable.Empty(); - if (self.World.RenderPlayer != null) + var size = font.Measure(Info.Text); + return new IRenderable[] { - var stance = self.Owner.Stances[self.World.RenderPlayer]; - if (!Info.ValidStances.HasStance(stance)) - return Enumerable.Empty(); - } - - if (!ShouldRender(self) || self.World.FogObscures(self)) - return Enumerable.Empty(); - - var bounds = decorationBounds.FirstNonEmptyBounds(self, wr); - var halfSize = font.Measure(Info.Text) / 2; - - var boundsOffset = new int2(bounds.Left + bounds.Right, bounds.Top + bounds.Bottom) / 2; - var sizeOffset = int2.Zero; - if (Info.ReferencePoint.HasFlag(ReferencePoints.Top)) - { - boundsOffset -= new int2(0, bounds.Height / 2); - sizeOffset += new int2(0, halfSize.Y); - } - else if (Info.ReferencePoint.HasFlag(ReferencePoints.Bottom)) - { - boundsOffset += new int2(0, bounds.Height / 2); - sizeOffset -= new int2(0, halfSize.Y); - } - - if (Info.ReferencePoint.HasFlag(ReferencePoints.Left)) - { - boundsOffset -= new int2(bounds.Width / 2, 0); - sizeOffset += new int2(halfSize.X, 0); - } - else if (Info.ReferencePoint.HasFlag(ReferencePoints.Right)) - { - boundsOffset += new int2(bounds.Width / 2, 0); - sizeOffset -= new int2(halfSize.X, 0); - } - - return new IRenderable[] { new TextAnnotationRenderable(font, wr.ProjectedPosition(boundsOffset + sizeOffset), Info.ZOffset, color, Info.Text) }; + new UITextRenderable(font, self.CenterPosition, screenPos - size / 2, 0, color, Info.Text) + }; } void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) diff --git a/OpenRA.Mods.Common/Traits/StoresResources.cs b/OpenRA.Mods.Common/Traits/StoresResources.cs index 5129790507..2cd55c9c43 100644 --- a/OpenRA.Mods.Common/Traits/StoresResources.cs +++ b/OpenRA.Mods.Common/Traits/StoresResources.cs @@ -22,16 +22,10 @@ namespace OpenRA.Mods.Common.Traits [FieldLoader.Require] public readonly int Capacity = 0; - [FieldLoader.Require] - [Desc("Number of little squares used to display how filled unit is.")] - public readonly int PipCount = 0; - - public readonly PipType PipColor = PipType.Yellow; - public object Create(ActorInitializer init) { return new StoresResources(init.Self, this); } } - public class StoresResources : IPips, INotifyOwnerChanged, INotifyCapture, IStoreResources, ISync, INotifyKilled, INotifyAddedToWorld, INotifyRemovedFromWorld + public class StoresResources : INotifyOwnerChanged, INotifyCapture, IStoreResources, ISync, INotifyKilled, INotifyAddedToWorld, INotifyRemovedFromWorld { readonly StoresResourcesInfo info; PlayerResources player; @@ -65,13 +59,6 @@ namespace OpenRA.Mods.Common.Traits player.TakeResources(Stored); } - IEnumerable IPips.GetPips(Actor self) - { - return Enumerable.Range(0, info.PipCount).Select(i => - player.Resources * info.PipCount > i * player.ResourceCapacity - ? info.PipColor : PipType.Transparent); - } - void INotifyAddedToWorld.AddedToWorld(Actor self) { player.AddStorage(info.Capacity); diff --git a/OpenRA.Mods.Common/Traits/SupportPowers/GrantExternalConditionPower.cs b/OpenRA.Mods.Common/Traits/SupportPowers/GrantExternalConditionPower.cs index 5050c94d09..045b425079 100644 --- a/OpenRA.Mods.Common/Traits/SupportPowers/GrantExternalConditionPower.cs +++ b/OpenRA.Mods.Common/Traits/SupportPowers/GrantExternalConditionPower.cs @@ -146,8 +146,9 @@ namespace OpenRA.Mods.Common.Traits var xy = wr.Viewport.ViewToWorld(Viewport.LastMousePos); foreach (var unit in power.UnitsInRange(xy)) { - var bounds = unit.TraitsImplementing().FirstNonEmptyBounds(unit, wr); - yield return new SelectionBoxAnnotationRenderable(unit, bounds, Color.Red); + var decorations = unit.TraitsImplementing().FirstEnabledTraitOrDefault(); + foreach (var d in decorations.RenderSelectionAnnotations(unit, wr, Color.Red)) + yield return d; } } diff --git a/OpenRA.Mods.Common/Traits/World/ResourceType.cs b/OpenRA.Mods.Common/Traits/World/ResourceType.cs index f11088f084..d4937ba61e 100644 --- a/OpenRA.Mods.Common/Traits/World/ResourceType.cs +++ b/OpenRA.Mods.Common/Traits/World/ResourceType.cs @@ -63,9 +63,6 @@ namespace OpenRA.Mods.Common.Traits [Desc("Allow resource to spawn on ramp tiles.")] public readonly bool AllowOnRamps = false; - [Desc("Harvester content pip color.")] - public PipType PipColor = PipType.Yellow; - void IMapPreviewSignatureInfo.PopulateMapPreviewSignatureCells(Map map, ActorInfo ai, ActorReference s, List> destinationBuffer) { var tileSet = map.Rules.TileSet; diff --git a/OpenRA.Mods.Common/TraitsInterfaces.cs b/OpenRA.Mods.Common/TraitsInterfaces.cs index 0a2e0cc47a..d060a975c4 100644 --- a/OpenRA.Mods.Common/TraitsInterfaces.cs +++ b/OpenRA.Mods.Common/TraitsInterfaces.cs @@ -15,6 +15,7 @@ using OpenRA.Activities; using OpenRA.Graphics; using OpenRA.Mods.Common.Activities; using OpenRA.Mods.Common.Graphics; +using OpenRA.Mods.Common.Traits.Render; using OpenRA.Primitives; using OpenRA.Traits; @@ -642,4 +643,40 @@ namespace OpenRA.Mods.Common.Traits { string Class { get; } } + + public interface IDecoration + { + DecorationPosition Position { get; } + bool RequiresSelection { get; } + + bool Enabled { get; } + + IEnumerable RenderDecoration(Actor self, WorldRenderer wr, int2 pos); + } + + public enum DecorationPosition + { + Center, + TopLeft, + TopRight, + BottomLeft, + BottomRight, + Top + } + + public static class DecorationExtensions + { + public static int2 CreateMargin(this DecorationPosition pos, int2 margin) + { + switch (pos) + { + case DecorationPosition.TopLeft: return margin; + case DecorationPosition.TopRight: return new int2(-margin.X, margin.Y); + case DecorationPosition.BottomLeft: return new int2(margin.X, -margin.Y); + case DecorationPosition.BottomRight: return -margin; + case DecorationPosition.Top: return new int2(0, margin.Y); + default: return int2.Zero; + } + } + } } diff --git a/OpenRA.Mods.Common/UpdateRules/Rules/AddPipDecorationTraits.cs b/OpenRA.Mods.Common/UpdateRules/Rules/AddPipDecorationTraits.cs new file mode 100644 index 0000000000..abe2a5d98e --- /dev/null +++ b/OpenRA.Mods.Common/UpdateRules/Rules/AddPipDecorationTraits.cs @@ -0,0 +1,273 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 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.Collections.Generic; +using System.Linq; + +namespace OpenRA.Mods.Common.UpdateRules.Rules +{ + public class AddPipDecorationTraits : UpdateRule + { + public override string Name { get { return "Add decoration traits for selection pips."; } } + public override string Description + { + get + { + return "The AmmoPool, Cargo, Harvester, and StoresResources traits no longer\n" + + "automatically add pips to the selection box. New traits WithAmmoPipsDecoration,\n" + + "WithCargoPipsDecoration, WithHarvesterPipsDecoration,\n" + + "WithResourceStoragePipsDecoration are added to provide the same functionality.\n\n" + + "Passenger.PipType has been replaced with CustomPipType, which now references a\n" + + "sequence defined in WithCargoDecoration.CustomPipTypeSequences.\n\n" + + "ResourceType.PipColor has been removed and resource pip colours are now defined\n" + + "in WithHarvesterPipsDecoration.ResourceSequences."; + } + } + + static readonly Dictionary PipReplacements = new Dictionary + { + { "transparent", "pip-empty" }, + { "green", "pip-green" }, + { "yellow", "pip-yellow" }, + { "red", "pip-red" }, + { "gray", "pip-gray" }, + { "blue", "pip-blue" }, + { "ammo", "pip-ammo" }, + { "ammoempty", "pip-ammoempty" }, + }; + + bool customPips; + readonly List locations = new List(); + readonly List cargoPipLocations = new List(); + readonly HashSet cargoCustomPips = new HashSet(); + readonly List harvesterPipLocations = new List(); + readonly Dictionary harvesterCustomPips = new Dictionary(); + + public override IEnumerable AfterUpdate(ModData modData) + { + if (customPips && locations.Any()) + yield return "Custom pip Images and Palettes are now defined on the individual With*PipsDecoration traits.\n" + + "You should review the following definitions and manually define the Image and Palette properties as required:\n" + + UpdateUtils.FormatMessageList(locations); + + if (cargoCustomPips.Any() && cargoPipLocations.Any()) + yield return "Some passenger types define custom cargo pips. Review the following definitions:\n" + + UpdateUtils.FormatMessageList(cargoPipLocations) + + "\nand, if required, add the following to the WithCargoPipsDecoration traits:\n" + + "CustomPipSequences:\n" + cargoCustomPips.Select(p => "\t{0}: {1}".F(p, PipReplacements[p])).JoinWith("\n"); + + if (harvesterCustomPips.Any() && harvesterPipLocations.Any()) + yield return "Review the following definitions:\n" + + UpdateUtils.FormatMessageList(harvesterPipLocations) + + "\nand, if required, add the following to the WithHarvesterPipsDecoration traits:\n" + + "ResourceSequences:\n" + harvesterCustomPips.Select(kv => "\t{0}: {1}".F(kv.Key, PipReplacements[kv.Value])).JoinWith("\n"); + + customPips = false; + locations.Clear(); + cargoPipLocations.Clear(); + harvesterPipLocations.Clear(); + } + + public override IEnumerable UpdateActorNode(ModData modData, MiniYamlNode actorNode) + { + var addNodes = new List(); + + foreach (var selectionDecorations in actorNode.ChildrenMatching("SelectionDecorations")) + { + customPips |= selectionDecorations.RemoveNodes("Palette") > 0; + customPips |= selectionDecorations.RemoveNodes("Image") > 0; + } + + foreach (var ammoPool in actorNode.ChildrenMatching("AmmoPool")) + { + var ammoPips = new MiniYamlNode("WithAmmoPipsDecoration", ""); + ammoPips.AddNode("Position", "BottomLeft"); + ammoPips.AddNode("RequiresSelection", "true"); + + var pipCountNode = ammoPool.LastChildMatching("PipCount"); + if (pipCountNode != null) + { + ammoPool.RemoveNode(pipCountNode); + var pipCount = pipCountNode.NodeValue(); + if (pipCount == 0) + { + addNodes.Add(new MiniYamlNode("-" + ammoPips.Key, "")); + continue; + } + + var ammoNode = ammoPool.LastChildMatching("Ammo"); + var maxAmmo = ammoNode != null ? ammoNode.NodeValue() : 0; + if (pipCount != maxAmmo) + ammoPips.AddNode("PipCount", pipCount); + } + + var pipTypeNode = ammoPool.LastChildMatching("PipType"); + if (pipTypeNode != null) + { + ammoPool.RemoveNode(pipTypeNode); + + string sequence; + if (PipReplacements.TryGetValue(pipTypeNode.Value.Value.ToLowerInvariant(), out sequence)) + ammoPips.AddNode("FullSequence", sequence); + } + + var pipTypeEmptyNode = ammoPool.LastChildMatching("PipTypeEmpty"); + if (pipTypeEmptyNode != null) + { + ammoPool.RemoveNode(pipTypeEmptyNode); + + string sequence; + if (PipReplacements.TryGetValue(pipTypeEmptyNode.Value.Value.ToLowerInvariant(), out sequence)) + ammoPips.AddNode("EmptySequence", sequence); + } + + addNodes.Add(ammoPips); + locations.Add("{0}: {1} ({2})".F(actorNode.Key, ammoPips.Key, actorNode.Location.Filename)); + } + + foreach (var cargo in actorNode.ChildrenMatching("Cargo")) + { + var cargoPips = new MiniYamlNode("WithCargoPipsDecoration", ""); + cargoPips.AddNode("Position", "BottomLeft"); + cargoPips.AddNode("RequiresSelection", "true"); + + var pipCountNode = cargo.LastChildMatching("PipCount"); + if (pipCountNode != null) + { + cargo.RemoveNode(pipCountNode); + + var pipCount = pipCountNode.NodeValue(); + if (pipCount == 0) + { + addNodes.Add(new MiniYamlNode("-" + cargoPips.Key, "")); + continue; + } + + var maxWeightNode = cargo.LastChildMatching("MaxWeight"); + var maxWeight = maxWeightNode != null ? maxWeightNode.NodeValue() : 0; + if (pipCount != maxWeight) + cargoPips.AddNode("PipCount", pipCount); + } + else + continue; + + addNodes.Add(cargoPips); + locations.Add("{0}: {1} ({2})".F(actorNode.Key, cargoPips.Key, actorNode.Location.Filename)); + cargoPipLocations.Add("{0} ({1})".F(actorNode.Key, actorNode.Location.Filename)); + } + + foreach (var passenger in actorNode.ChildrenMatching("Passenger")) + { + var pipTypeNode = passenger.LastChildMatching("PipType"); + if (pipTypeNode != null) + { + pipTypeNode.RenameKey("CustomPipType"); + pipTypeNode.Value.Value = pipTypeNode.Value.Value.ToLowerInvariant(); + cargoCustomPips.Add(pipTypeNode.Value.Value); + } + } + + foreach (var harvester in actorNode.ChildrenMatching("Harvester")) + { + var harvesterPips = new MiniYamlNode("WithHarvesterPipsDecoration", ""); + harvesterPips.AddNode("Position", "BottomLeft"); + harvesterPips.AddNode("RequiresSelection", "true"); + + // Harvester hardcoded a default PipCount > 0 so we can't use that to determine whether + // this is a definition or an override. Resources isn't ideal either, but is better than nothing + var resourcesNode = harvester.LastChildMatching("Resources"); + if (resourcesNode == null) + continue; + + var pipCountNode = harvester.LastChildMatching("PipCount"); + if (pipCountNode != null) + { + harvester.RemoveNode(pipCountNode); + + var pipCount = pipCountNode.NodeValue(); + if (pipCount == 0) + { + addNodes.Add(new MiniYamlNode("-" + harvesterPips.Key, "")); + continue; + } + + harvesterPips.AddNode("PipCount", pipCount); + } + else + harvesterPips.AddNode("PipCount", 7); + + addNodes.Add(harvesterPips); + locations.Add("{0}: {1} ({2})".F(actorNode.Key, harvesterPips.Key, actorNode.Location.Filename)); + harvesterPipLocations.Add("{0} ({1})".F(actorNode.Key, actorNode.Location.Filename)); + } + + foreach (var resourceType in actorNode.ChildrenMatching("ResourceType")) + { + var pipColor = "yellow"; + var pipCountNode = resourceType.LastChildMatching("PipColor"); + if (pipCountNode != null) + { + pipColor = pipCountNode.Value.Value.ToLowerInvariant(); + resourceType.RemoveNode(pipCountNode); + } + + var typeNode = resourceType.LastChildMatching("Type"); + if (typeNode != null) + harvesterCustomPips.Add(typeNode.Value.Value, pipColor); + } + + foreach (var storesResources in actorNode.ChildrenMatching("StoresResources")) + { + var storagePips = new MiniYamlNode("WithResourceStoragePipsDecoration", ""); + storagePips.AddNode("Position", "BottomLeft"); + storagePips.AddNode("RequiresSelection", "true"); + + var pipCountNode = storesResources.LastChildMatching("PipCount"); + if (pipCountNode != null) + { + storesResources.RemoveNode(pipCountNode); + var pipCount = pipCountNode.NodeValue(); + if (pipCount == 0) + { + addNodes.Add(new MiniYamlNode("-" + storagePips.Key, "")); + continue; + } + + storagePips.AddNode("PipCount", pipCount); + } + else + continue; + + // Default pip color changed from yellow to green for consistency with other pip traits + var pipColorNode = storesResources.LastChildMatching("PipColor"); + if (pipColorNode != null) + { + storesResources.RemoveNode(pipColorNode); + + string sequence; + var type = pipColorNode.Value.Value.ToLowerInvariant(); + if (type != "green" && PipReplacements.TryGetValue(type, out sequence)) + storagePips.AddNode("FullSequence", sequence); + } + else + storagePips.AddNode("FullSequence", PipReplacements["yellow"]); + + addNodes.Add(storagePips); + locations.Add("{0}: {1} ({2})".F(actorNode.Key, storagePips.Key, actorNode.Location.Filename)); + } + + foreach (var addNode in addNodes) + actorNode.AddNode(addNode); + + yield break; + } + } +} diff --git a/OpenRA.Mods.Common/UpdateRules/Rules/ModernizeDecorationTraits.cs b/OpenRA.Mods.Common/UpdateRules/Rules/ModernizeDecorationTraits.cs new file mode 100644 index 0000000000..16a577aef5 --- /dev/null +++ b/OpenRA.Mods.Common/UpdateRules/Rules/ModernizeDecorationTraits.cs @@ -0,0 +1,123 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 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.Mods.Common.Traits; + +namespace OpenRA.Mods.Common.UpdateRules.Rules +{ + public class ModernizeDecorationTraits : UpdateRule + { + public override string Name { get { return "Modernize SelectionDecorations and With*Decoration traits."; } } + public override string Description + { + get + { + return "The configuration properties exposed on the SelectionDecorations and With*Decoration\n" + + "traits have been reworked. RenderSelectionBars and RenderSelectionBox have been removed from\n" + + "SelectionDecorations. The obsolete ZOffset and ScreenOffset has been removed from With*Decoration, and ReferencePoint has\n" + + "been replaced by Position which takes a single value (TopLeft, TopRight, BottomLeft, BottomRight, Center, or Top).\n" + + "A new Margin property is available to control the decoration offset relative to the edges of the selection box.\n" + + "RenderNameTag has been renamed to WithNameTagDecoration and now behaves like a normal decoration trait.\n"; + } + } + + static readonly string[] LegacyDecorationTraits = { "WithDecoration", "WithSpriteControlGroupDecoration", "WithTextControlGroupDecoration", "WithTextDecoration", "WithBuildingRepairDecoration", "InfiltrateForDecoration" }; + static readonly string[] ModernDecorationTraits = { "WithAmmoPipsDecoration", "WithCargoPipsDecoration", "WithHarvesterPipsDecoration", "WithResourceStoragePipsDecoration", "WithNameTagDecoration" }; + + [Flags] + public enum LegacyReferencePoints + { + Center = 0, + Top = 1, + Bottom = 2, + Left = 4, + Right = 8, + } + + static readonly Dictionary PositionMap = new Dictionary() + { + { LegacyReferencePoints.Center, DecorationPosition.Center }, + { LegacyReferencePoints.Top, DecorationPosition.Top }, + { LegacyReferencePoints.Top | LegacyReferencePoints.Left, DecorationPosition.TopLeft }, + { LegacyReferencePoints.Top | LegacyReferencePoints.Right, DecorationPosition.TopRight }, + { LegacyReferencePoints.Bottom | LegacyReferencePoints.Left, DecorationPosition.BottomLeft }, + { LegacyReferencePoints.Bottom | LegacyReferencePoints.Right, DecorationPosition.BottomRight } + }; + + readonly Dictionary> locations = new Dictionary>(); + + public override IEnumerable AfterUpdate(ModData modData) + { + if (locations.Any()) + yield return "The way that decorations are positioned relative to the selection box has changed.\n" + + "Review the following definitions and define Margin properties as required:\n" + + UpdateUtils.FormatMessageList(locations.Select( + kv => kv.Key + ":\n" + UpdateUtils.FormatMessageList(kv.Value))); + + locations.Clear(); + } + + public override IEnumerable UpdateActorNode(ModData modData, MiniYamlNode actorNode) + { + var locationKey = "{0} ({1})".F(actorNode.Key, actorNode.Location.Filename); + + foreach (var trait in LegacyDecorationTraits) + { + foreach (var node in actorNode.ChildrenMatching(trait)) + { + node.RemoveNodes("ZOffset"); + node.RemoveNodes("ScreenOffset"); + + var positionNode = node.LastChildMatching("ReferencePoint"); + if (positionNode != null) + { + DecorationPosition value; + if (!PositionMap.TryGetValue(positionNode.NodeValue(), out value)) + value = DecorationPosition.TopLeft; + + if (value != DecorationPosition.TopLeft) + { + positionNode.RenameKey("Position"); + positionNode.ReplaceValue(FieldSaver.FormatValue(value)); + } + else + node.RemoveNode(positionNode); + } + + locations.GetOrAdd(locationKey).Add(node.Key); + } + } + + foreach (var trait in ModernDecorationTraits) + foreach (var node in actorNode.ChildrenMatching(trait)) + locations.GetOrAdd(locationKey).Add(node.Key); + + foreach (var selection in actorNode.ChildrenMatching("SelectionDecorations")) + { + selection.RemoveNodes("RenderSelectionBars"); + selection.RemoveNodes("RenderSelectionBox"); + } + + foreach (var nameTag in actorNode.ChildrenMatching("RenderNameTag")) + { + nameTag.RenameKey("WithNameTagDecoration"); + nameTag.AddNode("Position", "Top"); + nameTag.AddNode("UsePlayerColor", "true"); + locations.GetOrAdd(locationKey).Add(nameTag.Key); + } + + yield break; + } + } +} diff --git a/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs b/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs index 45fb01bd02..9679aee6b6 100644 --- a/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs +++ b/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs @@ -61,9 +61,8 @@ namespace OpenRA.Mods.Common.UpdateRules new ReplaceAttackTypeStrafe() }), - new UpdatePath("release-20200202", new UpdateRule[] + new UpdatePath("release-20200202", "playtest-20200303", new UpdateRule[] { - // Bleed only changes here new RemoveYesNo(), new RemoveInitialFacingHardcoding(), new RemoveAirdropActorTypeDefault(), @@ -74,6 +73,13 @@ namespace OpenRA.Mods.Common.UpdateRules new RenameSpins(), new CreateScreenShakeWarhead(), new RenameRallyPointPath(), + }), + + new UpdatePath("playtest-20200303", new UpdateRule[] + { + // Bleed only changes here + new AddPipDecorationTraits(), + new ModernizeDecorationTraits(), }) }; diff --git a/OpenRA.Mods.Common/Widgets/WorldInteractionControllerWidget.cs b/OpenRA.Mods.Common/Widgets/WorldInteractionControllerWidget.cs index ec636efa49..7b297ff016 100644 --- a/OpenRA.Mods.Common/Widgets/WorldInteractionControllerWidget.cs +++ b/OpenRA.Mods.Common/Widgets/WorldInteractionControllerWidget.cs @@ -64,7 +64,8 @@ namespace OpenRA.Mods.Common.Widgets if (selectionDecorations == null) return; - selectionDecorations.DrawRollover(unit, worldRenderer); + foreach (var r in selectionDecorations.RenderRolloverAnnotations(unit, worldRenderer)) + r.PrepareRender(worldRenderer).Render(worldRenderer); } public override void Draw()