using System; using System.Numerics; using Robust.Shared.ComponentTrees; using Robust.Shared.GameStates; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Physics; using Robust.Shared.Utility; namespace Robust.Shared.GameObjects; public abstract class OccluderSystem : ComponentTreeSystem { public const float MaxRaycastRange = 100f; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnGetState); SubscribeLocalEvent(OnHandleState); } private void OnGetState(EntityUid uid, OccluderComponent comp, ref ComponentGetState args) { args.State = new OccluderComponent.OccluderComponentState(comp.Enabled, comp.BoundingBox); } private void OnHandleState(EntityUid uid, OccluderComponent comp, ref ComponentHandleState args) { if (args.Current is not OccluderComponent.OccluderComponentState state) return; SetEnabled(uid, state.Enabled, comp); SetBoundingBox(uid, state.BoundingBox, comp); } #region Component Tree Overrides protected override bool DoFrameUpdate => true; protected override bool DoTickUpdate => true; // this system relies on the assumption that all occluders are parented directly to a grid or map. // if this ever changes, this will make server move events very expensive. protected override bool Recursive => false; protected override Box2 ExtractAabb(in ComponentTreeEntry entry) { DebugTools.Assert(entry.Transform.ParentUid == entry.Component.TreeUid); return entry.Component.BoundingBox.Translated(entry.Transform.LocalPosition); } protected override Box2 ExtractAabb(in ComponentTreeEntry entry, Vector2 pos, Angle rot) => ExtractAabb(in entry); #endregion #region Setters public void SetBoundingBox(EntityUid uid, Box2 box, OccluderComponent? comp = null) { if (!Resolve(uid, ref comp)) return; comp.BoundingBox = box; Dirty(uid, comp); if (comp.TreeUid != null) QueueTreeUpdate(uid, comp); } public virtual void SetEnabled(EntityUid uid, bool enabled, OccluderComponent? comp = null, MetaDataComponent? meta = null) { if (!Resolve(uid, ref comp, false) || enabled == comp.Enabled) return; comp.Enabled = enabled; Dirty(uid, comp, meta); QueueTreeUpdate(uid, comp); } #endregion #region InRangeUnoccluded /// /// Returns true if two points are within the specified range and there are no occluders between them that aren't /// ignored by the predicate. /// public bool InRangeUnoccluded( MapCoordinates origin, MapCoordinates other, float range, TState state, Func, TState, bool> ignore) { if (!GetRay(origin, other, range, out var length, out var ray, out var result)) return result; return IntersectRay(origin.MapId, ray, length, state, ignore) == null; } /// /// Returns true if two points are within the specified range and there are no occluders between them. /// /// If true, this will use as a predicate to ignore \ /// occluders that are touching the start or end point. public bool InRangeUnoccluded(MapCoordinates origin, MapCoordinates other, float range, bool ignoreTouching) { if (!GetRay(origin, other, range, out var length, out var ray, out var result)) return result; if (!ignoreTouching) return IntersectRay(origin.MapId, ray, length) == null; var state = (XformSystem, origin.Position, other.Position); return IntersectRay(origin.MapId, ray, length, state, IsTouchingEndpoint) == null; } private bool GetRay(MapCoordinates origin, MapCoordinates other, float range, out float length, out Ray ray, out bool result) { ray = default; length = default; result = false; if (other.MapId != origin.MapId || other.MapId == MapId.Nullspace) return false; var dir = other.Position - origin.Position; length = dir.Length(); if (MathHelper.CloseTo(length, 0)) { result = true; return false; } var normalized = dir / length; if (range > 0f && length > range + 0.01f) return false; if (length > MaxRaycastRange) { Log.Warning($"{nameof(InRangeUnoccluded)} check performed over extreme range. Limiting range."); length = MaxRaycastRange; } ray = new Ray(origin.Position, normalized); return true; } /// /// Simple predicate for use with that will ignore any occluders that intersect the /// start and end points. /// public static bool IsTouchingEndpoint(Entity ent, (SharedTransformSystem Sys, Vector2 Start, Vector2 End) state) { var occluderBox = ent.Comp1.BoundingBox; occluderBox = occluderBox.Translated(state.Sys.GetWorldPosition(ent.Comp2)); return occluderBox.Contains(state.Start) || occluderBox.Contains(state.End); } #endregion }