#nullable enable using System.Linq; using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.Interfaces.GameObjects.Components; using Content.Shared.Utility; using JetBrains.Annotations; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Components; using Robust.Shared.GameObjects.Systems; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Utility; using static Content.Shared.GameObjects.EntitySystems.SharedInteractionSystem; namespace Content.Shared.GameObjects.EntitySystems { public interface IExamine { /// /// Returns a status examine value for components appended to the end of the description of the entity /// /// The message to append to which will be displayed. /// Whether the examiner is within the 'Details' range, allowing you to show information logically only availabe when close to the examined entity. void Examine(FormattedMessage message, bool inDetailsRange); } public abstract class ExamineSystemShared : EntitySystem { public const float ExamineRange = 16f; public const float ExamineRangeSquared = ExamineRange * ExamineRange; protected const float ExamineDetailsRange = 3f; private static bool IsInDetailsRange(IEntity examiner, IEntity entity) { return examiner.InRangeUnobstructed(entity, ExamineDetailsRange, ignoreInsideBlocker: true) && examiner.IsInSameOrNoContainer(entity); } [Pure] protected static bool CanExamine(IEntity examiner, IEntity examined) { if (!examiner.TryGetComponent(out ExaminerComponent? examinerComponent)) { return false; } if (!examinerComponent.DoRangeCheck) { return true; } if (examiner.Transform.MapID != examined.Transform.MapID) { return false; } Ignored predicate = entity => entity == examiner || entity == examined; if (ContainerHelpers.TryGetContainer(examiner, out var container)) { predicate += entity => entity == container.Owner; } return InRangeUnOccluded( examiner.Transform.MapPosition, examined.Transform.MapPosition, ExamineRange, predicate: predicate, ignoreInsideBlocker: true); } public static bool InRangeUnOccluded(MapCoordinates origin, MapCoordinates other, float range, Ignored? predicate, bool ignoreInsideBlocker = true) { var occluderSystem = Get(); if (!origin.InRange(other, range)) return false; var dir = other.Position - origin.Position; if (dir.LengthSquared.Equals(0f)) return true; if (range > 0f && !(dir.LengthSquared <= range * range)) return false; predicate ??= _ => false; var ray = new Ray(origin.Position, dir.Normalized); var rayResults = occluderSystem .IntersectRayWithPredicate(origin.MapId, ray, dir.Length, predicate.Invoke, false).ToList(); if (rayResults.Count == 0) return true; if (!ignoreInsideBlocker) return false; foreach (var result in rayResults) { if (!result.HitEntity.TryGetComponent(out OccluderComponent? o)) { continue; } var bBox = o.BoundingBox.Translated(o.Owner.Transform.WorldPosition); if (bBox.Contains(origin.Position) || bBox.Contains(other.Position)) { continue; } return false; } return true; } public static bool InRangeUnOccluded(IEntity origin, IEntity other, float range, Ignored? predicate, bool ignoreInsideBlocker = true) { var originPos = origin.Transform.MapPosition; var otherPos = other.Transform.MapPosition; return InRangeUnOccluded(originPos, otherPos, range, predicate, ignoreInsideBlocker); } public static bool InRangeUnOccluded(IEntity origin, IComponent other, float range, Ignored? predicate, bool ignoreInsideBlocker = true) { var originPos = origin.Transform.MapPosition; var otherPos = other.Owner.Transform.MapPosition; return InRangeUnOccluded(originPos, otherPos, range, predicate, ignoreInsideBlocker); } public static bool InRangeUnOccluded(IEntity origin, EntityCoordinates other, float range, Ignored? predicate, bool ignoreInsideBlocker = true) { var originPos = origin.Transform.MapPosition; var otherPos = other.ToMap(origin.EntityManager); return InRangeUnOccluded(originPos, otherPos, range, predicate, ignoreInsideBlocker); } public static bool InRangeUnOccluded(IEntity origin, MapCoordinates other, float range, Ignored? predicate, bool ignoreInsideBlocker = true) { var originPos = origin.Transform.MapPosition; return InRangeUnOccluded(originPos, other, range, predicate, ignoreInsideBlocker); } public static bool InRangeUnOccluded(ITargetedInteractEventArgs args, float range, Ignored? predicate, bool ignoreInsideBlocker = true) { var originPos = args.User.Transform.MapPosition; var otherPos = args.Target.Transform.MapPosition; return InRangeUnOccluded(originPos, otherPos, range, predicate, ignoreInsideBlocker); } public static bool InRangeUnOccluded(DragDropEventArgs args, float range, Ignored? predicate, bool ignoreInsideBlocker = true) { var originPos = args.User.Transform.MapPosition; var otherPos = args.DropLocation.ToMap(args.User.EntityManager); return InRangeUnOccluded(originPos, otherPos, range, predicate, ignoreInsideBlocker); } public static bool InRangeUnOccluded(AfterInteractEventArgs args, float range, Ignored? predicate, bool ignoreInsideBlocker = true) { var originPos = args.User.Transform.MapPosition; var otherPos = args.Target.Transform.MapPosition; return InRangeUnOccluded(originPos, otherPos, range, predicate, ignoreInsideBlocker); } public static FormattedMessage GetExamineText(IEntity entity, IEntity examiner) { var message = new FormattedMessage(); var doNewline = false; //Add an entity description if one is declared if (!string.IsNullOrEmpty(entity.Description)) { message.AddText(entity.Description); doNewline = true; } message.PushColor(Color.DarkGray); //Add component statuses from components that report one foreach (var examineComponent in entity.GetAllComponents()) { var subMessage = new FormattedMessage(); examineComponent.Examine(subMessage, IsInDetailsRange(examiner, entity)); if (subMessage.Tags.Count == 0) continue; if (doNewline) message.AddText("\n"); message.AddMessage(subMessage); doNewline = true; } message.Pop(); return message; } } }