using Content.Shared.Interaction; using Content.Shared.Pinpointer; using System.Linq; using System.Numerics; using Robust.Shared.Utility; using Content.Server.Shuttles.Events; using Content.Shared.IdentityManagement; namespace Content.Server.Pinpointer; public sealed class PinpointerSystem : SharedPinpointerSystem { [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; private EntityQuery _xformQuery; public override void Initialize() { base.Initialize(); _xformQuery = GetEntityQuery(); SubscribeLocalEvent(OnActivate); SubscribeLocalEvent(OnLocateTarget); } public override bool TogglePinpointer(EntityUid uid, PinpointerComponent? pinpointer = null) { if (!Resolve(uid, ref pinpointer)) return false; var isActive = !pinpointer.IsActive; SetActive(uid, isActive, pinpointer); UpdateAppearance(uid, pinpointer); return isActive; } private void UpdateAppearance(EntityUid uid, PinpointerComponent pinpointer, AppearanceComponent? appearance = null) { if (!Resolve(uid, ref appearance)) return; _appearance.SetData(uid, PinpointerVisuals.IsActive, pinpointer.IsActive, appearance); _appearance.SetData(uid, PinpointerVisuals.TargetDistance, pinpointer.DistanceToTarget, appearance); } private void OnActivate(EntityUid uid, PinpointerComponent component, ActivateInWorldEvent args) { if (args.Handled || !args.Complex) return; TogglePinpointer(uid, component); if (!component.CanRetarget) LocateTarget(uid, component); args.Handled = true; } private void OnLocateTarget(ref FTLCompletedEvent ev) { // This feels kind of expensive, but it only happens once per hyperspace jump // todo: ideally, you would need to raise this event only on jumped entities // this code update ALL pinpointers in game var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var pinpointer)) { if (pinpointer.CanRetarget) continue; LocateTarget(uid, pinpointer); } } private void LocateTarget(EntityUid uid, PinpointerComponent component) { // try to find target from whitelist if (component.IsActive && component.Component != null) { if (!EntityManager.ComponentFactory.TryGetRegistration(component.Component, out var reg)) { Log.Error($"Unable to find component registration for {component.Component} for pinpointer!"); DebugTools.Assert(false); return; } var target = FindTargetFromComponent(uid, reg.Type); SetTarget(uid, target, component); } } public override void Update(float frameTime) { base.Update(frameTime); // because target or pinpointer can move // we need to update pinpointers arrow each frame var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var pinpointer)) { UpdateDirectionToTarget(uid, pinpointer); } } /// /// Try to find the closest entity from whitelist on a current map /// Will return null if can't find anything /// private EntityUid? FindTargetFromComponent(EntityUid uid, Type whitelist, TransformComponent? transform = null) { _xformQuery.Resolve(uid, ref transform, false); if (transform == null) return null; // sort all entities in distance increasing order var mapId = transform.MapID; var l = new SortedList(); var worldPos = _transform.GetWorldPosition(transform); foreach (var (otherUid, _) in EntityManager.GetAllComponents(whitelist)) { if (!_xformQuery.TryGetComponent(otherUid, out var compXform) || compXform.MapID != mapId) continue; var dist = (_transform.GetWorldPosition(compXform) - worldPos).LengthSquared(); l.TryAdd(dist, otherUid); } // return uid with a smallest distance return l.Count > 0 ? l.First().Value : null; } /// /// Update direction from pinpointer to selected target (if it was set) /// protected override void UpdateDirectionToTarget(EntityUid uid, PinpointerComponent? pinpointer = null) { if (!Resolve(uid, ref pinpointer)) return; if (!pinpointer.IsActive) return; var target = pinpointer.Target; if (target == null || !EntityManager.EntityExists(target.Value)) { SetDistance(uid, Distance.Unknown, pinpointer); return; } var dirVec = CalculateDirection(uid, target.Value); var oldDist = pinpointer.DistanceToTarget; if (dirVec != null) { var angle = dirVec.Value.ToWorldAngle(); TrySetArrowAngle(uid, angle, pinpointer); var dist = CalculateDistance(dirVec.Value, pinpointer); SetDistance(uid, dist, pinpointer); } else { SetDistance(uid, Distance.Unknown, pinpointer); } if (oldDist != pinpointer.DistanceToTarget) UpdateAppearance(uid, pinpointer); } /// /// Calculate direction from pinUid to trgUid /// /// Null if failed to calculate distance between two entities private Vector2? CalculateDirection(EntityUid pinUid, EntityUid trgUid) { var xformQuery = GetEntityQuery(); // check if entities have transform component if (!xformQuery.TryGetComponent(pinUid, out var pin)) return null; if (!xformQuery.TryGetComponent(trgUid, out var trg)) return null; // check if they are on same map if (pin.MapID != trg.MapID) return null; // get world direction vector var dir = _transform.GetWorldPosition(trg, xformQuery) - _transform.GetWorldPosition(pin, xformQuery); return dir; } private Distance CalculateDistance(Vector2 vec, PinpointerComponent pinpointer) { var dist = vec.Length(); if (dist <= pinpointer.ReachedDistance) return Distance.Reached; else if (dist <= pinpointer.CloseDistance) return Distance.Close; else if (dist <= pinpointer.MediumDistance) return Distance.Medium; else return Distance.Far; } }