| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575 |
- using System.Diagnostics;
- using System.Globalization;
- using System.Linq;
- using System.Text;
- using Content.Shared.Eye.Blinding.Components;
- using Content.Shared.Ghost;
- using Content.Shared.Interaction;
- using Content.Shared.Mobs.Components;
- using Content.Shared.Mobs.Systems;
- using JetBrains.Annotations;
- using Robust.Shared.Containers;
- using Robust.Shared.Map;
- using Robust.Shared.Physics;
- using Robust.Shared.Utility;
- using static Content.Shared.Interaction.SharedInteractionSystem;
- namespace Content.Shared.Examine
- {
- public abstract partial class ExamineSystemShared : EntitySystem
- {
- [Dependency] private readonly OccluderSystem _occluder = default!;
- [Dependency] private readonly SharedTransformSystem _transform = default!;
- [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
- [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
- [Dependency] protected readonly MobStateSystem MobStateSystem = default!;
- public const float MaxRaycastRange = 100;
- /// <summary>
- /// Examine range to use when the examiner is in critical condition.
- /// </summary>
- /// <remarks>
- /// Detailed examinations are disabled while incapactiated. Ideally this should just be set equal to the
- /// radius of the crit overlay that blackens most of the screen. The actual radius of that is defined
- /// in a shader sooo... eh.
- /// </remarks>
- public const float CritExamineRange = 1.3f;
- /// <summary>
- /// Examine range to use when the examiner is dead. See <see cref="CritExamineRange"/>.
- /// </summary>
- public const float DeadExamineRange = 0.75f;
- public const float ExamineRange = 16f;
- protected const float ExamineDetailsRange = 3f;
- protected const float ExamineBlurrinessMult = 2.5f;
- private EntityQuery<GhostComponent> _ghostQuery;
- /// <summary>
- /// Creates a new examine tooltip with arbitrary info.
- /// </summary>
- public abstract void SendExamineTooltip(EntityUid player, EntityUid target, FormattedMessage message, bool getVerbs, bool centerAtCursor);
- public bool IsInDetailsRange(EntityUid examiner, EntityUid entity)
- {
- if (IsClientSide(entity))
- return true;
- // Ghosts can see everything.
- if (_ghostQuery.HasComp(examiner))
- return true;
- // check if the mob is in critical or dead
- if (MobStateSystem.IsIncapacitated(examiner))
- return false;
- if (!InRangeUnOccluded(examiner, entity, ExamineDetailsRange))
- return false;
- // Is the target hidden in a opaque locker or something? Currently this check allows players to examine
- // their organs, if they can somehow target them. Really this should be with userSeeInsideSelf: false, and a
- // separate check for if the item is in their inventory or hands.
- if (_containerSystem.IsInSameOrTransparentContainer(examiner, entity, userSeeInsideSelf: true))
- return true;
- // is it inside of an open storage (e.g., an open backpack)?
- return _interactionSystem.CanAccessViaStorage(examiner, entity);
- }
- [Pure]
- public bool CanExamine(EntityUid examiner, EntityUid examined)
- {
- // special check for client-side entities stored in null-space for some UI guff.
- if (IsClientSide(examined))
- return true;
- return !Deleted(examined) && CanExamine(examiner, _transform.GetMapCoordinates(examined),
- entity => entity == examiner || entity == examined, examined);
- }
- [Pure]
- public virtual bool CanExamine(EntityUid examiner, MapCoordinates target, Ignored? predicate = null, EntityUid? examined = null, ExaminerComponent? examinerComp = null)
- {
- // TODO occluded container checks
- // also requires checking if the examiner has either a storage or stripping UI open, as the item may be accessible via that UI
- if (!Resolve(examiner, ref examinerComp, false))
- return false;
- // Ghosts and admins skip examine checks.
- if (examinerComp.SkipChecks)
- return true;
- if (examined != null)
- {
- var ev = new ExamineAttemptEvent(examiner);
- RaiseLocalEvent(examined.Value, ev);
- if (ev.Cancelled)
- return false;
- }
- if (!examinerComp.CheckInRangeUnOccluded)
- return true;
- if (EntityManager.GetComponent<TransformComponent>(examiner).MapID != target.MapId)
- return false;
- // Do target InRangeUnoccluded which has different checks.
- if (examined != null)
- {
- return InRangeUnOccluded(
- examiner,
- examined.Value,
- GetExaminerRange(examiner),
- predicate: predicate,
- ignoreInsideBlocker: true);
- }
- else
- {
- return InRangeUnOccluded(
- examiner,
- target,
- GetExaminerRange(examiner),
- predicate: predicate,
- ignoreInsideBlocker: true);
- }
- }
- /// <summary>
- /// Check if a given examiner is incapacitated. If yes, return a reduced examine range. Otherwise, return the deault range.
- /// </summary>
- public float GetExaminerRange(EntityUid examiner, MobStateComponent? mobState = null)
- {
- if (Resolve(examiner, ref mobState, logMissing: false))
- {
- if (MobStateSystem.IsDead(examiner, mobState))
- return DeadExamineRange;
- if (MobStateSystem.IsCritical(examiner, mobState) || TryComp<BlindableComponent>(examiner, out var blind) && blind.IsBlind)
- return CritExamineRange;
- if (TryComp<BlurryVisionComponent>(examiner, out var blurry))
- return Math.Clamp(ExamineRange - blurry.Magnitude * ExamineBlurrinessMult, 2, ExamineRange);
- }
- return ExamineRange;
- }
- /// <summary>
- /// True if occluders are drawn for this entity, otherwise false.
- /// </summary>
- public bool IsOccluded(EntityUid uid)
- {
- return TryComp<EyeComponent>(uid, out var eye) && eye.DrawFov;
- }
- public bool InRangeUnOccluded(MapCoordinates origin, MapCoordinates other, float range, Ignored? predicate, bool ignoreInsideBlocker = true, IEntityManager? entMan = null)
- {
- // No, rider. This is better.
- // ReSharper disable once ConvertToLocalFunction
- var wrapped = (EntityUid uid, Ignored? wrapped)
- => wrapped != null && wrapped(uid);
- return InRangeUnOccluded(origin, other, range, predicate, wrapped, ignoreInsideBlocker, entMan);
- }
- public bool InRangeUnOccluded<TState>(MapCoordinates origin, MapCoordinates other, float range,
- TState state, Func<EntityUid, TState, bool> predicate, bool ignoreInsideBlocker = true, IEntityManager? entMan = null)
- {
- if (other.MapId != origin.MapId ||
- other.MapId == MapId.Nullspace) return false;
- var dir = other.Position - origin.Position;
- var length = dir.Length();
- // If range specified also check it
- // TODO: This rounding check is here because the API is kinda eh
- if (range > 0f && length > range + 0.01f) return false;
- if (MathHelper.CloseTo(length, 0)) return true;
- if (length > MaxRaycastRange)
- {
- Log.Warning("InRangeUnOccluded check performed over extreme range. Limiting CollisionRay size.");
- length = MaxRaycastRange;
- }
- var ray = new Ray(origin.Position, dir.Normalized());
- var rayResults = _occluder
- .IntersectRayWithPredicate(origin.MapId, ray, length, state, predicate, false);
- if (rayResults.Count == 0) return true;
- if (!ignoreInsideBlocker) return false;
- foreach (var result in rayResults)
- {
- if (!TryComp(result.HitEntity, out OccluderComponent? o))
- {
- continue;
- }
- var bBox = o.BoundingBox;
- bBox = bBox.Translated(_transform.GetWorldPosition(result.HitEntity));
- if (bBox.Contains(origin.Position) || bBox.Contains(other.Position))
- {
- continue;
- }
- return false;
- }
- return true;
- }
- public bool InRangeUnOccluded(EntityUid origin, EntityUid other, float range = ExamineRange, Ignored? predicate = null, bool ignoreInsideBlocker = true)
- {
- var ev = new InRangeOverrideEvent(origin, other);
- RaiseLocalEvent(origin, ref ev);
- if (ev.Handled)
- {
- return ev.InRange;
- }
- var originPos = _transform.GetMapCoordinates(origin);
- var otherPos = _transform.GetMapCoordinates(other);
- return InRangeUnOccluded(originPos, otherPos, range, predicate, ignoreInsideBlocker);
- }
- public bool InRangeUnOccluded(EntityUid origin, EntityCoordinates other, float range = ExamineRange, Ignored? predicate = null, bool ignoreInsideBlocker = true)
- {
- var originPos = _transform.GetMapCoordinates(origin);
- var otherPos = _transform.ToMapCoordinates(other);
- return InRangeUnOccluded(originPos, otherPos, range, predicate, ignoreInsideBlocker);
- }
- public bool InRangeUnOccluded(EntityUid origin, MapCoordinates other, float range = ExamineRange, Ignored? predicate = null, bool ignoreInsideBlocker = true)
- {
- var originPos = _transform.GetMapCoordinates(origin);
- return InRangeUnOccluded(originPos, other, range, predicate, ignoreInsideBlocker);
- }
- public FormattedMessage GetExamineText(EntityUid entity, EntityUid? examiner)
- {
- var message = new FormattedMessage();
- if (examiner == null)
- {
- return message;
- }
- var hasDescription = false;
- var metadata = MetaData(entity);
- //Add an entity description if one is declared
- if (!string.IsNullOrEmpty(metadata.EntityDescription))
- {
- message.AddText(metadata.EntityDescription);
- hasDescription = true;
- }
- message.PushColor(Color.DarkGray);
- // Raise the event and let things that subscribe to it change the message...
- var isInDetailsRange = IsInDetailsRange(examiner.Value, entity);
- var examinedEvent = new ExaminedEvent(message, entity, examiner.Value, isInDetailsRange, hasDescription);
- RaiseLocalEvent(entity, examinedEvent);
- var newMessage = examinedEvent.GetTotalMessage();
- // pop color tag
- newMessage.Pop();
- return newMessage;
- }
- }
- /// <summary>
- /// Raised when an entity is examined.
- /// If you're pushing multiple messages that should be grouped together (or ordered in some way),
- /// call <see cref="PushGroup"/> before pushing and <see cref="PopGroup"/> when finished.
- /// </summary>
- public sealed class ExaminedEvent : EntityEventArgs
- {
- /// <summary>
- /// The message that will be displayed as the examine text.
- /// You should use <see cref="PushMarkup"/> and similar instead to modify this,
- /// since it handles newlines/priority and such correctly.
- /// </summary>
- /// <seealso cref="PushMessage"/>
- /// <seealso cref="PushMarkup"/>
- /// <seealso cref="PushText"/>
- /// <seealso cref="AddMessage"/>
- /// <seealso cref="AddMarkup"/>
- /// <seealso cref="AddText"/>
- private FormattedMessage Message { get; }
- /// <summary>
- /// Parts of the examine message that will later be sorted by priority and pushed onto <see cref="Message"/>.
- /// </summary>
- private List<ExamineMessagePart> Parts { get; } = new();
- /// <summary>
- /// Whether the examiner is in range of the entity to get some extra details.
- /// </summary>
- public bool IsInDetailsRange { get; }
- /// <summary>
- /// The entity performing the examining.
- /// </summary>
- public EntityUid Examiner { get; }
- /// <summary>
- /// Entity being examined, for broadcast event purposes.
- /// </summary>
- public EntityUid Examined { get; }
- private bool _hasDescription;
- private ExamineMessagePart? _currentGroupPart;
- public ExaminedEvent(FormattedMessage message, EntityUid examined, EntityUid examiner, bool isInDetailsRange, bool hasDescription)
- {
- Message = message;
- Examined = examined;
- Examiner = examiner;
- IsInDetailsRange = isInDetailsRange;
- _hasDescription = hasDescription;
- }
- /// <summary>
- /// Returns <see cref="Message"/> with all <see cref="Parts"/> appended according to their priority.
- /// </summary>
- public FormattedMessage GetTotalMessage()
- {
- int Comparison(ExamineMessagePart a, ExamineMessagePart b)
- {
- // Try sort by priority, then group, then by string contents
- if (a.Priority != b.Priority)
- {
- // negative so that expected behavior is consistent with what makes sense
- // i.e. a negative priority should mean its at the bottom of the list, right?
- return -a.Priority.CompareTo(b.Priority);
- }
- if (a.Group != b.Group)
- {
- return string.Compare(a.Group, b.Group, StringComparison.Ordinal);
- }
- return string.Compare(a.Message.ToString(), b.Message.ToString(), StringComparison.Ordinal);
- }
- // tolist/clone formatted message so calling this multiple times wont fuck shit up
- // (if that happens for some reason)
- var parts = Parts.ToList();
- var totalMessage = new FormattedMessage(Message);
- parts.Sort(Comparison);
- if (_hasDescription && parts.Count > 0)
- {
- totalMessage.PushNewline();
- }
- foreach (var part in parts)
- {
- totalMessage.AddMessage(part.Message);
- if (part.DoNewLine && parts.Last() != part)
- totalMessage.PushNewline();
- }
- totalMessage.TrimEnd();
- return totalMessage;
- }
- /// <summary>
- /// Message group handling. Call this if you want the next set of examine messages that you're adding to have
- /// a consistent order with regards to each other. This is done so that client & server will always
- /// sort messages the same as well as grouped together properly, even if subscriptions are different.
- /// You should wrap it in a using() block so popping automatically occurs.
- /// <summary>
- /// Begins a new message group for examine text, allowing related messages to be grouped and ordered together.
- /// </summary>
- /// <param name="groupName">The name of the group for identification and ordering.</param>
- /// <param name="priority">The priority of the group, affecting its order in the final message.</param>
- /// <returns>A disposable object that ends the group when disposed.</returns>
- public ExamineGroupDisposable PushGroup(string groupName, int priority = 0)
- {
- // Ensure that other examine events correctly ended their groups.
- DebugTools.Assert(_currentGroupPart == null);
- _currentGroupPart = new ExamineMessagePart(new FormattedMessage(), priority, false, groupName);
- return new ExamineGroupDisposable(this);
- }
- /// <summary>
- /// Ends the current group and pushes its groups contents to the message.
- /// This will be called automatically if in using a `using` block with <see cref="PushGroup"/>.
- /// </summary>
- private void PopGroup()
- {
- DebugTools.Assert(_currentGroupPart != null);
- if (_currentGroupPart != null && !_currentGroupPart.Message.IsEmpty)
- {
- Parts.Add(_currentGroupPart);
- }
- _currentGroupPart = null;
- }
- /// <summary>
- /// Push another message into this examine result, on its own line.
- /// End message will be grouped by <see cref="priority"/>, then by group if one was started
- /// then by ordinal comparison.
- /// </summary>
- /// <seealso cref="PushMarkup"/>
- /// <summary>
- /// Adds a formatted message as a new line to the examine message, either within the current group or as a separate part.
- /// </summary>
- /// <param name="message">The formatted message to add.</param>
- /// <param name="priority">The priority for ordering this message part.</param>
- public void PushMessage(FormattedMessage message, int priority = 0)
- {
- if (message.Nodes.Count == 0)
- return;
- if (_currentGroupPart != null)
- {
- message.PushNewline();
- _currentGroupPart.Message.AddMessage(message);
- }
- else
- {
- Parts.Add(new ExamineMessagePart(message, priority, true, null));
- }
- }
- /// <summary>
- /// Push another message parsed from markup into this examine result, on its own line.
- /// End message will be grouped by <see cref="priority"/>, then by group if one was started
- /// then by ordinal comparison.
- /// </summary>
- /// <seealso cref="PushText"/>
- /// <summary>
- /// Parses markup text and adds it as a new message part on its own line, with optional priority for ordering.
- /// </summary>
- /// <param name="markup">The markup-formatted string to add.</param>
- /// <param name="priority">Optional priority for message ordering; higher values appear later.</param>
- public void PushMarkup(string markup, int priority = 0)
- {
- PushMessage(FormattedMessage.FromMarkupOrThrow(markup), priority);
- }
- /// <summary>
- /// Push another message containing raw text into this examine result, on its own line.
- /// End message will be grouped by <see cref="priority"/>, then by group if one was started
- /// then by ordinal comparison.
- /// </summary>
- /// <seealso cref="PushMarkup"/>
- /// <summary>
- /// Adds a line of plain text to the examine message, optionally specifying its priority.
- /// </summary>
- /// <param name="text">The text to add as a separate message line.</param>
- /// <param name="priority">The priority for ordering this message part. Higher values appear later.</param>
- public void PushText(string text, int priority = 0)
- {
- var msg = new FormattedMessage();
- msg.AddText(text);
- PushMessage(msg, priority);
- }
- /// <summary>
- /// Adds a message directly without starting a newline after.
- /// End message will be grouped by <see cref="priority"/>, then by group if one was started
- /// then by ordinal comparison.
- /// </summary>
- /// <seealso cref="AddMarkup"/>
- /// <seealso cref="AddText"/>
- public void AddMessage(FormattedMessage message, int priority = 0)
- {
- if (message.Nodes.Count == 0)
- return;
- if (_currentGroupPart != null)
- {
- _currentGroupPart.Message.AddMessage(message);
- }
- else
- {
- Parts.Add(new ExamineMessagePart(message, priority, false, null));
- }
- }
- /// <summary>
- /// Adds markup directly without starting a newline after.
- /// End message will be grouped by <see cref="priority"/>, then by group if one was started
- /// then by ordinal comparison.
- /// </summary>
- /// <seealso cref="AddText"/>
- /// <summary>
- /// Adds markup-formatted text inline to the examine message without a newline.
- /// </summary>
- /// <param name="markup">The markup-formatted string to add.</param>
- /// <param name="priority">The priority for ordering this message part.</param>
- public void AddMarkup(string markup, int priority = 0)
- {
- AddMessage(FormattedMessage.FromMarkupOrThrow(markup), priority);
- }
- /// <summary>
- /// Adds text directly without starting a newline after.
- /// End message will be grouped by <see cref="priority"/>, then by group if one was started
- /// then by ordinal comparison.
- /// </summary>
- /// <seealso cref="AddMarkup"/>
- /// <summary>
- /// Adds plain text inline to the examine message at the specified priority.
- /// </summary>
- /// <param name="text">The text to add to the message.</param>
- /// <param name="priority">The priority for ordering this message part. Higher values appear later.</param>
- public void AddText(string text, int priority = 0)
- {
- var msg = new FormattedMessage();
- msg.AddText(text);
- AddMessage(msg, priority);
- }
- public struct ExamineGroupDisposable : IDisposable
- {
- private ExaminedEvent _event;
- public ExamineGroupDisposable(ExaminedEvent @event)
- {
- _event = @event;
- }
- public void Dispose()
- {
- _event.PopGroup();
- }
- }
- private record ExamineMessagePart(FormattedMessage Message, int Priority, bool DoNewLine, string? Group);
- }
- /// <summary>
- /// Event raised directed at an entity that someone is attempting to examine
- /// </summary>
- public sealed class ExamineAttemptEvent : CancellableEntityEventArgs
- {
- public readonly EntityUid Examiner;
- public ExamineAttemptEvent(EntityUid examiner)
- {
- Examiner = examiner;
- }
- }
- }
|