Files
OpenRA/OpenRA.Game/Traits/Player/FrozenActorLayer.cs
RoosterDragon 1fc1bdc849 Fix frozen actors lacking tooltips if they have the cloak ability.
Since bbf5970bc1 we update frozen actors only when required.

In 8339c6843e a regression was fixed where actors created in line of sight would be invisible.

Here, we fix a related regression where cloaked units that are revealed, and then frozen when you move out of line of sight would lack tooltips.

The fix centers around the setting of the Hidden flag. In the old code this used CanBeViewedByPlayer which checks for visibility modifiers and then uses the default visibility. The bug with this code is that when a visibility modifier was not hiding the actor, then we would report the default visibility state instead. However the frozen visibility state applies here which means if the frozen actor is visible, then we consider the actor to be hidden and therefore tooltips will not appear. In the fixed version we only consider the modifiers. This means a visibility modifier such as Cloak can hide the frozen actor tooltips. But otherwise we do not consider the frozen actor to be hidden. This prevents a frozen actor from hiding its own tooltips in some unintended circular logic. Hidden now becomes just a flag to indicate if the visibility modifiers are overriding things, as intended.
2022-08-31 23:31:48 +03:00

384 lines
10 KiB
C#

#region Copyright & License Information
/*
* Copyright 2007-2022 The OpenRA Developers (see AUTHORS)
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Primitives;
namespace OpenRA.Traits
{
public interface ICreatesFrozenActors
{
void OnVisibilityChanged(FrozenActor frozen);
}
[TraitLocation(SystemActors.Player)]
[Desc("Required for FrozenUnderFog to work. Attach this to the player actor.")]
public class FrozenActorLayerInfo : TraitInfo, Requires<ShroudInfo>
{
[Desc("Size of partition bins (cells)")]
public readonly int BinSize = 10;
public override object Create(ActorInitializer init) { return new FrozenActorLayer(init.Self, this); }
}
public class FrozenActor
{
public readonly PPos[] Footprint;
public readonly WPos CenterPosition;
readonly Actor actor;
readonly ICreatesFrozenActors frozenTrait;
readonly Player viewer;
readonly Shroud shroud;
readonly List<WPos> targetablePositions = new List<WPos>();
public Player Owner { get; private set; }
public BitSet<TargetableType> TargetTypes { get; private set; }
public IEnumerable<WPos> TargetablePositions => targetablePositions;
public ITooltipInfo TooltipInfo { get; private set; }
public Player TooltipOwner { get; private set; }
readonly ITooltip[] tooltips;
public int HP { get; private set; }
public DamageState DamageState { get; private set; }
readonly IHealth health;
readonly IVisibilityModifier[] visibilityModifiers;
// The Visible flag is tied directly to the actor visibility under the fog.
// If Visible is true, the actor is made invisible (via FrozenUnderFog/IDefaultVisibility)
// and this FrozenActor is rendered instead.
// The Hidden flag covers the edge case that occurs when the backing actor was last "seen"
// but not actually visible because a visibility modifier hid the actor. Setting Visible to
// true when the actor is hidden under the fog would leak the actors position via the
// tooltips and AutoTargetability, and keeping Visible as false would cause the actor to be
// rendered under the fog.
public bool Visible { get; private set; } = true;
public bool Hidden { get; private set; } = false;
public bool Shrouded { get; private set; }
public bool NeedRenderables { get; set; }
public IRenderable[] Renderables = NoRenderables;
public Rectangle[] ScreenBounds = NoBounds;
public Polygon MouseBounds = Polygon.Empty;
static readonly IRenderable[] NoRenderables = Array.Empty<IRenderable>();
static readonly Rectangle[] NoBounds = Array.Empty<Rectangle>();
int flashTicks;
TintModifiers flashModifiers;
float3 flashTint;
float? flashAlpha;
public FrozenActor(Actor actor, ICreatesFrozenActors frozenTrait, PPos[] footprint, Player viewer, bool startsRevealed)
{
this.actor = actor;
this.frozenTrait = frozenTrait;
this.viewer = viewer;
shroud = viewer.Shroud;
NeedRenderables = startsRevealed;
// Consider all cells inside the map area (ignoring the current map bounds)
Footprint = footprint
.Where(m => shroud.Contains(m))
.ToArray();
if (Footprint.Length == 0)
throw new ArgumentException(("This frozen actor has no footprint.\n" +
"Actor Name: {0}\n" +
"Actor Location: {1}\n" +
"Input footprint: [{2}]\n" +
"Input footprint (after shroud.Contains): [{3}]")
.F(actor.Info.Name,
actor.Location.ToString(),
footprint.Select(p => p.ToString()).JoinWith("|"),
footprint.Select(p => shroud.Contains(p).ToString()).JoinWith("|")));
CenterPosition = actor.CenterPosition;
tooltips = actor.TraitsImplementing<ITooltip>().ToArray();
health = actor.TraitOrDefault<IHealth>();
visibilityModifiers = actor.TraitsImplementing<IVisibilityModifier>().ToArray();
UpdateVisibility();
}
public uint ID => actor.ActorID;
public bool IsValid => Owner != null;
public ActorInfo Info => actor.Info;
public Actor Actor => !actor.IsDead ? actor : null;
public Player Viewer => viewer;
public void RefreshState()
{
Owner = actor.Owner;
TargetTypes = actor.GetEnabledTargetTypes();
targetablePositions.Clear();
targetablePositions.AddRange(actor.GetTargetablePositions());
if (health != null)
{
HP = health.HP;
DamageState = health.DamageState;
}
var tooltip = tooltips.FirstEnabledTraitOrDefault();
if (tooltip != null)
{
TooltipInfo = tooltip.TooltipInfo;
TooltipOwner = tooltip.Owner;
}
}
public void RefreshHidden()
{
Hidden = false;
foreach (var visibilityModifier in visibilityModifiers)
{
if (!visibilityModifier.IsVisible(actor, viewer))
{
Hidden = true;
break;
}
}
}
public void Tick()
{
if (flashTicks > 0)
flashTicks--;
}
public void UpdateVisibility()
{
var wasVisible = Visible;
Shrouded = true;
Visible = true;
// PERF: Avoid LINQ.
foreach (var puv in Footprint)
{
var cv = shroud.GetVisibility(puv);
if (cv.HasFlag(Shroud.CellVisibility.Visible))
{
Visible = false;
Shrouded = false;
break;
}
if (Shrouded && cv.HasFlag(Shroud.CellVisibility.Explored))
Shrouded = false;
}
// Force the backing trait to update so other actors can't
// query inconsistent state (both hidden or both visible)
if (Visible != wasVisible)
frozenTrait.OnVisibilityChanged(this);
NeedRenderables |= Visible && !wasVisible;
}
public void Invalidate()
{
Owner = null;
}
public void Flash(Color color, float alpha)
{
flashTicks = 5;
flashModifiers = TintModifiers.ReplaceColor;
flashTint = new float3(color.R, color.G, color.B) / 255f;
flashAlpha = alpha;
}
public void Flash(float3 tint)
{
flashTicks = 5;
flashModifiers = TintModifiers.None;
flashTint = tint;
flashAlpha = null;
}
public IEnumerable<IRenderable> Render()
{
if (Shrouded)
return NoRenderables;
if (flashTicks > 0 && flashTicks % 2 == 0)
{
return Renderables.Concat(Renderables.Where(r => !r.IsDecoration && r is IModifyableRenderable)
.Select(r =>
{
var mr = (IModifyableRenderable)r;
mr = mr.WithTint(flashTint, mr.TintModifiers | flashModifiers);
if (flashAlpha.HasValue)
mr = mr.WithAlpha(flashAlpha.Value);
return mr;
}));
}
return Renderables;
}
public bool HasRenderables => !Shrouded && Renderables.Length > 0;
public override string ToString()
{
return $"{Info.Name} {ID}{(IsValid ? "" : " (invalid)")}";
}
}
public class FrozenActorLayer : IRender, ITick, ISync
{
[Sync]
public int VisibilityHash;
[Sync]
public int FrozenHash;
readonly int binSize;
readonly World world;
readonly Player owner;
readonly Dictionary<uint, FrozenActor> frozenActorsById;
readonly SpatiallyPartitioned<uint> partitionedFrozenActorIds;
readonly HashSet<uint> dirtyFrozenActorIds = new HashSet<uint>();
public FrozenActorLayer(Actor self, FrozenActorLayerInfo info)
{
binSize = info.BinSize;
world = self.World;
owner = self.Owner;
frozenActorsById = new Dictionary<uint, FrozenActor>();
partitionedFrozenActorIds = new SpatiallyPartitioned<uint>(
world.Map.MapSize.X, world.Map.MapSize.Y, binSize);
self.Trait<Shroud>().OnShroudChanged += uv => dirtyFrozenActorIds.UnionWith(partitionedFrozenActorIds.At(new int2(uv.U, uv.V)));
}
public void Add(FrozenActor fa)
{
frozenActorsById.Add(fa.ID, fa);
world.ScreenMap.AddOrUpdate(owner, fa);
partitionedFrozenActorIds.Add(fa.ID, FootprintBounds(fa));
}
public void Remove(FrozenActor fa)
{
partitionedFrozenActorIds.Remove(fa.ID);
world.ScreenMap.Remove(owner, fa);
frozenActorsById.Remove(fa.ID);
}
Rectangle FootprintBounds(FrozenActor fa)
{
var p1 = fa.Footprint[0];
var minU = p1.U;
var maxU = p1.U;
var minV = p1.V;
var maxV = p1.V;
foreach (var p in fa.Footprint)
{
if (minU > p.U)
minU = p.U;
else if (maxU < p.U)
maxU = p.U;
if (minV > p.V)
minV = p.V;
else if (maxV < p.V)
maxV = p.V;
}
return Rectangle.FromLTRB(minU, minV, maxU + 1, maxV + 1);
}
void ITick.Tick(Actor self)
{
var frozenActorsToRemove = new List<FrozenActor>();
VisibilityHash = 0;
FrozenHash = 0;
foreach (var kvp in frozenActorsById)
{
var id = kvp.Key;
var hash = (int)id;
FrozenHash += hash;
var frozenActor = kvp.Value;
frozenActor.Tick();
if (dirtyFrozenActorIds.Contains(id))
frozenActor.UpdateVisibility();
if (frozenActor.Visible)
VisibilityHash += hash;
else if (frozenActor.Actor == null)
frozenActorsToRemove.Add(frozenActor);
}
dirtyFrozenActorIds.Clear();
foreach (var fa in frozenActorsToRemove)
Remove(fa);
}
public virtual IEnumerable<IRenderable> Render(Actor self, WorldRenderer wr)
{
return world.ScreenMap.RenderableFrozenActorsInBox(owner, wr.Viewport.TopLeft, wr.Viewport.BottomRight)
.Where(f => f.Visible)
.SelectMany(ff => ff.Render());
}
public IEnumerable<Rectangle> ScreenBounds(Actor self, WorldRenderer wr)
{
// Player-actor render traits don't require screen bounds
yield break;
}
public FrozenActor FromID(uint id)
{
if (!frozenActorsById.TryGetValue(id, out var fa))
return null;
return fa;
}
public IEnumerable<FrozenActor> FrozenActorsInRegion(CellRegion region, bool onlyVisible = true)
{
var tl = region.TopLeft;
var br = region.BottomRight;
return partitionedFrozenActorIds.InBox(Rectangle.FromLTRB(tl.X, tl.Y, br.X, br.Y))
.Select(FromID)
.Where(fa => fa.IsValid && (!onlyVisible || fa.Visible));
}
public IEnumerable<FrozenActor> FrozenActorsInCircle(World world, WPos origin, WDist r, bool onlyVisible = true)
{
var centerCell = world.Map.CellContaining(origin);
var cellRange = (r.Length + 1023) / 1024;
var tl = centerCell - new CVec(cellRange, cellRange);
var br = centerCell + new CVec(cellRange, cellRange);
// Target ranges are calculated in 2D, so ignore height differences
return partitionedFrozenActorIds.InBox(Rectangle.FromLTRB(tl.X, tl.Y, br.X, br.Y))
.Select(FromID)
.Where(fa => fa.IsValid &&
(!onlyVisible || fa.Visible) &&
(fa.CenterPosition - origin).HorizontalLengthSquared <= r.LengthSquared);
}
}
}