using JetBrains.Annotations; using Robust.Shared.GameObjects; using Robust.Shared.Map.Components; using Robust.Shared.Map.Enumerators; using Robust.Shared.Maths; using Robust.Shared.Utility; using System; using System.Collections.Generic; using Robust.Shared.IoC; using static Robust.Shared.GameObjects.OccluderComponent; namespace Robust.Client.GameObjects; // NOTE: this class handles both snap grid updates of occluders, as well as occluder tree updates (via its parent). // This seems like it's doing somewhat double work because it already has an update queue for occluders but... // See the thing is the snap grid stuff was coded earlier // and technically it only cares about changes in the entity's SNAP GRID position. // Whereas the tree stuff is precise. // Also I just realized this and I cba to refactor this again. [UsedImplicitly] internal sealed partial class ClientOccluderSystem : OccluderSystem { private readonly HashSet _dirtyEntities = new(); [Dependency] private SharedMapSystem _mapSystem = default!; /// public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnAnchorChanged); SubscribeLocalEvent(OnShutdown); } public override void SetEnabled(EntityUid uid, bool enabled, OccluderComponent? comp = null, MetaDataComponent? meta = null) { if (!Resolve(uid, ref comp, false) || enabled == comp.Enabled) return; base.SetEnabled(uid, enabled, comp, meta); var xform = Transform(uid); QueueTreeUpdate(uid, comp, xform); QueueOccludedDirectionUpdate(uid, comp, xform); } private void OnShutdown(EntityUid uid, OccluderComponent comp, ComponentShutdown args) { if (!Terminating(uid)) QueueOccludedDirectionUpdate(uid, comp); } protected override void OnCompStartup(EntityUid uid, OccluderComponent comp, ComponentStartup args) { base.OnCompStartup(uid, comp, args); AnchorStateChanged(uid, comp, Transform(uid)); } public void AnchorStateChanged(EntityUid uid, OccluderComponent comp, TransformComponent xform) { QueueOccludedDirectionUpdate(uid, comp, xform); } public override void FrameUpdate(float frameTime) { base.FrameUpdate(frameTime); if (_dirtyEntities.Count == 0) return; var query = GetEntityQuery(); var xforms = GetEntityQuery(); var grids = GetEntityQuery(); try { foreach (var entity in _dirtyEntities) { if (query.TryGetComponent(entity, out var occluder)) UpdateOccluder(entity, occluder, query, xforms, grids); } } finally { _dirtyEntities.Clear(); } } private void OnAnchorChanged(EntityUid uid, OccluderComponent comp, ref AnchorStateChangedEvent args) { AnchorStateChanged(uid, comp, args.Transform); } private void QueueOccludedDirectionUpdate(EntityUid sender, OccluderComponent occluder, TransformComponent? xform = null) { if (!Resolve(sender, ref xform)) return; occluder.Occluding = OccluderDir.None; var query = GetEntityQuery(); Vector2i pos; EntityUid gridId; MapGridComponent? grid; if (occluder.Enabled && xform.Anchored && TryComp(xform.GridUid, out grid)) { gridId = xform.GridUid.Value; pos = _mapSystem.TileIndicesFor(gridId, grid, xform.Coordinates); _dirtyEntities.Add(sender); } else if (occluder.LastPosition != null) { (gridId, pos) = occluder.LastPosition.Value; occluder.LastPosition = null; if (!TryComp(gridId, out grid)) return; } else { return; } DirtyNeighbours(_mapSystem.GetAnchoredEntitiesEnumerator(gridId, grid, pos + new Vector2i(0, 1)), query); DirtyNeighbours(_mapSystem.GetAnchoredEntitiesEnumerator(gridId, grid, pos + new Vector2i(0, -1)), query); DirtyNeighbours(_mapSystem.GetAnchoredEntitiesEnumerator(gridId, grid, pos + new Vector2i(1, 0)), query); DirtyNeighbours(_mapSystem.GetAnchoredEntitiesEnumerator(gridId, grid, pos + new Vector2i(-1, 0)), query); } private void DirtyNeighbours(AnchoredEntitiesEnumerator enumerator, EntityQuery occluderQuery) { while (enumerator.MoveNext(out var entity)) { if (occluderQuery.TryGetComponent(entity.Value, out var occluder)) { _dirtyEntities.Add(entity.Value); occluder.Occluding = OccluderDir.None; } } } private void UpdateOccluder(EntityUid uid, OccluderComponent occluder, EntityQuery occluders, EntityQuery xforms, EntityQuery grids) { // Content may want to override the default behavior for occlusion. // Apparently OD needs this? { var ev = new OccluderDirectionsEvent(uid, occluder); RaiseLocalEvent(uid, ref ev, true); if (ev.Handled) return; } if (!occluder.Enabled) { DebugTools.Assert(occluder.Occluding == OccluderDir.None); DebugTools.Assert(occluder.LastPosition == null); return; } var xform = xforms.GetComponent(uid); if (!xform.Anchored || !grids.TryGetComponent(xform.GridUid, out var grid)) { DebugTools.Assert(occluder.Occluding == OccluderDir.None); DebugTools.Assert(occluder.LastPosition == null); return; } var tile = _mapSystem.TileIndicesFor(xform.GridUid.Value, grid, xform.Coordinates); // TODO: Sub to parent changes instead or something. // DebugTools.Assert(occluder.LastPosition == null // || occluder.LastPosition.Value.Grid == xform.GridUid && occluder.LastPosition.Value.Tile == tile); occluder.LastPosition = (xform.GridUid.Value, tile); // dir starts at the relative effective south direction; var dir = xform.LocalRotation.GetCardinalDir(); CheckDir(dir, OccluderDir.South, tile, occluder, xform.GridUid.Value, grid, occluders, xforms); dir = dir.GetClockwise90Degrees(); CheckDir(dir, OccluderDir.West, tile, occluder, xform.GridUid.Value, grid, occluders, xforms); dir = dir.GetClockwise90Degrees(); CheckDir(dir, OccluderDir.North, tile, occluder, xform.GridUid.Value, grid, occluders, xforms); dir = dir.GetClockwise90Degrees(); CheckDir(dir, OccluderDir.East, tile, occluder, xform.GridUid.Value, grid, occluders, xforms); } private void CheckDir( Direction dir, OccluderDir occDir, Vector2i tile, OccluderComponent occluder, EntityUid gridUid, MapGridComponent grid, EntityQuery query, EntityQuery xforms) { if ((occluder.Occluding & occDir) != 0) return; foreach (var neighbor in _mapSystem.GetAnchoredEntities(gridUid, grid, tile.Offset(dir))) { if (!query.TryGetComponent(neighbor, out var otherOccluder) || !otherOccluder.Enabled) continue; occluder.Occluding |= occDir; // while we are here, also set the occluder flag for the other entity; var otherXform = xforms.GetComponent(neighbor); DebugTools.Assert(otherXform.Anchored); var rot = -otherXform.LocalRotation; var otherOcDir = FromDirection(rot.RotateDir(dir.GetOpposite())); otherOccluder.Occluding |= otherOcDir; } } public static OccluderDir FromDirection(Direction dir) { return dir switch { Direction.South => OccluderDir.South, Direction.North => OccluderDir.North, Direction.East => OccluderDir.East, Direction.West => OccluderDir.West, _ => throw new ArgumentException($"Invalid dir: {dir}.") }; } /// /// Raised by occluders when trying to get occlusion directions. /// [ByRefEvent] public struct OccluderDirectionsEvent { public bool Handled = false; public readonly EntityUid Sender = default!; public readonly OccluderComponent Occluder = default!; public OccluderDirectionsEvent(EntityUid sender, OccluderComponent occluder) { Sender = sender; Occluder = occluder; } } }