Revert "Directional visibility for wall-mounted entities" (#44770)

Revert "Directional visibility for wall-mounted entities (#43476)"

This reverts commit c17429daa4.
This commit is contained in:
slarticodefast 2026-07-19 17:00:34 +02:00 • committed by GitHub
parent 63765280a3
commit f3cff7bb8c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 13 additions and 586 deletions

View file

@ -1,11 +0,0 @@
using Content.Shared.Wall;
using Robust.Shared.ComponentTrees;
using Robust.Shared.Physics;
namespace Content.Client.Wall.Components;
[RegisterComponent]
public sealed partial class WallMountTreeComponent : Component, IComponentTreeComponent<WallMountComponent>
{
public DynamicTree<ComponentTreeEntry<WallMountComponent>> Tree { get; set; } = default!;
}

View file

@ -1,22 +0,0 @@
using System.Numerics;
using Content.Client.Wall.Components;
using Content.Shared.Wall;
using Robust.Client.GameObjects;
using Robust.Shared.ComponentTrees;
using Robust.Shared.Physics;
namespace Content.Client.Wall.Systems;
public sealed partial class WallMountTreeSystem : ComponentTreeSystem<WallMountTreeComponent, WallMountComponent>
{
[Dependency] private SpriteSystem _sprite = default!;
protected override bool DoFrameUpdate => true;
protected override bool DoTickUpdate => false;
protected override bool Recursive => false;
protected override Box2 ExtractAabb(in ComponentTreeEntry<WallMountComponent> entry, Vector2 pos, Angle rot)
{
return _sprite.CalculateBounds((entry.Uid, Comp<SpriteComponent>(entry.Uid)), pos, rot, default).CalcBoundingBox();
}
}

View file

@ -1,187 +0,0 @@
using System.Linq;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Content.Shared.Tag;
using Content.Shared.Wall;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Configuration;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.Wall.Systems;
/// <summary>
/// Manages the directional visibility overlay for wall-mounted entities.
/// </summary>
public sealed partial class WallMountVisibilitySystem : EntitySystem
{
[Dependency] private IConfigurationManager _cfg = default!;
[Dependency] private IGameTiming _timing = default!;
[Dependency] private IOverlayManager _overlay = default!;
[Dependency] private SharedMapSystem _map = default!;
[Dependency] private SpriteSystem _sprite = default!;
[Dependency] private TagSystem _tag = default!;
[Dependency] private TransformSystem _xform = default!;
[Dependency] private WallMountTreeSystem _tree = default!;
[Dependency] private EntityQuery<MapGridComponent> _gridQuery = default!;
[Dependency] private EntityQuery<SpriteComponent> _spriteQuery = default!;
/// <summary>
/// Tags that block visibility when present on the same tile.
/// </summary>
private static readonly ProtoId<TagPrototype>[] BlockingTags = ["Wall"];
/// <summary>
/// Caches for whether a tile has any blocking entity.
/// </summary>
private readonly Dictionary<(EntityUid Grid, Vector2i Tile), bool> _tileCache = [];
private WallMountVisibilityOverlay _overlayInstance = default!;
/// <summary>
/// Whether directional visibility is currently enabled.
/// </summary>
internal bool DirectionalVisibilityEnabled = true;
public override void Initialize()
{
base.Initialize();
_overlayInstance = new WallMountVisibilityOverlay(_timing, _map, _sprite, _xform, _tree, this, _gridQuery, _spriteQuery);
Subs.CVar(_cfg, CCVars.WallMountDirectionalVisibility, OnDirectionalVisibilityChanged, true);
}
public override void Shutdown()
{
base.Shutdown();
_overlay.RemoveOverlay(_overlayInstance);
}
private void OnDirectionalVisibilityChanged(bool enabled)
{
DirectionalVisibilityEnabled = enabled;
if (enabled)
_overlay.AddOverlay(_overlayInstance);
else
{
_overlay.RemoveOverlay(_overlayInstance);
SetAllVisible(true);
}
}
/// <summary>
/// Invalidates tile cache when anchor state changes for a blocking entity.
/// </summary>
[SubscribeLocalEvent]
private void OnTagAnchorChanged(Entity<TagComponent> ent, ref AnchorStateChangedEvent args)
{
if (!_tag.HasAnyTag(ent.Comp, BlockingTags))
return;
var xform = args.Transform;
if (xform.GridUid is not { } gridUid)
return;
if (!_gridQuery.TryGetComponent(gridUid, out var grid))
return;
var tile = _map.TileIndicesFor(gridUid, grid, xform.Coordinates);
_tileCache.Remove((gridUid, tile));
}
/// <summary>
/// Makes the entity visible again on component shutdown.
/// </summary>
[SubscribeLocalEvent]
private void OnWallMountShutdown(Entity<WallMountComponent> ent, ref ComponentShutdown args)
{
if (TerminatingOrDeleted(ent))
return;
if (!TryComp<SpriteComponent>(ent, out var sprite))
return;
_sprite.SetVisible((ent, sprite), true);
}
/// <summary>
/// Makes the entity visible again if directional visibility is disabled for this mount.
/// </summary>
[SubscribeLocalEvent]
private void OnWallMountAfterHandleState(Entity<WallMountComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (ent.Comp.DirectionalVisibility)
return;
if (!TryComp<SpriteComponent>(ent, out var sprite))
return;
_sprite.SetVisible((ent, sprite), true);
}
/// <summary>
/// Removes all cached entries for a grid that is being removed.
/// </summary>
[SubscribeLocalEvent]
private void OnGridRemoval(GridRemovalEvent ev)
{
foreach (var key in _tileCache.Keys.Where(k => k.Grid == ev.EntityUid).ToList())
{
_tileCache.Remove(key);
}
}
/// <summary>
/// Clears tile cache and resets all wall-mount visibility on round restart.
/// </summary>
[SubscribeNetworkEvent]
private void OnRoundRestart(RoundRestartCleanupEvent ev)
{
_tileCache.Clear();
SetAllVisible(true);
}
/// <summary>
/// Forces all wall-mount entities to become visible or hidden.
/// </summary>
internal void SetAllVisible(bool visible)
{
var query = AllEntityQuery<WallMountComponent, SpriteComponent>();
while (query.MoveNext(out var uid, out _, out var sprite))
{
_sprite.SetVisible((uid, sprite), visible);
}
}
/// <summary>
/// Checks whether the tile contains any anchored blocking entity.
/// </summary>
internal bool IsTileBlocked(EntityUid gridUid, Vector2i tile, EntityUid? ignoreUid = null)
{
if (!_gridQuery.TryGetComponent(gridUid, out var grid))
return false;
var key = (gridUid, tile);
if (_tileCache.TryGetValue(key, out var cached))
return cached;
var enumerator = _map.GetAnchoredEntitiesEnumerator(gridUid, grid, tile);
while (enumerator.MoveNext(out var anchored))
{
if (anchored == ignoreUid)
continue;
if (!_tag.HasAnyTag(anchored.Value, BlockingTags))
continue;
return _tileCache[key] = true;
}
return _tileCache[key] = false;
}
}

View file

@ -1,88 +0,0 @@
using Robust.Client.GameObjects;
namespace Content.Client.Wall;
/// <summary>
/// Tracks fade progress for a wall-mounted entity.
/// </summary>
internal struct WallMountFadeState
{
public float OriginalAlpha;
public float CurrentAlpha;
public float TargetAlpha;
/// <summary>
/// Sprite alpha after applying the fade multiplier.
/// </summary>
public readonly float EffectiveAlpha => OriginalAlpha * CurrentAlpha;
/// <summary>
/// Whether the entity should be rendered as visible at the current fade level.
/// </summary>
public readonly bool IsVisible => CurrentAlpha > 0f;
/// <summary>
/// Creates a new fade state snapped immediately to the given target alpha.
/// </summary>
public static WallMountFadeState Snapped(float originalAlpha, float targetAlpha)
{
return new()
{
OriginalAlpha = originalAlpha,
CurrentAlpha = targetAlpha,
TargetAlpha = targetAlpha,
};
}
/// <summary>
/// Moves <see cref="CurrentAlpha"/> one step towards <see cref="TargetAlpha"/>.
/// </summary>
public void StepTowards(float fadeStep)
{
if (MathHelper.CloseTo(CurrentAlpha, TargetAlpha))
return;
var delta = TargetAlpha - CurrentAlpha;
var step = MathF.Sign(delta) * MathF.Min(MathF.Abs(delta), fadeStep);
CurrentAlpha = Math.Clamp(CurrentAlpha + step, 0f, 1f);
}
}
/// <summary>
/// Per-viewport fade state for wall-mounted entities.
/// </summary>
internal sealed class ViewportFadeState(SpriteSystem sprite, EntityQuery<SpriteComponent> spriteQuery) : IDisposable
{
private readonly SpriteSystem _sprite = sprite;
private readonly EntityQuery<SpriteComponent> _spriteQuery = spriteQuery;
/// <summary>
/// Fade states for entities tracked in this viewport.
/// </summary>
public readonly Dictionary<EntityUid, WallMountFadeState> FadeStates = [];
/// <summary>
/// Entities visible this frame.
/// </summary>
public readonly HashSet<EntityUid> SeenThisFrame = [];
/// <summary>
/// Whether FOV was enabled during the previous frame.
/// </summary>
public bool WasFovEnabled = true;
public void Dispose()
{
foreach (var (uid, state) in FadeStates)
{
if (!_spriteQuery.TryGetComponent(uid, out var sprite))
continue;
_sprite.SetColor((uid, sprite), sprite.Color.WithAlpha(state.OriginalAlpha));
_sprite.SetVisible((uid, sprite), true);
}
FadeStates.Clear();
SeenThisFrame.Clear();
}
}

View file

@ -1,242 +0,0 @@
using System.Linq;
using System.Numerics;
using Content.Client.Graphics;
using Content.Client.Wall.Systems;
using Content.Shared.Wall;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.Graphics;
using Robust.Shared.Map.Components;
using Robust.Shared.Timing;
namespace Content.Client.Wall;
/// <summary>
/// Renders wall-mounted entities conditionally based on their facing arc relative to the viewport's eye.
/// </summary>
public sealed partial class WallMountVisibilityOverlay(
IGameTiming timing,
SharedMapSystem map,
SpriteSystem sprite,
TransformSystem xform,
WallMountTreeSystem tree,
WallMountVisibilitySystem visibility,
EntityQuery<MapGridComponent> gridQuery,
EntityQuery<SpriteComponent> spriteQuery) : Overlay
{
private readonly IGameTiming _timing = timing;
private readonly SharedMapSystem _map = map;
private readonly SpriteSystem _sprite = sprite;
private readonly TransformSystem _xform = xform;
private readonly WallMountTreeSystem _tree = tree;
private readonly WallMountVisibilitySystem _visibility = visibility;
private readonly EntityQuery<MapGridComponent> _gridQuery = gridQuery;
private readonly EntityQuery<SpriteComponent> _spriteQuery = spriteQuery;
/// <summary>
/// Caches <see cref="ViewportFadeState"/> instances per viewport.
/// </summary>
private readonly OverlayResourceCache<ViewportFadeState> _fadeCache = new();
/// <summary>
/// Original sprite alphas, shared across viewports to avoid capturing values modified by another viewport.
/// </summary>
private readonly Dictionary<EntityUid, float> _originalAlphas = [];
/// <summary>
/// Alpha change per second during fade.
/// </summary>
private const float FadeSpeed = 9f;
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowEntities;
protected override void Draw(in OverlayDrawArgs args)
{
if (!_visibility.DirectionalVisibilityEnabled)
return;
if (args.Viewport.Eye is not { } eye)
return;
var viewportState = _fadeCache.GetForViewport(args.Viewport, _ => new ViewportFadeState(_sprite, _spriteQuery));
if (!eye.DrawFov)
{
HandleFovDisabled(args, viewportState);
return;
}
viewportState.WasFovEnabled = true;
var fadeStep = FadeSpeed * (float)_timing.FrameTime.TotalSeconds;
var matrix = args.Viewport.GetWorldToLocalMatrix();
viewportState.SeenThisFrame.Clear();
ProcessVisibleEntities(args, eye, matrix, fadeStep, viewportState);
// Remove entities that left the viewport this frame.
foreach (var uid in viewportState.FadeStates.Keys.Except(viewportState.SeenThisFrame).ToList())
{
RemoveTrackedEntity(uid, viewportState);
}
ApplyFadeToVisibleEntities(viewportState);
}
/// <summary>
/// When FOV gets disabled, clears fade state and restores sprites.
/// On subsequent frames, keeps wall-mounts visible and restores alpha modified by other viewports.
/// </summary>
private void HandleFovDisabled(in OverlayDrawArgs args, ViewportFadeState viewportState)
{
if (viewportState.WasFovEnabled)
{
ClearViewportFadeState(viewportState);
viewportState.WasFovEnabled = false;
}
// Restore alpha modified by other viewports.
foreach (var entity in _tree.QueryAabb(args.MapId, args.WorldBounds))
{
var uid = entity.Uid;
if (!_spriteQuery.TryGetComponent(uid, out var sprite))
continue;
if (_originalAlphas.Remove(uid, out var origAlpha))
_sprite.SetColor((uid, sprite), sprite.Color.WithAlpha(origAlpha));
_sprite.SetVisible((uid, sprite), true);
}
}
/// <summary>
/// Updates fade state for all wall-mounted entities in the viewport.
/// </summary>
private void ProcessVisibleEntities(in OverlayDrawArgs args, IEye eye, Matrix3x2 matrix, float fadeStep, ViewportFadeState viewportState)
{
foreach (var entity in _tree.QueryAabb(args.MapId, args.WorldBounds))
{
var (wallmount, xform) = entity;
var uid = entity.Uid;
if (!_spriteQuery.TryGetComponent(uid, out var sprite))
continue;
// Capture original alpha before any viewport modifies it.
if (!_originalAlphas.TryGetValue(uid, out var originalAlpha))
_originalAlphas[uid] = originalAlpha = sprite.Color.A;
viewportState.SeenThisFrame.Add(uid);
var targetAlpha = ComputeTargetAlpha(uid, wallmount, xform, eye, matrix);
UpdateFadeState(uid, originalAlpha, targetAlpha, fadeStep, viewportState);
}
}
/// <summary>
/// Returns 1 if the entity is within its facing arc relative to the eye, 0 otherwise.
/// </summary>
private float ComputeTargetAlpha(EntityUid uid, WallMountComponent wallmount, TransformComponent xform, IEye eye, Matrix3x2 matrix)
{
if (!wallmount.DirectionalVisibility || wallmount.Arc >= Math.Tau)
return 1f;
if (xform.GridUid is not { } gridUid || !_gridQuery.TryGetComponent(gridUid, out var grid))
return 1f;
var tile = _map.TileIndicesFor(gridUid, grid, xform.Coordinates);
if (!_visibility.IsTileBlocked(gridUid, tile, uid))
return 1f;
var (pos, rot) = _xform.GetWorldPositionRotation(xform);
var facingAngle = rot + eye.Rotation + wallmount.Direction;
var entityScreenPos = Vector2.Transform(pos, matrix);
var eyeScreenPos = Vector2.Transform(eye.Position.Position, matrix);
var toEntity = entityScreenPos - eyeScreenPos;
var eyeToEntityAngle = (toEntity with { X = -toEntity.X }).ToWorldAngle();
var angleDiff = Angle.ShortestDistance(eyeToEntityAngle, facingAngle);
return Math.Abs(angleDiff) < wallmount.Arc / 2 ? 1f : 0f;
}
/// <summary>
/// Applies the current fade state to all entities seen this frame.
/// </summary>
private void ApplyFadeToVisibleEntities(ViewportFadeState viewportState)
{
foreach (var uid in viewportState.SeenThisFrame)
{
if (!viewportState.FadeStates.TryGetValue(uid, out var state))
continue;
if (!_spriteQuery.TryGetComponent(uid, out var sprite))
{
RemoveTrackedEntity(uid, viewportState);
continue;
}
_sprite.SetColor((uid, sprite), sprite.Color.WithAlpha(state.EffectiveAlpha));
_sprite.SetVisible((uid, sprite), state.IsVisible);
}
}
/// <summary>
/// Updates fade target for a tracked entity. Newly seen entities snap immediately without fading.
/// </summary>
private static void UpdateFadeState(EntityUid uid, float originalAlpha, float targetAlpha, float fadeStep, ViewportFadeState viewportState)
{
if (viewportState.FadeStates.TryGetValue(uid, out var state))
{
state.TargetAlpha = targetAlpha;
state.StepTowards(fadeStep);
viewportState.FadeStates[uid] = state;
return;
}
viewportState.FadeStates[uid] = WallMountFadeState.Snapped(originalAlpha, targetAlpha);
}
private void RestoreSprite(EntityUid uid, WallMountFadeState state)
{
if (!_spriteQuery.TryGetComponent(uid, out var sprite))
return;
_sprite.SetColor((uid, sprite), sprite.Color.WithAlpha(state.OriginalAlpha));
_sprite.SetVisible((uid, sprite), true);
}
/// <summary>
/// Removes an entity from fade tracking and restores its sprite.
/// </summary>
private void RemoveTrackedEntity(EntityUid uid, ViewportFadeState viewportState)
{
if (viewportState.FadeStates.Remove(uid, out var state))
RestoreSprite(uid, state);
_originalAlphas.Remove(uid);
}
/// <summary>
/// Clears all fade tracking for a viewport and restores sprites.
/// </summary>
private void ClearViewportFadeState(ViewportFadeState viewportState)
{
foreach (var (uid, state) in viewportState.FadeStates)
{
RestoreSprite(uid, state);
_originalAlphas.Remove(uid);
}
viewportState.FadeStates.Clear();
viewportState.SeenThisFrame.Clear();
}
protected override void DisposeBehavior()
{
_fadeCache.Dispose();
base.DisposeBehavior();
}
}

View file

@ -435,10 +435,4 @@ public sealed partial class CCVars
/// </remarks>
public static readonly CVarDef<string> NewCharacterJobs =
CVarDef.Create("game.new_character_jobs", "Passenger", CVar.REPLICATED);
/// <summary>
/// Determines whether wall-mounted entities are hidden when viewed from outside their facing arc.
/// </summary>
public static readonly CVarDef<bool> WallMountDirectionalVisibility =
CVarDef.Create("game.wallmount_directional_visibility", true, CVar.SERVER | CVar.REPLICATED);
}

