using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using JetBrains.Annotations; using Robust.Server.Player; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.ViewVariables; namespace Robust.Server.GameObjects { [UsedImplicitly] public sealed class UserInterfaceSystem : SharedUserInterfaceSystem { [Dependency] private readonly TransformSystem _xformSys = default!; private readonly List _sessionCache = new(); // List of all bound user interfaces that have at least one player looking at them. [ViewVariables] private readonly List _activeInterfaces = new(); /// public override void Initialize() { base.Initialize(); SubscribeNetworkEvent(OnMessageReceived); SubscribeLocalEvent(OnUserInterfaceShutdown); } private void OnUserInterfaceShutdown(EntityUid uid, ServerUserInterfaceComponent component, ComponentShutdown args) { foreach (var bui in component.Interfaces) { DeactivateInterface(bui); } } internal void SendTo(IPlayerSession session, BoundUIWrapMessage msg) { RaiseNetworkEvent(msg, session.ConnectedClient); } /// /// Validates the received message, and then pass it onto systems/components /// private void OnMessageReceived(BoundUIWrapMessage msg, EntitySessionEventArgs args) { var uid = msg.Entity; if (!TryComp(uid, out ServerUserInterfaceComponent? uiComp) || args.SenderSession is not IPlayerSession session) return; if (!uiComp.TryGetBoundUserInterface(msg.UiKey, out var ui)) { Logger.DebugS("go.comp.ui", "Got BoundInterfaceMessageWrapMessage for unknown UI key: {0}", msg.UiKey); return; } if (!ui.SessionHasOpen(session)) { Logger.DebugS("go.comp.ui", $"UI {msg.UiKey} got BoundInterfaceMessageWrapMessage from a client who was not subscribed: {session}", msg.UiKey); return; } // if they want to close the UI, we can go home early. if (msg.Message is CloseBoundInterfaceMessage) { ui.CloseShared(session); return; } // verify that the user is allowed to press buttons on this UI: if (ui.RequireInputValidation) { var attempt = new BoundUserInterfaceMessageAttempt(args.SenderSession, uid, msg.UiKey); RaiseLocalEvent(attempt); if (attempt.Cancelled) return; } // get the wrapped message and populate it with the sender & UI key information. var message = msg.Message; message.Session = args.SenderSession; message.Entity = uid; message.UiKey = msg.UiKey; // Raise as object so the correct type is used. RaiseLocalEvent(uid, (object)message, true); // Once we have populated our message's wrapped message, we will wrap it up into a message that can be sent // to old component-code. var WrappedUnwrappedMessageMessageMessage = new ServerBoundUserInterfaceMessage(message, session); ui.ReceiveMessage(WrappedUnwrappedMessageMessageMessage); } /// public override void Update(float frameTime) { var query = GetEntityQuery(); foreach (var userInterface in _activeInterfaces.ToList()) { CheckRange(userInterface, query); userInterface.DispatchPendingState(); } } /// /// Verify that the subscribed clients are still in range of the interface. /// private void CheckRange(BoundUserInterface ui, EntityQuery query) { if (ui.InteractionRangeSqrd <= 0) return; // We have to cache the set of sessions because Unsubscribe modifies the original. _sessionCache.Clear(); _sessionCache.AddRange(ui.SubscribedSessions); var transform = query.GetComponent(ui.Owner.Owner); var uiPos = _xformSys.GetWorldPosition(transform, query); var uiMap = transform.MapID; foreach (var session in _sessionCache) { // The component manages the set of sessions, so this invalid session should be removed soon. if (!query.TryGetComponent(session.AttachedEntity, out var xform)) { continue; } if (uiMap != xform.MapID) { ui.Close(session); continue; } var distanceSquared = (uiPos - _xformSys.GetWorldPosition(xform, query)).LengthSquared; if (distanceSquared > ui.InteractionRangeSqrd) { ui.Close(session); } } } internal void DeactivateInterface(BoundUserInterface userInterface) { _activeInterfaces.Remove(userInterface); } internal void ActivateInterface(BoundUserInterface userInterface) { _activeInterfaces.Add(userInterface); } #region Proxy Methods public bool HasUi(EntityUid uid, object uiKey, ServerUserInterfaceComponent? ui = null) { if (!Resolve(uid, ref ui)) return false; return ui.HasBoundUserInterface(uiKey); } public BoundUserInterface GetUi(EntityUid uid, object uiKey, ServerUserInterfaceComponent? ui = null) { if (!Resolve(uid, ref ui)) throw new InvalidOperationException($"Cannot get {typeof(BoundUserInterface)} from an entity without {typeof(ServerUserInterfaceComponent)}!"); return ui.GetBoundUserInterface(uiKey); } public BoundUserInterface? GetUiOrNull(EntityUid uid, object uiKey, ServerUserInterfaceComponent? ui = null) { return TryGetUi(uid, uiKey, out var bui, ui) ? bui : null; } public bool TryGetUi(EntityUid uid, object uiKey, [NotNullWhen(true)] out BoundUserInterface? bui, ServerUserInterfaceComponent? ui = null) { bui = null; return Resolve(uid, ref ui, false) && ui.TryGetBoundUserInterface(uiKey, out bui); } public bool IsUiOpen(EntityUid uid, object uiKey, ServerUserInterfaceComponent? ui = null) { if (!Resolve(uid, ref ui, false)) return false; if (!TryGetUi(uid, uiKey, out var bui, ui)) return false; return bui.SubscribedSessions.Count > 0; } public bool TrySetUiState(EntityUid uid, object uiKey, BoundUserInterfaceState state, IPlayerSession? session = null, ServerUserInterfaceComponent? ui = null) { if (!Resolve(uid, ref ui, false)) return false; if (!TryGetUi(uid, uiKey, out var bui, ui)) return false; bui.SetState(state, session); return true; } public bool TryToggleUi(EntityUid uid, object uiKey, IPlayerSession session, ServerUserInterfaceComponent? ui = null) { if (!Resolve(uid, ref ui)) return false; if (!TryGetUi(uid, uiKey, out var bui)) return false; bui.Toggle(session); return true; } public bool TryOpen(EntityUid uid, object uiKey, IPlayerSession session, ServerUserInterfaceComponent? ui = null) { if (!Resolve(uid, ref ui)) return false; if (!TryGetUi(uid, uiKey, out var bui)) return false; return bui.Open(session); } public bool TryClose(EntityUid uid, object uiKey, IPlayerSession session, ServerUserInterfaceComponent? ui = null) { if (!Resolve(uid, ref ui)) return false; if (!TryGetUi(uid, uiKey, out var bui, ui)) return false; return bui.Close(session); } public bool TryCloseAll(EntityUid uid, object uiKey, ServerUserInterfaceComponent? ui = null) { if (!Resolve(uid, ref ui)) return false; if (!TryGetUi(uid, uiKey, out var bui, ui)) return false; bui.CloseAll(); return true; } public bool SessionHasOpenUi(EntityUid uid, object uiKey, IPlayerSession session, ServerUserInterfaceComponent? ui = null) { if (!Resolve(uid, ref ui)) return false; if (!TryGetUi(uid, uiKey, out var bui)) return false; return bui.SessionHasOpen(session); } public bool TrySendUiMessage(EntityUid uid, object uiKey, BoundUserInterfaceMessage message, ServerUserInterfaceComponent? ui = null) { if (!Resolve(uid, ref ui)) return false; if (!TryGetUi(uid, uiKey, out var bui)) return false; bui.SendMessage(message); return true; } public bool TrySendUiMessage(EntityUid uid, object uiKey, BoundUserInterfaceMessage message, IPlayerSession session, ServerUserInterfaceComponent? ui = null) { if (!Resolve(uid, ref ui)) return false; if (!TryGetUi(uid, uiKey, out var bui)) return false; try { bui.SendMessage(message, session); } catch (ArgumentException) { return false; } return true; } #endregion } }