Introduce IDecorationBounds to replace Actor.SelectionOverlayBounds.
This commit is contained in:
@@ -16,7 +16,7 @@ using OpenRA.Graphics;
|
|||||||
namespace OpenRA.Traits
|
namespace OpenRA.Traits
|
||||||
{
|
{
|
||||||
[Desc("This actor is selectable. Defines bounds of selectable area, selection class and selection priority.")]
|
[Desc("This actor is selectable. Defines bounds of selectable area, selection class and selection priority.")]
|
||||||
public class SelectableInfo : ITraitInfo
|
public class SelectableInfo : ITraitInfo, IDecorationBoundsInfo
|
||||||
{
|
{
|
||||||
public readonly int Priority = 10;
|
public readonly int Priority = 10;
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ namespace OpenRA.Traits
|
|||||||
public object Create(ActorInitializer init) { return new Selectable(init.Self, this); }
|
public object Create(ActorInitializer init) { return new Selectable(init.Self, this); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Selectable : IMouseBounds
|
public class Selectable : IMouseBounds, IDecorationBounds
|
||||||
{
|
{
|
||||||
public readonly string Class = null;
|
public readonly string Class = null;
|
||||||
|
|
||||||
@@ -61,5 +61,20 @@ namespace OpenRA.Traits
|
|||||||
var xy = wr.ScreenPxPosition(self.CenterPosition) + offset;
|
var xy = wr.ScreenPxPosition(self.CenterPosition) + offset;
|
||||||
return new Rectangle(xy.X, xy.Y, size.X + 2 * Info.Margin, size.Y + 2 * Info.Margin);
|
return new Rectangle(xy.X, xy.Y, size.X + 2 * Info.Margin, size.Y + 2 * Info.Margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle IDecorationBounds.DecorationBounds(Actor self, WorldRenderer wr)
|
||||||
|
{
|
||||||
|
if (Info.Bounds == null)
|
||||||
|
return Rectangle.Empty;
|
||||||
|
|
||||||
|
var size = new int2(Info.Bounds[0], Info.Bounds[1]);
|
||||||
|
|
||||||
|
var offset = -size / 2;
|
||||||
|
if (Info.Bounds.Length > 2)
|
||||||
|
offset += new int2(Info.Bounds[2], Info.Bounds[3]);
|
||||||
|
|
||||||
|
var xy = wr.ScreenPxPosition(self.CenterPosition) + offset;
|
||||||
|
return new Rectangle(xy.X, xy.Y, size.X, size.Y);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,10 @@ namespace OpenRA.Traits
|
|||||||
public interface IMouseBounds { Rectangle MouseoverBounds(Actor self, WorldRenderer wr); }
|
public interface IMouseBounds { Rectangle MouseoverBounds(Actor self, WorldRenderer wr); }
|
||||||
public interface IAutoMouseBounds { Rectangle AutoMouseoverBounds(Actor self, WorldRenderer wr); }
|
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 interface IAutoRenderSizeInfo : ITraitInfoInterface { }
|
public interface IAutoRenderSizeInfo : ITraitInfoInterface { }
|
||||||
public interface IAutoRenderSize { int2 RenderSize(Actor self); }
|
public interface IAutoRenderSize { int2 RenderSize(Actor self); }
|
||||||
|
|
||||||
|
|||||||
@@ -160,8 +160,16 @@ namespace OpenRA.Mods.Cnc.Traits
|
|||||||
var targetUnits = power.UnitsInRange(xy).Where(a => !world.FogObscures(a));
|
var targetUnits = power.UnitsInRange(xy).Where(a => !world.FogObscures(a));
|
||||||
|
|
||||||
foreach (var unit in targetUnits)
|
foreach (var unit in targetUnits)
|
||||||
|
{
|
||||||
if (unit.CanBeViewedByPlayer(manager.Self.Owner))
|
if (unit.CanBeViewedByPlayer(manager.Self.Owner))
|
||||||
yield return new SelectionBoxRenderable(unit, Color.Red);
|
{
|
||||||
|
var bounds = unit.TraitsImplementing<IDecorationBounds>()
|
||||||
|
.Select(b => b.DecorationBounds(unit, wr))
|
||||||
|
.FirstOrDefault(b => !b.IsEmpty);
|
||||||
|
|
||||||
|
yield return new SelectionBoxRenderable(unit, bounds, Color.Red);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<IRenderable> Render(WorldRenderer wr, World world)
|
public IEnumerable<IRenderable> Render(WorldRenderer wr, World world)
|
||||||
@@ -270,8 +278,16 @@ namespace OpenRA.Mods.Cnc.Traits
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach (var unit in power.UnitsInRange(sourceLocation))
|
foreach (var unit in power.UnitsInRange(sourceLocation))
|
||||||
|
{
|
||||||
if (unit.CanBeViewedByPlayer(manager.Self.Owner))
|
if (unit.CanBeViewedByPlayer(manager.Self.Owner))
|
||||||
yield return new SelectionBoxRenderable(unit, Color.Red);
|
{
|
||||||
|
var bounds = unit.TraitsImplementing<IDecorationBounds>()
|
||||||
|
.Select(b => b.DecorationBounds(unit, wr))
|
||||||
|
.FirstOrDefault(b => !b.IsEmpty);
|
||||||
|
|
||||||
|
yield return new SelectionBoxRenderable(unit, bounds, Color.Red);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<IRenderable> Render(WorldRenderer wr, World world)
|
public IEnumerable<IRenderable> Render(WorldRenderer wr, World world)
|
||||||
|
|||||||
@@ -21,19 +21,21 @@ namespace OpenRA.Mods.Common.Graphics
|
|||||||
readonly Actor actor;
|
readonly Actor actor;
|
||||||
readonly bool displayHealth;
|
readonly bool displayHealth;
|
||||||
readonly bool displayExtra;
|
readonly bool displayExtra;
|
||||||
|
readonly Rectangle decorationBounds;
|
||||||
|
|
||||||
public SelectionBarsRenderable(Actor actor, bool displayHealth, bool displayExtra)
|
public SelectionBarsRenderable(Actor actor, Rectangle decorationBounds, bool displayHealth, bool displayExtra)
|
||||||
: this(actor.CenterPosition, actor)
|
: this(actor.CenterPosition, actor, decorationBounds)
|
||||||
{
|
{
|
||||||
this.displayHealth = displayHealth;
|
this.displayHealth = displayHealth;
|
||||||
this.displayExtra = displayExtra;
|
this.displayExtra = displayExtra;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SelectionBarsRenderable(WPos pos, Actor actor)
|
public SelectionBarsRenderable(WPos pos, Actor actor, Rectangle decorationBounds)
|
||||||
: this()
|
: this()
|
||||||
{
|
{
|
||||||
this.pos = pos;
|
this.pos = pos;
|
||||||
this.actor = actor;
|
this.actor = actor;
|
||||||
|
this.decorationBounds = decorationBounds;
|
||||||
}
|
}
|
||||||
|
|
||||||
public WPos Pos { get { return pos; } }
|
public WPos Pos { get { return pos; } }
|
||||||
@@ -46,7 +48,7 @@ namespace OpenRA.Mods.Common.Graphics
|
|||||||
|
|
||||||
public IRenderable WithPalette(PaletteReference newPalette) { return this; }
|
public IRenderable WithPalette(PaletteReference newPalette) { return this; }
|
||||||
public IRenderable WithZOffset(int newOffset) { return this; }
|
public IRenderable WithZOffset(int newOffset) { return this; }
|
||||||
public IRenderable OffsetBy(WVec vec) { return new SelectionBarsRenderable(pos + vec, actor); }
|
public IRenderable OffsetBy(WVec vec) { return new SelectionBarsRenderable(pos + vec, actor, decorationBounds); }
|
||||||
public IRenderable AsDecoration() { return this; }
|
public IRenderable AsDecoration() { return this; }
|
||||||
|
|
||||||
void DrawExtraBars(WorldRenderer wr, float3 start, float3 end)
|
void DrawExtraBars(WorldRenderer wr, float3 start, float3 end)
|
||||||
@@ -150,11 +152,8 @@ namespace OpenRA.Mods.Common.Graphics
|
|||||||
var health = actor.TraitOrDefault<IHealth>();
|
var health = actor.TraitOrDefault<IHealth>();
|
||||||
|
|
||||||
var screenPos = wr.Screen3DPxPosition(pos);
|
var screenPos = wr.Screen3DPxPosition(pos);
|
||||||
var bounds = actor.SelectionOverlayBounds;
|
var start = new float3(decorationBounds.Left + 1, decorationBounds.Top, screenPos.Z);
|
||||||
bounds.Offset((int)screenPos.X, (int)screenPos.Y);
|
var end = new float3(decorationBounds.Right - 1, decorationBounds.Top, screenPos.Z);
|
||||||
|
|
||||||
var start = new float3(bounds.Left + 1, bounds.Top, screenPos.Z);
|
|
||||||
var end = new float3(bounds.Right - 1, bounds.Top, screenPos.Z);
|
|
||||||
|
|
||||||
if (DisplayHealth)
|
if (DisplayHealth)
|
||||||
DrawHealthBar(wr, health, start, end);
|
DrawHealthBar(wr, health, start, end);
|
||||||
|
|||||||
@@ -17,16 +17,16 @@ namespace OpenRA.Mods.Common.Graphics
|
|||||||
public struct SelectionBoxRenderable : IRenderable, IFinalizedRenderable
|
public struct SelectionBoxRenderable : IRenderable, IFinalizedRenderable
|
||||||
{
|
{
|
||||||
readonly WPos pos;
|
readonly WPos pos;
|
||||||
readonly Rectangle visualBounds;
|
readonly Rectangle decorationBounds;
|
||||||
readonly Color color;
|
readonly Color color;
|
||||||
|
|
||||||
public SelectionBoxRenderable(Actor actor, Color color)
|
public SelectionBoxRenderable(Actor actor, Rectangle decorationBounds, Color color)
|
||||||
: this(actor.CenterPosition, actor.SelectionOverlayBounds, color) { }
|
: this(actor.CenterPosition, decorationBounds, color) { }
|
||||||
|
|
||||||
public SelectionBoxRenderable(WPos pos, Rectangle visualBounds, Color color)
|
public SelectionBoxRenderable(WPos pos, Rectangle decorationBounds, Color color)
|
||||||
{
|
{
|
||||||
this.pos = pos;
|
this.pos = pos;
|
||||||
this.visualBounds = visualBounds;
|
this.decorationBounds = decorationBounds;
|
||||||
this.color = color;
|
this.color = color;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,18 +38,18 @@ namespace OpenRA.Mods.Common.Graphics
|
|||||||
|
|
||||||
public IRenderable WithPalette(PaletteReference newPalette) { return this; }
|
public IRenderable WithPalette(PaletteReference newPalette) { return this; }
|
||||||
public IRenderable WithZOffset(int newOffset) { return this; }
|
public IRenderable WithZOffset(int newOffset) { return this; }
|
||||||
public IRenderable OffsetBy(WVec vec) { return new SelectionBoxRenderable(pos + vec, visualBounds, color); }
|
public IRenderable OffsetBy(WVec vec) { return new SelectionBoxRenderable(pos + vec, decorationBounds, color); }
|
||||||
public IRenderable AsDecoration() { return this; }
|
public IRenderable AsDecoration() { return this; }
|
||||||
|
|
||||||
public IFinalizedRenderable PrepareRender(WorldRenderer wr) { return this; }
|
public IFinalizedRenderable PrepareRender(WorldRenderer wr) { return this; }
|
||||||
public void Render(WorldRenderer wr)
|
public void Render(WorldRenderer wr)
|
||||||
{
|
{
|
||||||
var iz = 1 / wr.Viewport.Zoom;
|
var iz = 1 / wr.Viewport.Zoom;
|
||||||
var screenPos = wr.Screen3DPxPosition(pos);
|
var screenDepth = wr.Screen3DPxPosition(pos).Z;
|
||||||
var tl = screenPos + new float2(visualBounds.Left, visualBounds.Top);
|
var tl = new float2(decorationBounds.Left, decorationBounds.Top);
|
||||||
var br = screenPos + new float2(visualBounds.Right, visualBounds.Bottom);
|
var br = new float2(decorationBounds.Right, decorationBounds.Bottom);
|
||||||
var tr = new float3(br.X, tl.Y, screenPos.Z);
|
var tr = new float3(br.X, tl.Y, screenDepth);
|
||||||
var bl = new float3(tl.X, br.Y, screenPos.Z);
|
var bl = new float3(tl.X, br.Y, screenDepth);
|
||||||
var u = new float2(4 * iz, 0);
|
var u = new float2(4 * iz, 0);
|
||||||
var v = new float2(0, 4 * iz);
|
var v = new float2(0, 4 * iz);
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ using OpenRA.Traits;
|
|||||||
namespace OpenRA.Mods.Common.Traits.Render
|
namespace OpenRA.Mods.Common.Traits.Render
|
||||||
{
|
{
|
||||||
[Desc("Automatically calculates the screen map boundaries from the sprite size.")]
|
[Desc("Automatically calculates the screen map boundaries from the sprite size.")]
|
||||||
public class AutoRenderSizeInfo : ITraitInfo, Requires<RenderSpritesInfo>, IAutoRenderSizeInfo
|
public class AutoRenderSizeInfo : ITraitInfo, Requires<RenderSpritesInfo>, IAutoRenderSizeInfo, IDecorationBoundsInfo
|
||||||
{
|
{
|
||||||
public object Create(ActorInitializer init) { return new AutoRenderSize(init.Self); }
|
public object Create(ActorInitializer init) { return new AutoRenderSize(init.Self); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AutoRenderSize : IAutoRenderSize, IMouseBounds
|
public class AutoRenderSize : IAutoRenderSize, IMouseBounds, IDecorationBounds
|
||||||
{
|
{
|
||||||
readonly RenderSprites rs;
|
readonly RenderSprites rs;
|
||||||
|
|
||||||
@@ -37,11 +37,21 @@ namespace OpenRA.Mods.Common.Traits.Render
|
|||||||
return rs.AutoRenderSize(self);
|
return rs.AutoRenderSize(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle IMouseBounds.MouseoverBounds(Actor self, WorldRenderer wr)
|
Rectangle Bounds(Actor self, WorldRenderer wr)
|
||||||
{
|
{
|
||||||
return self.TraitsImplementing<IAutoMouseBounds>()
|
return self.TraitsImplementing<IAutoMouseBounds>()
|
||||||
.Select(s => s.AutoMouseoverBounds(self, wr))
|
.Select(s => s.AutoMouseoverBounds(self, wr))
|
||||||
.FirstOrDefault(r => !r.IsEmpty);
|
.FirstOrDefault(r => !r.IsEmpty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle IMouseBounds.MouseoverBounds(Actor self, WorldRenderer wr)
|
||||||
|
{
|
||||||
|
return Bounds(self, wr);
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle IDecorationBounds.DecorationBounds(Actor self, WorldRenderer wr)
|
||||||
|
{
|
||||||
|
return Bounds(self, wr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,15 +16,19 @@ using OpenRA.Traits;
|
|||||||
namespace OpenRA.Mods.Common.Traits
|
namespace OpenRA.Mods.Common.Traits
|
||||||
{
|
{
|
||||||
[Desc("Special case trait for actors that need to define targetable area and screen map bounds manually.")]
|
[Desc("Special case trait for actors that need to define targetable area and screen map bounds manually.")]
|
||||||
public class CustomRenderSizeInfo : ITraitInfo, IAutoRenderSizeInfo
|
public class CustomRenderSizeInfo : ITraitInfo, IAutoRenderSizeInfo, IDecorationBoundsInfo
|
||||||
{
|
{
|
||||||
[FieldLoader.Require]
|
[FieldLoader.Require]
|
||||||
public readonly int[] CustomBounds = null;
|
public readonly int[] CustomBounds = null;
|
||||||
|
|
||||||
|
[Desc("Defines a custom rectangle for Decorations.",
|
||||||
|
"If null, CustomBounds will be used instead")]
|
||||||
|
public readonly int[] DecorationBounds = null;
|
||||||
|
|
||||||
public object Create(ActorInitializer init) { return new CustomRenderSize(this); }
|
public object Create(ActorInitializer init) { return new CustomRenderSize(this); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CustomRenderSize : IAutoRenderSize, IMouseBounds
|
public class CustomRenderSize : IAutoRenderSize, IMouseBounds, IDecorationBounds
|
||||||
{
|
{
|
||||||
readonly CustomRenderSizeInfo info;
|
readonly CustomRenderSizeInfo info;
|
||||||
public CustomRenderSize(CustomRenderSizeInfo info) { this.info = info; }
|
public CustomRenderSize(CustomRenderSizeInfo info) { this.info = info; }
|
||||||
@@ -48,5 +52,21 @@ namespace OpenRA.Mods.Common.Traits
|
|||||||
var xy = wr.ScreenPxPosition(self.CenterPosition);
|
var xy = wr.ScreenPxPosition(self.CenterPosition);
|
||||||
return new Rectangle(xy.X, xy.Y, size.X, size.Y);
|
return new Rectangle(xy.X, xy.Y, size.X, size.Y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle IDecorationBounds.DecorationBounds(Actor self, WorldRenderer wr)
|
||||||
|
{
|
||||||
|
var bounds = info.DecorationBounds ?? info.CustomBounds;
|
||||||
|
if (bounds == null)
|
||||||
|
return Rectangle.Empty;
|
||||||
|
|
||||||
|
var size = new int2(bounds[0], bounds[1]);
|
||||||
|
|
||||||
|
var offset = -size / 2;
|
||||||
|
if (bounds.Length > 2)
|
||||||
|
offset += new int2(bounds[2], bounds[3]);
|
||||||
|
|
||||||
|
var xy = wr.ScreenPxPosition(self.CenterPosition);
|
||||||
|
return new Rectangle(xy.X, xy.Y, size.X, size.Y);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
|
using System.Linq;
|
||||||
using OpenRA.Graphics;
|
using OpenRA.Graphics;
|
||||||
using OpenRA.Mods.Common.Graphics;
|
using OpenRA.Mods.Common.Graphics;
|
||||||
using OpenRA.Traits;
|
using OpenRA.Traits;
|
||||||
@@ -18,7 +19,7 @@ using OpenRA.Traits;
|
|||||||
namespace OpenRA.Mods.Common.Traits.Render
|
namespace OpenRA.Mods.Common.Traits.Render
|
||||||
{
|
{
|
||||||
[Desc("Displays the player name above the unit")]
|
[Desc("Displays the player name above the unit")]
|
||||||
class RenderNameTagInfo : ITraitInfo
|
class RenderNameTagInfo : ITraitInfo, Requires<IDecorationBoundsInfo>
|
||||||
{
|
{
|
||||||
public readonly int MaxLength = 10;
|
public readonly int MaxLength = 10;
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ namespace OpenRA.Mods.Common.Traits.Render
|
|||||||
readonly SpriteFont font;
|
readonly SpriteFont font;
|
||||||
readonly Color color;
|
readonly Color color;
|
||||||
readonly string name;
|
readonly string name;
|
||||||
|
readonly IDecorationBounds[] decorationBounds;
|
||||||
|
|
||||||
public RenderNameTag(Actor self, RenderNameTagInfo info)
|
public RenderNameTag(Actor self, RenderNameTagInfo info)
|
||||||
{
|
{
|
||||||
@@ -42,15 +44,15 @@ namespace OpenRA.Mods.Common.Traits.Render
|
|||||||
name = self.Owner.PlayerName.Substring(0, info.MaxLength);
|
name = self.Owner.PlayerName.Substring(0, info.MaxLength);
|
||||||
else
|
else
|
||||||
name = self.Owner.PlayerName;
|
name = self.Owner.PlayerName;
|
||||||
|
|
||||||
|
decorationBounds = self.TraitsImplementing<IDecorationBounds>().ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<IRenderable> Render(Actor self, WorldRenderer wr)
|
public IEnumerable<IRenderable> Render(Actor self, WorldRenderer wr)
|
||||||
{
|
{
|
||||||
var pos = wr.ScreenPxPosition(self.CenterPosition);
|
var bounds = decorationBounds.Select(b => b.DecorationBounds(self, wr)).FirstOrDefault(b => !b.IsEmpty);
|
||||||
var bounds = self.SelectionOverlayBounds;
|
|
||||||
bounds.Offset(pos.X, pos.Y);
|
|
||||||
var spaceBuffer = (int)(10 / wr.Viewport.Zoom);
|
var spaceBuffer = (int)(10 / wr.Viewport.Zoom);
|
||||||
var effectPos = wr.ProjectedPosition(new int2(pos.X, bounds.Y - spaceBuffer));
|
var effectPos = wr.ProjectedPosition(new int2((bounds.Left + bounds.Right) / 2, bounds.Y - spaceBuffer));
|
||||||
|
|
||||||
return new IRenderable[] { new TextRenderable(font, effectPos, 0, color, name) };
|
return new IRenderable[] { new TextRenderable(font, effectPos, 0, color, name) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ using OpenRA.Traits;
|
|||||||
|
|
||||||
namespace OpenRA.Mods.Common.Traits.Render
|
namespace OpenRA.Mods.Common.Traits.Render
|
||||||
{
|
{
|
||||||
public class SelectionDecorationsInfo : ITraitInfo, ISelectionDecorationsInfo
|
public class SelectionDecorationsInfo : ITraitInfo, ISelectionDecorationsInfo, Requires<IDecorationBoundsInfo>
|
||||||
{
|
{
|
||||||
[PaletteReference] public readonly string Palette = "chrome";
|
[PaletteReference] public readonly string Palette = "chrome";
|
||||||
|
|
||||||
@@ -48,6 +48,7 @@ namespace OpenRA.Mods.Common.Traits.Render
|
|||||||
|
|
||||||
public readonly SelectionDecorationsInfo Info;
|
public readonly SelectionDecorationsInfo Info;
|
||||||
|
|
||||||
|
readonly IDecorationBounds[] decorationBounds;
|
||||||
readonly Animation pipImages;
|
readonly Animation pipImages;
|
||||||
IPips[] pipSources;
|
IPips[] pipSources;
|
||||||
|
|
||||||
@@ -55,6 +56,7 @@ namespace OpenRA.Mods.Common.Traits.Render
|
|||||||
{
|
{
|
||||||
Info = info;
|
Info = info;
|
||||||
|
|
||||||
|
decorationBounds = self.TraitsImplementing<IDecorationBounds>().ToArray();
|
||||||
pipImages = new Animation(self.World, Info.Image);
|
pipImages = new Animation(self.World, Info.Image);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +94,7 @@ namespace OpenRA.Mods.Common.Traits.Render
|
|||||||
var selected = self.World.Selection.Contains(self);
|
var selected = self.World.Selection.Contains(self);
|
||||||
var regularWorld = self.World.Type == WorldType.Regular;
|
var regularWorld = self.World.Type == WorldType.Regular;
|
||||||
var statusBars = Game.Settings.Game.StatusBars;
|
var statusBars = Game.Settings.Game.StatusBars;
|
||||||
|
var bounds = decorationBounds.Select(b => b.DecorationBounds(self, wr)).FirstOrDefault(b => !b.IsEmpty);
|
||||||
|
|
||||||
// Health bars are shown when:
|
// Health bars are shown when:
|
||||||
// * actor is selected
|
// * actor is selected
|
||||||
@@ -107,10 +110,10 @@ namespace OpenRA.Mods.Common.Traits.Render
|
|||||||
var displayExtra = selected || (regularWorld && statusBars != StatusBarsType.Standard);
|
var displayExtra = selected || (regularWorld && statusBars != StatusBarsType.Standard);
|
||||||
|
|
||||||
if (Info.RenderSelectionBox && selected)
|
if (Info.RenderSelectionBox && selected)
|
||||||
yield return new SelectionBoxRenderable(self, Info.SelectionBoxColor);
|
yield return new SelectionBoxRenderable(self, bounds, Info.SelectionBoxColor);
|
||||||
|
|
||||||
if (Info.RenderSelectionBars && (displayHealth || displayExtra))
|
if (Info.RenderSelectionBars && (displayHealth || displayExtra))
|
||||||
yield return new SelectionBarsRenderable(self, displayHealth, displayExtra);
|
yield return new SelectionBarsRenderable(self, bounds, displayHealth, displayExtra);
|
||||||
|
|
||||||
// Target lines and pips are always only displayed for selected allied actors
|
// Target lines and pips are always only displayed for selected allied actors
|
||||||
if (!selected || !self.Owner.IsAlliedWith(wr.World.RenderPlayer))
|
if (!selected || !self.Owner.IsAlliedWith(wr.World.RenderPlayer))
|
||||||
@@ -119,31 +122,28 @@ namespace OpenRA.Mods.Common.Traits.Render
|
|||||||
if (self.World.LocalPlayer != null && self.World.LocalPlayer.PlayerActor.Trait<DeveloperMode>().PathDebug)
|
if (self.World.LocalPlayer != null && self.World.LocalPlayer.PlayerActor.Trait<DeveloperMode>().PathDebug)
|
||||||
yield return new TargetLineRenderable(ActivityTargetPath(self), Color.Green);
|
yield return new TargetLineRenderable(ActivityTargetPath(self), Color.Green);
|
||||||
|
|
||||||
foreach (var r in DrawPips(self, wr))
|
foreach (var r in DrawPips(self, bounds, wr))
|
||||||
yield return r;
|
yield return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
IEnumerable<IRenderable> DrawPips(Actor self, WorldRenderer wr)
|
IEnumerable<IRenderable> DrawPips(Actor self, Rectangle bounds, WorldRenderer wr)
|
||||||
{
|
{
|
||||||
if (pipSources.Length == 0)
|
if (pipSources.Length == 0)
|
||||||
return Enumerable.Empty<IRenderable>();
|
return Enumerable.Empty<IRenderable>();
|
||||||
|
|
||||||
var b = self.SelectionOverlayBounds;
|
return DrawPipsInner(self, bounds, wr);
|
||||||
var pos = wr.ScreenPxPosition(self.CenterPosition);
|
|
||||||
var bl = wr.Viewport.WorldToViewPx(pos + new int2(b.Left, b.Bottom));
|
|
||||||
var pal = wr.Palette(Info.Palette);
|
|
||||||
|
|
||||||
return DrawPips(self, bl, pal);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IEnumerable<IRenderable> DrawPips(Actor self, int2 basePosition, PaletteReference palette)
|
IEnumerable<IRenderable> DrawPipsInner(Actor self, Rectangle bounds, WorldRenderer wr)
|
||||||
{
|
{
|
||||||
pipImages.PlayRepeating(PipStrings[0]);
|
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 pipSize = pipImages.Image.Size.XY.ToInt2();
|
||||||
var pipxyBase = basePosition + new int2(1 - pipSize.X / 2, -(3 + pipSize.Y / 2));
|
var pipxyBase = basePosition + new int2(1 - pipSize.X / 2, -(3 + pipSize.Y / 2));
|
||||||
var pipxyOffset = new int2(0, 0);
|
var pipxyOffset = new int2(0, 0);
|
||||||
var width = self.SelectionOverlayBounds.Width;
|
var width = bounds.Width;
|
||||||
|
|
||||||
foreach (var pips in pipSources)
|
foreach (var pips in pipSources)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ namespace OpenRA.Mods.Common.Traits.Render
|
|||||||
Right = 8,
|
Right = 8,
|
||||||
}
|
}
|
||||||
|
|
||||||
[Desc("Displays a custom UI overlay relative to the selection box.")]
|
[Desc("Displays a custom UI overlay relative to the actor's mouseover bounds.")]
|
||||||
public class WithDecorationInfo : ConditionalTraitInfo
|
public class WithDecorationInfo : ConditionalTraitInfo, Requires<IDecorationBoundsInfo>
|
||||||
{
|
{
|
||||||
[Desc("Image used for this decoration. Defaults to the actor's type.")]
|
[Desc("Image used for this decoration. Defaults to the actor's type.")]
|
||||||
public readonly string Image = null;
|
public readonly string Image = null;
|
||||||
@@ -58,7 +58,7 @@ namespace OpenRA.Mods.Common.Traits.Render
|
|||||||
public class WithDecoration : ConditionalTrait<WithDecorationInfo>, ITick, IRenderAboveShroud, IRenderAboveShroudWhenSelected
|
public class WithDecoration : ConditionalTrait<WithDecorationInfo>, ITick, IRenderAboveShroud, IRenderAboveShroudWhenSelected
|
||||||
{
|
{
|
||||||
protected readonly Animation Anim;
|
protected readonly Animation Anim;
|
||||||
|
readonly IDecorationBounds[] decorationBounds;
|
||||||
readonly string image;
|
readonly string image;
|
||||||
|
|
||||||
public WithDecoration(Actor self, WithDecorationInfo info)
|
public WithDecoration(Actor self, WithDecorationInfo info)
|
||||||
@@ -67,6 +67,7 @@ namespace OpenRA.Mods.Common.Traits.Render
|
|||||||
image = info.Image ?? self.Info.Name;
|
image = info.Image ?? self.Info.Name;
|
||||||
Anim = new Animation(self.World, image, () => self.World.Paused);
|
Anim = new Animation(self.World, image, () => self.World.Paused);
|
||||||
Anim.PlayRepeating(info.Sequence);
|
Anim.PlayRepeating(info.Sequence);
|
||||||
|
decorationBounds = self.TraitsImplementing<IDecorationBounds>().ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual bool ShouldRender(Actor self)
|
protected virtual bool ShouldRender(Actor self)
|
||||||
@@ -99,7 +100,7 @@ namespace OpenRA.Mods.Common.Traits.Render
|
|||||||
if (!ShouldRender(self) || self.World.FogObscures(self))
|
if (!ShouldRender(self) || self.World.FogObscures(self))
|
||||||
return Enumerable.Empty<IRenderable>();
|
return Enumerable.Empty<IRenderable>();
|
||||||
|
|
||||||
var bounds = self.SelectionOverlayBounds;
|
var bounds = decorationBounds.Select(b => b.DecorationBounds(self, wr)).FirstOrDefault(b => !b.IsEmpty);
|
||||||
var halfSize = (0.5f * Anim.Image.Size.XY).ToInt2();
|
var halfSize = (0.5f * Anim.Image.Size.XY).ToInt2();
|
||||||
|
|
||||||
var boundsOffset = new int2(bounds.Left + bounds.Right, bounds.Top + bounds.Bottom) / 2;
|
var boundsOffset = new int2(bounds.Left + bounds.Right, bounds.Top + bounds.Bottom) / 2;
|
||||||
@@ -126,7 +127,7 @@ namespace OpenRA.Mods.Common.Traits.Render
|
|||||||
sizeOffset -= new int2(halfSize.X, 0);
|
sizeOffset -= new int2(halfSize.X, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
var pxPos = wr.Viewport.WorldToViewPx(wr.ScreenPxPosition(self.CenterPosition) + boundsOffset) + sizeOffset;
|
var pxPos = wr.Viewport.WorldToViewPx(boundsOffset) + sizeOffset;
|
||||||
return new IRenderable[] { new UISpriteRenderable(Anim.Image, self.CenterPosition, pxPos, Info.ZOffset, wr.Palette(Info.Palette), 1f) };
|
return new IRenderable[] { new UISpriteRenderable(Anim.Image, self.CenterPosition, pxPos, Info.ZOffset, wr.Palette(Info.Palette), 1f) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ using OpenRA.Traits;
|
|||||||
namespace OpenRA.Mods.Common.Traits.Render
|
namespace OpenRA.Mods.Common.Traits.Render
|
||||||
{
|
{
|
||||||
[Desc("Renders Ctrl groups using pixel art.")]
|
[Desc("Renders Ctrl groups using pixel art.")]
|
||||||
public class WithSpriteControlGroupDecorationInfo : ITraitInfo
|
public class WithSpriteControlGroupDecorationInfo : ITraitInfo, Requires<IDecorationBoundsInfo>
|
||||||
{
|
{
|
||||||
[PaletteReference] public readonly string Palette = "chrome";
|
[PaletteReference] public readonly string Palette = "chrome";
|
||||||
|
|
||||||
@@ -38,12 +38,14 @@ namespace OpenRA.Mods.Common.Traits.Render
|
|||||||
public class WithSpriteControlGroupDecoration : IRenderAboveShroudWhenSelected
|
public class WithSpriteControlGroupDecoration : IRenderAboveShroudWhenSelected
|
||||||
{
|
{
|
||||||
public readonly WithSpriteControlGroupDecorationInfo Info;
|
public readonly WithSpriteControlGroupDecorationInfo Info;
|
||||||
|
readonly IDecorationBounds[] decorationBounds;
|
||||||
readonly Animation pipImages;
|
readonly Animation pipImages;
|
||||||
|
|
||||||
public WithSpriteControlGroupDecoration(Actor self, WithSpriteControlGroupDecorationInfo info)
|
public WithSpriteControlGroupDecoration(Actor self, WithSpriteControlGroupDecorationInfo info)
|
||||||
{
|
{
|
||||||
Info = info;
|
Info = info;
|
||||||
|
|
||||||
|
decorationBounds = self.TraitsImplementing<IDecorationBounds>().ToArray();
|
||||||
pipImages = new Animation(self.World, Info.Image);
|
pipImages = new Animation(self.World, Info.Image);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +70,7 @@ namespace OpenRA.Mods.Common.Traits.Render
|
|||||||
|
|
||||||
pipImages.PlayFetchIndex(Info.GroupSequence, () => (int)group);
|
pipImages.PlayFetchIndex(Info.GroupSequence, () => (int)group);
|
||||||
|
|
||||||
var bounds = self.SelectionOverlayBounds;
|
var bounds = decorationBounds.Select(b => b.DecorationBounds(self, wr)).FirstOrDefault(b => !b.IsEmpty);
|
||||||
var boundsOffset = 0.5f * new float2(bounds.Left + bounds.Right, bounds.Top + bounds.Bottom);
|
var boundsOffset = 0.5f * new float2(bounds.Left + bounds.Right, bounds.Top + bounds.Bottom);
|
||||||
if (Info.ReferencePoint.HasFlag(ReferencePoints.Top))
|
if (Info.ReferencePoint.HasFlag(ReferencePoints.Top))
|
||||||
boundsOffset -= new float2(0, 0.5f * bounds.Height);
|
boundsOffset -= new float2(0, 0.5f * bounds.Height);
|
||||||
@@ -82,7 +84,7 @@ namespace OpenRA.Mods.Common.Traits.Render
|
|||||||
if (Info.ReferencePoint.HasFlag(ReferencePoints.Right))
|
if (Info.ReferencePoint.HasFlag(ReferencePoints.Right))
|
||||||
boundsOffset += new float2(0.5f * bounds.Width, 0);
|
boundsOffset += new float2(0.5f * bounds.Width, 0);
|
||||||
|
|
||||||
var pxPos = wr.Viewport.WorldToViewPx(wr.ScreenPxPosition(self.CenterPosition) + boundsOffset.ToInt2()) - (0.5f * pipImages.Image.Size.XY).ToInt2();
|
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);
|
yield return new UISpriteRenderable(pipImages.Image, self.CenterPosition, pxPos, 0, palette, 1f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
|
using System.Linq;
|
||||||
using OpenRA.Graphics;
|
using OpenRA.Graphics;
|
||||||
using OpenRA.Mods.Common.Graphics;
|
using OpenRA.Mods.Common.Graphics;
|
||||||
using OpenRA.Traits;
|
using OpenRA.Traits;
|
||||||
@@ -18,7 +19,7 @@ using OpenRA.Traits;
|
|||||||
namespace OpenRA.Mods.Common.Traits.Render
|
namespace OpenRA.Mods.Common.Traits.Render
|
||||||
{
|
{
|
||||||
[Desc("Renders Ctrl groups using typeface.")]
|
[Desc("Renders Ctrl groups using typeface.")]
|
||||||
public class WithTextControlGroupDecorationInfo : ITraitInfo, IRulesetLoaded
|
public class WithTextControlGroupDecorationInfo : ITraitInfo, IRulesetLoaded, Requires<IDecorationBoundsInfo>
|
||||||
{
|
{
|
||||||
public readonly string Font = "TinyBold";
|
public readonly string Font = "TinyBold";
|
||||||
|
|
||||||
@@ -50,6 +51,7 @@ namespace OpenRA.Mods.Common.Traits.Render
|
|||||||
public class WithTextControlGroupDecoration : IRenderAboveShroudWhenSelected, INotifyOwnerChanged
|
public class WithTextControlGroupDecoration : IRenderAboveShroudWhenSelected, INotifyOwnerChanged
|
||||||
{
|
{
|
||||||
readonly WithTextControlGroupDecorationInfo info;
|
readonly WithTextControlGroupDecorationInfo info;
|
||||||
|
readonly IDecorationBounds[] decorationBounds;
|
||||||
readonly SpriteFont font;
|
readonly SpriteFont font;
|
||||||
|
|
||||||
Color color;
|
Color color;
|
||||||
@@ -61,6 +63,7 @@ namespace OpenRA.Mods.Common.Traits.Render
|
|||||||
if (!Game.Renderer.Fonts.TryGetValue(info.Font, out font))
|
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));
|
throw new YamlException("Font '{0}' is not listed in the mod.yaml's Fonts section".F(info.Font));
|
||||||
|
|
||||||
|
decorationBounds = self.TraitsImplementing<IDecorationBounds>().ToArray();
|
||||||
color = info.UsePlayerColor ? self.Owner.Color.RGB : info.Color;
|
color = info.UsePlayerColor ? self.Owner.Color.RGB : info.Color;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +85,7 @@ namespace OpenRA.Mods.Common.Traits.Render
|
|||||||
if (group == null)
|
if (group == null)
|
||||||
yield break;
|
yield break;
|
||||||
|
|
||||||
var bounds = self.SelectionOverlayBounds;
|
var bounds = decorationBounds.Select(b => b.DecorationBounds(self, wr)).FirstOrDefault(b => !b.IsEmpty);
|
||||||
var number = group.Value.ToString();
|
var number = group.Value.ToString();
|
||||||
var halfSize = font.Measure(number) / 2;
|
var halfSize = font.Measure(number) / 2;
|
||||||
|
|
||||||
@@ -110,7 +113,7 @@ namespace OpenRA.Mods.Common.Traits.Render
|
|||||||
sizeOffset -= new int2(halfSize.X, 0);
|
sizeOffset -= new int2(halfSize.X, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
var screenPos = wr.ScreenPxPosition(self.CenterPosition) + boundsOffset + sizeOffset + info.ScreenOffset;
|
var screenPos = boundsOffset + sizeOffset + info.ScreenOffset;
|
||||||
|
|
||||||
yield return new TextRenderable(font, wr.ProjectedPosition(screenPos), info.ZOffset, color, number);
|
yield return new TextRenderable(font, wr.ProjectedPosition(screenPos), info.ZOffset, color, number);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ using OpenRA.Traits;
|
|||||||
namespace OpenRA.Mods.Common.Traits.Render
|
namespace OpenRA.Mods.Common.Traits.Render
|
||||||
{
|
{
|
||||||
[Desc("Displays a text overlay relative to the selection box.")]
|
[Desc("Displays a text overlay relative to the selection box.")]
|
||||||
public class WithTextDecorationInfo : ConditionalTraitInfo
|
public class WithTextDecorationInfo : ConditionalTraitInfo, Requires<IDecorationBoundsInfo>
|
||||||
{
|
{
|
||||||
[FieldLoader.Require] [Translate] public readonly string Text = null;
|
[FieldLoader.Require] [Translate] public readonly string Text = null;
|
||||||
|
|
||||||
@@ -59,12 +59,14 @@ namespace OpenRA.Mods.Common.Traits.Render
|
|||||||
public class WithTextDecoration : ConditionalTrait<WithTextDecorationInfo>, IRender, IRenderAboveShroudWhenSelected, INotifyOwnerChanged
|
public class WithTextDecoration : ConditionalTrait<WithTextDecorationInfo>, IRender, IRenderAboveShroudWhenSelected, INotifyOwnerChanged
|
||||||
{
|
{
|
||||||
readonly SpriteFont font;
|
readonly SpriteFont font;
|
||||||
|
readonly IDecorationBounds[] decorationBounds;
|
||||||
Color color;
|
Color color;
|
||||||
|
|
||||||
public WithTextDecoration(Actor self, WithTextDecorationInfo info)
|
public WithTextDecoration(Actor self, WithTextDecorationInfo info)
|
||||||
: base(info)
|
: base(info)
|
||||||
{
|
{
|
||||||
font = Game.Renderer.Fonts[info.Font];
|
font = Game.Renderer.Fonts[info.Font];
|
||||||
|
decorationBounds = self.TraitsImplementing<IDecorationBounds>().ToArray();
|
||||||
color = Info.UsePlayerColor ? self.Owner.Color.RGB : Info.Color;
|
color = Info.UsePlayerColor ? self.Owner.Color.RGB : Info.Color;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +103,7 @@ namespace OpenRA.Mods.Common.Traits.Render
|
|||||||
if (!ShouldRender(self) || self.World.FogObscures(self))
|
if (!ShouldRender(self) || self.World.FogObscures(self))
|
||||||
return Enumerable.Empty<IRenderable>();
|
return Enumerable.Empty<IRenderable>();
|
||||||
|
|
||||||
var bounds = self.SelectionOverlayBounds;
|
var bounds = decorationBounds.Select(b => b.DecorationBounds(self, wr)).FirstOrDefault(b => !b.IsEmpty);
|
||||||
var halfSize = font.Measure(Info.Text) / 2;
|
var halfSize = font.Measure(Info.Text) / 2;
|
||||||
|
|
||||||
var boundsOffset = new int2(bounds.Left + bounds.Right, bounds.Top + bounds.Bottom) / 2;
|
var boundsOffset = new int2(bounds.Left + bounds.Right, bounds.Top + bounds.Bottom) / 2;
|
||||||
@@ -128,8 +130,7 @@ namespace OpenRA.Mods.Common.Traits.Render
|
|||||||
sizeOffset -= new int2(halfSize.X, 0);
|
sizeOffset -= new int2(halfSize.X, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
var screenPos = wr.ScreenPxPosition(self.CenterPosition) + boundsOffset + sizeOffset;
|
return new IRenderable[] { new TextRenderable(font, wr.ProjectedPosition(boundsOffset + sizeOffset), Info.ZOffset, color, Info.Text) };
|
||||||
return new IRenderable[] { new TextRenderable(font, wr.ProjectedPosition(screenPos), Info.ZOffset, color, Info.Text) };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner)
|
void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner)
|
||||||
|
|||||||
@@ -134,7 +134,12 @@ namespace OpenRA.Mods.Common.Traits
|
|||||||
{
|
{
|
||||||
var xy = wr.Viewport.ViewToWorld(Viewport.LastMousePos);
|
var xy = wr.Viewport.ViewToWorld(Viewport.LastMousePos);
|
||||||
foreach (var unit in power.UnitsInRange(xy))
|
foreach (var unit in power.UnitsInRange(xy))
|
||||||
yield return new SelectionBoxRenderable(unit, Color.Red);
|
{
|
||||||
|
var bounds = unit.TraitsImplementing<IDecorationBounds>()
|
||||||
|
.Select(b => b.DecorationBounds(unit, wr))
|
||||||
|
.FirstOrDefault(b => !b.IsEmpty);
|
||||||
|
yield return new SelectionBoxRenderable(unit, bounds, Color.Red);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<IRenderable> Render(WorldRenderer wr, World world)
|
public IEnumerable<IRenderable> Render(WorldRenderer wr, World world)
|
||||||
|
|||||||
@@ -50,7 +50,13 @@ namespace OpenRA.Mods.Common.Widgets
|
|||||||
{
|
{
|
||||||
// TODO: Integrate this with SelectionDecorations to unhardcode the *Renderable
|
// TODO: Integrate this with SelectionDecorations to unhardcode the *Renderable
|
||||||
if (unit.Info.HasTraitInfo<SelectableInfo>())
|
if (unit.Info.HasTraitInfo<SelectableInfo>())
|
||||||
new SelectionBarsRenderable(unit, true, true).Render(worldRenderer);
|
{
|
||||||
|
var bounds = unit.TraitsImplementing<IDecorationBounds>()
|
||||||
|
.Select(b => b.DecorationBounds(unit, worldRenderer))
|
||||||
|
.FirstOrDefault(b => !b.IsEmpty);
|
||||||
|
|
||||||
|
new SelectionBarsRenderable(unit, bounds, true, true).Render(worldRenderer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Draw()
|
public override void Draw()
|
||||||
|
|||||||
Reference in New Issue
Block a user