View file

@ -1,45 +1,28 @@
using Robust.Shared.ComponentTrees;
using Robust.Shared.GameStates;
using Robust.Shared.Physics;
namespace Content.Shared.Wall;
/// <summary>
/// Marks an entity as wall-mounted.
/// Allows interaction through other anchored entities on the same tile within the <see cref="Arc"/>.
/// Hides the sprite when viewed from outside that arc.
/// This component enables an entity to ignore some obstructions for interaction checks.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true)]
public sealed partial class WallMountComponent : Component, IComponentTreeEntry<WallMountComponent>
/// <remarks>
/// This will only exempt anchored entities that intersect the wall-mount. Additionally, this exemption will apply
/// in a limited arc, providing basic functionality for directional wall mounts.
/// </remarks>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class WallMountComponent : Component
{
/// <summary>
/// Range of angles where interaction through other anchored entities on the same tile is allowed.
/// Bigger is more permissive.
/// Range of angles for which the exemption applies. Bigger is more permissive.
/// </summary>
[DataField, AutoNetworkedField]
[ViewVariables(VVAccess.ReadWrite)]
[DataField("arc"), AutoNetworkedField]
public Angle Arc = new(MathF.PI);
/// <summary>
/// The direction the allowed angle range faces, relative to the entity's rotation.
/// Defaults to south.
/// The direction in which the exemption arc is facing, relative to the entity's rotation. Defaults to south.
/// </summary>
[DataField, AutoNetworkedField]
[ViewVariables(VVAccess.ReadWrite)]
[DataField("direction"), AutoNetworkedField]
public Angle Direction = Angle.Zero;
/// <summary>
/// If true, the sprite is only visible from within the facing <see cref="Arc"/>.
/// </summary>
[DataField, AutoNetworkedField]
public bool DirectionalVisibility = true;
[ViewVariables]
public EntityUid? TreeUid { get; set; }
[ViewVariables]
public DynamicTree<ComponentTreeEntry<WallMountComponent>>? Tree { get; set; }
[ViewVariables]
public bool AddToTree => Arc < Math.Tau && DirectionalVisibility;
public bool TreeUpdateQueued { get; set; }
}