using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using JetBrains.Annotations; using Robust.Shared.Collections; using Robust.Shared.GameStates; using Robust.Shared.IoC; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Reflection; using Robust.Shared.Threading; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Robust.Shared.GameObjects; public abstract class SharedUserInterfaceSystem : EntitySystem { [Dependency] private readonly IDynamicTypeFactory _factory = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly INetManager _netManager = default!; [Dependency] private readonly IParallelManager _parallel = default!; [Dependency] protected readonly IPrototypeManager ProtoManager = default!; [Dependency] private readonly IReflectionManager _reflection = default!; [Dependency] protected readonly ISharedPlayerManager Player = default!; [Dependency] private readonly SharedTransformSystem _transforms = default!; private EntityQuery _ignoreUIRangeQuery; private EntityQuery _xformQuery; protected EntityQuery UIQuery; protected EntityQuery UserQuery; private ActorRangeCheckJob _rangeJob; /// /// Defer BUIs during state handling so client doesn't spam a BUI constantly during prediction. /// private readonly List<(BoundUserInterface Bui, bool value)> _queuedBuis = new(); public override void Initialize() { base.Initialize(); EntityManager.ComponentFactory.RegisterNetworkedFields( nameof(UserInterfaceComponent.Actors), nameof(UserInterfaceComponent.Interfaces), nameof(UserInterfaceComponent.States)); _ignoreUIRangeQuery = GetEntityQuery(); _xformQuery = GetEntityQuery(); UIQuery = GetEntityQuery(); UserQuery = GetEntityQuery(); _rangeJob = new() { System = this, XformQuery = _xformQuery, }; SubscribeAllEvent((msg, args) => { if (args.SenderSession.AttachedEntity is not { } player) return; OnMessageReceived(msg, player); }); SubscribeLocalEvent(OnUserInterfaceOpen); SubscribeLocalEvent(OnUserInterfaceClosed); SubscribeLocalEvent(OnUserInterfaceStartup); SubscribeLocalEvent(OnUserInterfaceShutdown); SubscribeLocalEvent(OnUserInterfaceGetState); SubscribeLocalEvent(OnUserInterfaceHandleState); SubscribeLocalEvent(OnPlayerAttached); SubscribeLocalEvent(OnPlayerDetached); SubscribeLocalEvent(OnActorShutdown); } private void AddQueued(BoundUserInterface bui, bool value) { _queuedBuis.Add((bui, value)); } /// /// Validates the received message, and then pass it onto systems/components /// private void OnMessageReceived(BoundUIWrapMessage msg, EntityUid sender) { // This is more or less the main BUI method that handles all messages. var uid = GetEntity(msg.Entity); if (!UIQuery.TryComp(uid, out var uiComp)) { return; } if (!uiComp.Interfaces.TryGetValue(msg.UiKey, out var ui)) { Log.Debug($"Got BoundInterfaceMessageWrapMessage for unknown UI key: {msg.UiKey}"); return; } // If it's not an open message check we're even a subscriber. if (msg.Message is not OpenBoundInterfaceMessage && (!uiComp.Actors.TryGetValue(msg.UiKey, out var actors) || !actors.Contains(sender))) { Log.Debug($"UI {msg.UiKey} got BoundInterfaceMessageWrapMessage from a client who was not subscribed: {ToPrettyString(sender)}"); return; } // verify that the user is allowed to press buttons on this UI: // If it's a close message something else might try to cancel it but we want to force it. if (msg.Message is not CloseBoundInterfaceMessage && ui.RequireInputValidation) { var attempt = new BoundUserInterfaceMessageAttempt(sender, uid, msg.UiKey, msg.Message); RaiseLocalEvent(attempt); if (attempt.Cancelled) return; } // get the wrapped message and populate it with the sender & UI key information. var message = msg.Message; message.Actor = sender; message.Entity = msg.Entity; message.UiKey = msg.UiKey; if (uiComp.ClientOpenInterfaces.TryGetValue(msg.UiKey, out var cBui)) { cBui.ReceiveMessage(message); } // Raise as object so the correct type is used. RaiseLocalEvent(uid, (object)message, true); } #region User private void OnActorShutdown(Entity ent, ref ComponentShutdown args) { CloseUserUis((ent.Owner, ent.Comp)); } #endregion private void OnPlayerAttached(PlayerAttachedEvent ev) { if (!UserQuery.TryGetComponent(ev.Entity, out var actor)) return; // Open BUIs upon attachment foreach (var (uid, keys) in actor.OpenInterfaces) { if (!UIQuery.TryGetComponent(uid, out var uiComp)) continue; // Player can now receive information about open UIs Dirty(uid, uiComp); foreach (var key in keys) { if (!uiComp.Interfaces.TryGetValue(key, out var data)) continue; EnsureClientBui((uid, uiComp), key, data); } } } private void OnPlayerDetached(PlayerDetachedEvent ev) { if (!UserQuery.TryGetComponent(ev.Entity, out var actor)) return; // Close BUIs open detachment. foreach (var (uid, keys) in actor.OpenInterfaces) { if (!UIQuery.TryGetComponent(uid, out var uiComp)) continue; // Player can no longer receive information about open UIs Dirty(uid, uiComp); foreach (var key in keys) { if (!uiComp.ClientOpenInterfaces.Remove(key, out var cBui)) continue; cBui.Dispose(); } } } private void OnUserInterfaceClosed(Entity ent, ref CloseBoundInterfaceMessage args) { CloseUiInternal(ent!, args.UiKey, args.Actor); } private void CloseUiInternal(Entity ent, Enum key, EntityUid actor) { if (!UIQuery.Resolve(ent.Owner, ref ent.Comp, false)) return; if (!ent.Comp.Actors.TryGetValue(key, out var actors)) return; actors.Remove(actor); if (actors.Count == 0) ent.Comp.Actors.Remove(key); DirtyField(ent, nameof(UserInterfaceComponent.Actors)); // If the actor is also deleting then don't worry about updating what they have open. if (!TerminatingOrDeleted(actor) && UserQuery.TryComp(actor, out var actorComp) && actorComp.OpenInterfaces.TryGetValue(ent.Owner, out var keys)) { keys.Remove(key); if (keys.Count == 0) { actorComp.OpenInterfaces.Remove(ent.Owner); if (actorComp.OpenInterfaces.Count == 0) RemCompDeferred(actor); } } if (ent.Comp.ClientOpenInterfaces.TryGetValue(key, out var cBui)) { AddQueued(cBui, false); } if (ent.Comp.Actors.Count == 0) RemCompDeferred(ent.Owner); var ev = new BoundUIClosedEvent(key, ent.Owner, actor); RaiseLocalEvent(ent.Owner, ev); } private void OnUserInterfaceOpen(Entity ent, ref OpenBoundInterfaceMessage args) { OpenUiInternal(ent!, args.UiKey, args.Actor); } private void OpenUiInternal(Entity ent, Enum key, EntityUid actor) { if (!UIQuery.Resolve(ent.Owner, ref ent.Comp, false)) return; // Similar to the close method this handles actually opening a UI, it just gets relayed here EnsureComp(ent.Owner); var actorComp = EnsureComp(actor); // Let state handling open the UI clientside. actorComp.OpenInterfaces.GetOrNew(ent.Owner).Add(key); ent.Comp.Actors.GetOrNew(key).Add(actor); DirtyField(ent, nameof(UserInterfaceComponent.Actors)); var ev = new BoundUIOpenedEvent(key, ent.Owner, actor); RaiseLocalEvent(ent.Owner, ev); // If we're client we want this handled immediately. EnsureClientBui(ent!, key, ent.Comp.Interfaces[key]); } private void OnUserInterfaceStartup(Entity ent, ref ComponentStartup args) { // PlayerAttachedEvent will catch some of these. foreach (var (key, bui) in ent.Comp.ClientOpenInterfaces) { AddQueued(bui, true); } } protected virtual void OnUserInterfaceShutdown(Entity ent, ref ComponentShutdown args) { var ents = new ValueList(); foreach (var (key, acts) in ent.Comp.Actors) { ents.Clear(); ents.AddRange(acts); foreach (var actor in ents) { CloseUiInternal(ent!, key, actor); DebugTools.Assert(!acts.Contains(actor)); } DebugTools.Assert(!ent.Comp.Actors.ContainsKey(key)); } DebugTools.Assert(ent.Comp.ClientOpenInterfaces.Values.All(x => _queuedBuis.Contains((x, false)))); } private void OnUserInterfaceGetState(Entity ent, ref ComponentGetState args) { if (ent.Comp.LastFieldUpdate >= args.FromTick) { var fields = EntityManager.GetModifiedFields(ent.Comp, args.FromTick); switch (fields) { case 1 << 0: { var state = new UserInterfaceActorsDeltaState(); AddActors(ent, state.Actors, ref args); args.State = state; return; } case 1 << 2: { var states = ent.Comp.States; // TODO Game State // Force the client to serialize & de-serialize implicitly generated component states. if (_netManager.IsClient) states = new(states); args.State = new UserInterfaceStatesDeltaState {States = states}; return; } } } var actors = new Dictionary>(); var dataCopy = new Dictionary(ent.Comp.Interfaces.Count); // TODO Game State // Force the client to serialize & de-serialize implicitly generated component states. foreach (var (weh, a) in ent.Comp.Interfaces) { dataCopy[weh] = new InterfaceData(a); } args.State = new UserInterfaceComponentState(actors, new(ent.Comp.States), dataCopy); // Ensure that only the player that currently has the UI open gets to know what they have it open. AddActors(ent, actors, ref args); } private void AddActors(Entity ent, Dictionary> actors, ref ComponentGetState args) { // Ensure that only the player that currently has the UI open gets to know what they have it open. if (args.ReplayState) { foreach (var (key, acts) in ent.Comp.Actors) { actors[key] = GetNetEntityList(acts); } } else if (args.Player.AttachedEntity is { } player) { var netPlayer = new List { GetNetEntity(player) }; foreach (var (key, acts) in ent.Comp.Actors) { if (acts.Contains(player)) actors[key] = netPlayer; } } } private void OnUserInterfaceHandleState(Entity ent, ref ComponentHandleState args) { Dictionary>? stateActors = null; Dictionary? stateData = null; Dictionary? stateStates = null; if (args.Current is UserInterfaceComponentState state) { stateActors = state.Actors; stateData = state.Data; stateStates = state.States; } else if (args.Current is UserInterfaceActorsDeltaState actorDelta) { stateActors = actorDelta.Actors; } else if (args.Current is UserInterfaceStatesDeltaState stateDelta) { stateStates = stateDelta.States; } else { return; } // Interfaces if (stateData != null) { ent.Comp.Interfaces.Clear(); foreach (var data in stateData) { ent.Comp.Interfaces[data.Key] = new(data.Value); } } var attachedEnt = Player.LocalEntity; // Actors if (stateActors != null) { foreach (var key in ent.Comp.Actors.Keys) { if (!stateActors.ContainsKey(key)) CloseUi(ent!, key); } var toRemoveActors = new ValueList(); var newSet = new HashSet(); foreach (var (key, acts) in stateActors) { var actors = ent.Comp.Actors.GetOrNew(key); newSet.Clear(); foreach (var netEntity in acts) { var uid = EnsureEntity(netEntity, ent.Owner); if (uid.IsValid()) newSet.Add(uid); } foreach (var actor in newSet) { if (!actors.Contains(actor)) OpenUiInternal(ent!, key, actor); } foreach (var actor in actors) { if (!newSet.Contains(actor)) toRemoveActors.Add(actor); } foreach (var actor in toRemoveActors) { CloseUiInternal(ent!, key, actor); } } var clientBuis = new ValueList(ent.Comp.ClientOpenInterfaces.Keys); // Check if the UI is open by us, otherwise dispose of it. foreach (var key in clientBuis) { if (ent.Comp.Actors.TryGetValue(key, out var actors) && (attachedEnt == null || actors.Contains(attachedEnt.Value))) { continue; } var bui = ent.Comp.ClientOpenInterfaces[key]; AddQueued(bui, false); } } // States if (stateStates != null) { foreach (var key in ent.Comp.States.Keys) { if (!stateStates.ContainsKey(key)) ent.Comp.States.Remove(key); } // update any states we have open foreach (var (key, buiState) in stateStates) { if (ent.Comp.States.TryGetValue(key, out var existing) && existing.Equals(buiState)) { continue; } ent.Comp.States[key] = buiState; if (!ent.Comp.ClientOpenInterfaces.TryGetValue(key, out var cBui) || !cBui.IsOpened) continue; cBui.State = buiState; cBui.UpdateState(buiState); cBui.Update(); } } // If UI not open then open it // If we get the first state for an ent coming in then don't open BUIs yet, just defer it until later. var open = ent.Comp.LifeStage > ComponentLifeStage.Added; if (attachedEnt != null && stateActors != null) { foreach (var (key, value) in ent.Comp.Interfaces) { EnsureClientBui(ent, key, value, open); } } } /// /// Opens a client's BUI if not already open and applies the state to it. /// private void EnsureClientBui(Entity entity, Enum key, InterfaceData data, bool open = true) { // If it's out BUI open it up and apply the state, otherwise do nothing. var player = Player.LocalEntity; // Existing BUI just keep it. if (entity.Comp.ClientOpenInterfaces.TryGetValue(key, out var existing)) { _queuedBuis.Remove((existing, false)); return; } if (player == null || !entity.Comp.Actors.TryGetValue(key, out var actors) || !actors.Contains(player.Value)) { return; } DebugTools.Assert(_netManager.IsClient); // Try-catch to try prevent error loops / bricked clients that constantly throw exceptions while applying game // states. E.g., stripping UI used to throw NREs in some instances while fetching the identity of unknown // entities. var type = _reflection.LooseGetType(data.ClientType); var boundUserInterface = (BoundUserInterface) _factory.CreateInstance(type, [entity.Owner, key]); entity.Comp.ClientOpenInterfaces[key] = boundUserInterface; // This is just so we don't open while applying UI states. if (!open) return; AddQueued(boundUserInterface, true); } /// /// Yields all the entities + keys currently open by this entity. /// public IEnumerable<(EntityUid Entity, Enum Key)> GetActorUis(Entity entity) { if (!UserQuery.Resolve(entity.Owner, ref entity.Comp, false)) yield break; foreach (var berry in entity.Comp.OpenInterfaces) { foreach (var key in berry.Value) { yield return (berry.Key, key); } } } /// /// Gets the actors that have the specified key attached to this entity open. /// public IEnumerable GetActors(Entity entity, Enum key) { if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false) || !entity.Comp.Actors.TryGetValue(key, out var actors)) yield break; foreach (var actorUid in actors) { yield return actorUid; } } /// /// Closes the attached UI for all entities. /// public void CloseUi(Entity entity, Enum key) { if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false)) return; if (!entity.Comp.Actors.TryGetValue(key, out var actorSet)) return; var actors = actorSet.ToArray(); foreach (var actor in actors) { CloseUiInternal(entity, key, actor); } } /// /// Closes the attached UI only for the specified actor. /// public void CloseUi(Entity entity, Enum key, ICommonSession? actor, bool predicted = false) { var actorEnt = actor?.AttachedEntity; if (actorEnt == null) return; CloseUi(entity, key, actorEnt.Value, predicted); } /// /// Closes the attached Ui only for the specified actor. /// public void CloseUi(Entity entity, Enum key, EntityUid? actor, bool predicted = false) { if (actor == null) return; if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false)) return; // Short-circuit if no UI. if (!entity.Comp.Interfaces.ContainsKey(key)) return; if (!entity.Comp.Actors.TryGetValue(key, out var actors) || !actors.Contains(actor.Value)) return; // Rely upon the client telling us. if (!predicted) { CloseUiInternal(entity, key, actor.Value); return; } if (!_timing.IsFirstTimePredicted) return; EntityManager.RaisePredictiveEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new CloseBoundInterfaceMessage(), key)); } /// /// Tries to call OpenUi and return false if it isn't open. /// public bool TryOpenUi(Entity entity, Enum key, EntityUid actor, bool predicted = false) { if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false)) return false; OpenUi(entity, key, actor, predicted); // Due to the event actually handling the UI open / closed we can't if (!entity.Comp.Actors.TryGetValue(key, out var actors) || !actors.Contains(actor)) { return false; } return true; } /// /// Opens the UI for the local client. Does nothing on server. /// public virtual void OpenUi(Entity entity, Enum key, bool predicted = false) { } public void OpenUi(Entity entity, Enum key, EntityUid? actor, bool predicted = false) { if (actor == null || !UIQuery.Resolve(entity.Owner, ref entity.Comp, false)) return; // No implementation for that UI key on this ent so short-circuit. if (!entity.Comp.Interfaces.ContainsKey(key)) return; if (entity.Comp.Actors.TryGetValue(key, out var actors) && actors.Contains(actor.Value)) return; if (predicted) { if (_timing.IsFirstTimePredicted) { // Not guaranteed to open so rely upon the event handling it. // Also lets client request it to be opened remotely too. EntityManager.RaisePredictiveEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new OpenBoundInterfaceMessage(), key)); } } else { OnMessageReceived(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new OpenBoundInterfaceMessage(), key), actor.Value); } } public void OpenUi(Entity entity, Enum key, ICommonSession actor, bool predicted = false) { var actorEnt = actor.AttachedEntity; if (actorEnt == null) return; OpenUi(entity, key, actorEnt.Value, predicted); } /// /// Tries to return the saved position of a user interface. /// public virtual bool TryGetPosition(Entity entity, Enum key, out Vector2 position) { position = Vector2.Zero; return false; } /// /// Saves a position for the BUI. /// protected virtual void SavePosition(BoundUserInterface bui) { } /// /// Sets a BUI state and networks it to all clients. /// public void SetUiState(Entity entity, Enum key, BoundUserInterfaceState? state) { if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false)) return; if (!entity.Comp.Interfaces.ContainsKey(key)) return; // Null state if (state == null) { if (!entity.Comp.States.Remove(key)) return; DirtyField(entity, nameof(UserInterfaceComponent.States)); } // Non-null state, check if it matches existing. else { ref var stateRef = ref CollectionsMarshal.GetValueRefOrAddDefault(entity.Comp.States, key, out var exists); if (exists && stateRef?.Equals(state) == true) return; stateRef = state; } // Predict the change on client if (state != null && _netManager.IsClient && entity.Comp.ClientOpenInterfaces.TryGetValue(key, out var bui)) { if (bui.State?.Equals(state) != true) { bui.UpdateState(state); bui.Update(); } } DirtyField(entity, nameof(UserInterfaceComponent.States)); } /// /// Returns true if this entity has the specified Ui key available, even if not currently open. /// public bool HasUi(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null) { if (!Resolve(uid, ref ui, false)) return false; return ui.Interfaces.ContainsKey(uiKey); } /// /// Returns true if the specified UI key is open for this entity by anyone. /// public bool IsUiOpen(Entity entity, Enum uiKey) { if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false)) return false; if (!entity.Comp.Actors.TryGetValue(uiKey, out var actors)) return false; DebugTools.Assert(actors.Count > 0); return actors.Count > 0; } public bool IsUiOpen(Entity entity, Enum uiKey, EntityUid actor) { if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false)) return false; if (!entity.Comp.Actors.TryGetValue(uiKey, out var actors)) return false; return actors.Contains(actor); } /// /// Raises a BUI message locally (on client or server) without networking it. /// [PublicAPI] public void RaiseUiMessage(Entity entity, Enum key, BoundUserInterfaceMessage message) { if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false)) return; if (!entity.Comp.Actors.TryGetValue(key, out var actors)) return; OnMessageReceived(new BoundUIWrapMessage(GetNetEntity(entity.Owner), message, key), message.Actor); } #region Server messages /// /// Sends a BUI message to any actors who have the specified Ui key open. /// public void ServerSendUiMessage(Entity entity, Enum key, BoundUserInterfaceMessage message) { if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false)) return; if (!entity.Comp.Actors.TryGetValue(key, out var actors)) return; var filter = Filter.Entities(actors.ToArray()); RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), message, key), filter); } /// /// Sends a Bui message to the specified actor only. /// public void ServerSendUiMessage(Entity entity, Enum key, BoundUserInterfaceMessage message, EntityUid actor) { if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false)) return; if (!entity.Comp.Actors.TryGetValue(key, out var actors) || !actors.Contains(actor)) return; RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), message, key), actor); } /// /// Sends a Bui message to the specified actor only. /// public void ServerSendUiMessage(Entity entity, Enum key, BoundUserInterfaceMessage message, ICommonSession actor) { if (!_netManager.IsClient) return; if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false) || actor.AttachedEntity is not { } attachedEntity) return; if (!entity.Comp.Actors.TryGetValue(key, out var actors) || !actors.Contains(attachedEntity)) return; RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), message, key), actor); } #endregion /// /// Raises a BUI message from the client to the server. /// public void ClientSendUiMessage(Entity entity, Enum key, BoundUserInterfaceMessage message) { var player = Player.LocalEntity; // Don't send it if we're not a valid actor for it just in case. if (player == null || !UIQuery.Resolve(entity.Owner, ref entity.Comp, false) || !entity.Comp.Actors.TryGetValue(key, out var actors) || !actors.Contains(player.Value)) { return; } RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), message, key)); } /// /// Closes the user's UIs that match the specified key. /// public void CloseUserUis(Entity actor) where T: Enum { if (!UserQuery.Resolve(actor.Owner, ref actor.Comp, false)) return; if (actor.Comp.OpenInterfaces.Count == 0) return; var keys = new ValueList(); foreach (var (uid, enums) in actor.Comp.OpenInterfaces) { keys.Clear(); keys.AddRange(enums); foreach (var weh in keys) { if (weh is not T) continue; CloseUiInternal(uid, weh, actor.Owner); } } } /// /// Closes all Uis for the actor. /// public void CloseUserUis(Entity actor) { if (!UserQuery.Resolve(actor.Owner, ref actor.Comp, false)) return; if (actor.Comp.OpenInterfaces.Count == 0) return; var keys = new ValueList(); foreach (var (uid, enums) in actor.Comp.OpenInterfaces) { keys.Clear(); keys.AddRange(enums); foreach (var key in keys) { CloseUiInternal(uid, key, actor.Owner); } } } /// /// Closes all Uis for the entity. /// public void CloseUis(Entity entity) { if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false)) return; var toClose = new ValueList(); foreach (var (key, actors) in entity.Comp.Actors) { toClose.Clear(); toClose.AddRange(actors); foreach (var actor in toClose) { CloseUiInternal(entity, key, actor); } } } /// /// Closes all Uis for the entity that the specified actor has open. /// public void CloseUis(Entity entity, EntityUid actor) { if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false)) return; foreach (var key in entity.Comp.Interfaces.Keys) { CloseUiInternal(entity, key, actor); } } /// /// Closes all Uis for the entity that the specified actor has open. /// public void CloseUis(Entity entity, ICommonSession actor) { if (actor.AttachedEntity is not { } attachedEnt || !UIQuery.Resolve(entity.Owner, ref entity.Comp, false)) return; CloseUis(entity, attachedEnt); } /// /// Tries to get the BUI if it is currently open. /// public bool TryGetOpenUi(Entity entity, Enum uiKey, [NotNullWhen(true)] out BoundUserInterface? bui) { bui = null; return UIQuery.Resolve(entity.Owner, ref entity.Comp, false) && entity.Comp.ClientOpenInterfaces.TryGetValue(uiKey, out bui); } /// /// Tries to get the BUI if it is currently open. /// public bool TryGetOpenUi(Entity entity, Enum uiKey, [NotNullWhen(true)] out T? bui) where T : BoundUserInterface { if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false) || !entity.Comp.ClientOpenInterfaces.TryGetValue(uiKey, out var cBui)) { bui = null; return false; } bui = (T)cBui; return true; } public bool TryToggleUi(Entity entity, Enum uiKey, ICommonSession actor) { if (actor.AttachedEntity is not { } attachedEntity) return false; return TryToggleUi(entity, uiKey, attachedEntity); } /// /// Switches between closed and open for a specific client. /// public bool TryToggleUi(Entity entity, Enum uiKey, EntityUid actor) { if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false) || !entity.Comp.Interfaces.ContainsKey(uiKey)) { return false; } if (entity.Comp.Actors.TryGetValue(uiKey, out var actors) && actors.Contains(actor)) { CloseUi(entity, uiKey, actor); } else { OpenUi(entity, uiKey, actor); } return true; } /// /// Raised by client-side UIs to send predicted messages to server. /// public void SendPredictedUiMessage(BoundUserInterface bui, BoundUserInterfaceMessage msg) { RaisePredictiveEvent(new BoundUIWrapMessage(GetNetEntity(bui.Owner), msg, bui.UiKey)); } public bool TryGetInterfaceData(Entity entity, Enum key, [NotNullWhen(true)] out InterfaceData? data) { data = null; return Resolve(entity, ref entity.Comp, false) && entity.Comp.Interfaces.TryGetValue(key, out data); } public float GetUiRange(Entity entity, Enum key) { TryGetInterfaceData(entity, key, out var data); return data?.InteractionRange ?? 0; } /// public override void Update(float frameTime) { if (_timing.IsFirstTimePredicted) { foreach (var (bui, open) in _queuedBuis) { if (open) { bui.Open(); #if EXCEPTION_TOLERANCE try { #endif if (UIQuery.TryComp(bui.Owner, out var uiComp)) { if (uiComp.States.TryGetValue(bui.UiKey, out var buiState)) { bui.State = buiState; bui.UpdateState(buiState); bui.Update(); } } #if EXCEPTION_TOLERANCE } catch (Exception e) { Log.Error( $"Caught exception while attempting to create a BUI {bui.UiKey} with type {bui.GetType()} on entity {ToPrettyString(bui.Owner)}. Exception: {e}"); } #endif } // Close BUI else { if (UIQuery.TryComp(bui.Owner, out var uiComp)) { uiComp.ClientOpenInterfaces.Remove(bui.UiKey); } SavePosition(bui); bui.Dispose(); } } _queuedBuis.Clear(); } var query = AllEntityQuery(); // Run these in parallel because it's expensive. _rangeJob.ActorRanges.Clear(); // Handles closing the BUI if actors move out of range of them. // TODO iterate over BUI users, not BUI entities. // I.e., a user may have more than one BUI open, but its rare for a bui to be open by more than one user. // This means we won't have to fetch the user's transform as frequently. while (query.MoveNext(out var uid, out _, out var uiComp)) { foreach (var (key, actors) in uiComp.Actors) { DebugTools.Assert(actors.Count > 0); var data = uiComp.Interfaces[key]; // Short-circuit if (data.InteractionRange <= 0f) continue; foreach (var actor in actors) { if (_netManager.IsClient && !actor.IsValid()) continue; // Client might not have received the entity. Server should log errors. _rangeJob.ActorRanges.Add((uid, key, data, actor, false)); } } } _parallel.ProcessNow(_rangeJob, _rangeJob.ActorRanges.Count); foreach (var data in _rangeJob.ActorRanges) { var uid = data.Ui; var actor = data.Actor; var key = data.Key; if (data.Result || Deleted(uid) || Deleted(actor) || !UIQuery.TryComp(uid, out var uiComp)) continue; CloseUi((uid, uiComp), key, actor); } } /// /// Set a UI after an entity has been created. /// public void SetUi(Entity ent, Enum key, InterfaceData data) { if (!Resolve(ent, ref ent.Comp, false)) ent.Comp = AddComp(ent); ent.Comp.Interfaces[key] = data; DirtyField(ent, nameof(UserInterfaceComponent.Interfaces)); } public bool TryGetUiState(Entity ent, Enum key, [NotNullWhen(true)] out T? state) where T : BoundUserInterfaceState { if (!Resolve(ent, ref ent.Comp, false) || !ent.Comp.States.TryGetValue(key, out var stateComp)) { state = null; return false; } state = (T)stateComp; return true; } /// /// Verify that the subscribed clients are still in range of the interface. /// private bool CheckRange( Entity UiEnt, Enum key, InterfaceData data, Entity actor) { if (actor.Comp.MapID != UiEnt.Comp.MapID) return false; // Handle pluggable BoundUserInterfaceCheckRangeEvent var checkRangeEvent = new BoundUserInterfaceCheckRangeEvent(UiEnt, key, data, actor!); RaiseLocalEvent(UiEnt.Owner, ref checkRangeEvent, true); if (checkRangeEvent.Result == BoundUserInterfaceRangeResult.Pass) return true; // We only check if the range check should be ignored if it did not pass. // The majority of the time the check will be passing and users generally do not have this component. if (_ignoreUIRangeQuery.HasComponent(actor)) return true; if (checkRangeEvent.Result == BoundUserInterfaceRangeResult.Fail) return false; DebugTools.Assert(checkRangeEvent.Result == BoundUserInterfaceRangeResult.Default); return _transforms.InRange(UiEnt!, (actor.Owner, actor.Comp), data.InteractionRange); } /// /// Used for running UI raycast checks in parallel. /// private record struct ActorRangeCheckJob() : IParallelRobustJob { public required EntityQuery XformQuery; public required SharedUserInterfaceSystem System; public readonly List<(EntityUid Ui, Enum Key, InterfaceData Data, EntityUid Actor, bool Result)> ActorRanges = new(); public void Execute(int index) { var data = ActorRanges[index]; if (!XformQuery.TryComp(data.Ui, out var uiXform) || !XformQuery.TryComp(data.Actor, out var actorXform)) { data.Result = false; } else { data.Result = System.CheckRange((data.Ui, uiXform), data.Key, data.Data, (data.Actor, actorXform)); } ActorRanges[index] = data; } } } /// /// Raised by to check whether an interface is still accessible by its user. /// The event is raised directed at the entity that owns the interface. /// [ByRefEvent] [PublicAPI] public struct BoundUserInterfaceCheckRangeEvent( Entity target, Enum uiKey, InterfaceData data, Entity actor) { /// /// The entity owning the UI being checked for. /// public readonly EntityUid Target = target; /// /// The UI itself. /// /// public readonly Enum UiKey = uiKey; public readonly InterfaceData Data = data; /// /// The player for which the UI is being checked. /// public readonly Entity Actor = actor; /// /// The result of the range check. /// public BoundUserInterfaceRangeResult Result; } /// /// Possible results for a . /// public enum BoundUserInterfaceRangeResult : byte { /// /// Run built-in range check. /// Default, /// /// Range check passed, UI is accessible. /// Pass, /// /// Range check failed, UI is inaccessible. /// Fail }