Predicted BUIs (#5059)

* Add TryGetOpenBUI

Avoids having to get the component and openinterfaces separately.

* Couple more helpers

* entityquery

* reviews

* Shared BUIs

* zawehdo

* More boilerplate

* Bunch more work

* Building

* Stuff

* More state handling

* API cleanup

* Slight tweak

* Tweaks

* gabriel

* Disposies

* Active UI support

* Lots of fixes

- Fix states not applying properly, fix predicted messages, remove redundant message type, add RaiseUiMessage for an easy way to do it from shared, add the old BUI state change events back.

* Fix test failures

* weh

* Remove unncessary closes.

* release note
This commit is contained in:
metalgearsloth
2024-04-26 18:12:55 +10:00
committed by GitHub
parent 0fdba836ee
commit d72de032fa
13 changed files with 992 additions and 818 deletions

View File

@@ -35,7 +35,11 @@ END TEMPLATE-->
### Breaking changes
*None yet*
* Refactor UserInterfaceSystem.
- The API has been significantly cleaned up and standardised, most noticeably callers don't need to worry about TryGetUi and can rely on either HasUi, SetUiState, CloseUi, or OpenUi to handle their code as appropriate.
- Interface data is now stored via key rather than as a flat list which is a breaking change for YAML.
- BoundUserInterfaces can now be completely handled via Shared code. Existing Server-side callers will behave similarly to before.
- BoundUserInterfaces now properly close in many more situations, additionally they are now attached to the entity so reconnecting can re-open them and they can be serialized properly.
### New features

View File

@@ -1,84 +1,8 @@
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Reflection;
using System;
using UserInterfaceComponent = Robust.Shared.GameObjects.UserInterfaceComponent;
namespace Robust.Client.GameObjects
namespace Robust.Client.GameObjects;
public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
{
public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
{
[Dependency] private readonly IDynamicTypeFactory _dynamicTypeFactory = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<BoundUIWrapMessage>(MessageReceived);
}
private void MessageReceived(BoundUIWrapMessage ev)
{
var uid = GetEntity(ev.Entity);
if (!TryComp<UserInterfaceComponent>(uid, out var cmp))
return;
var uiKey = ev.UiKey;
var message = ev.Message;
message.Session = _playerManager.LocalSession!;
message.Entity = GetNetEntity(uid);
message.UiKey = uiKey;
// Raise as object so the correct type is used.
RaiseLocalEvent(uid, (object)message, true);
switch (message)
{
case OpenBoundInterfaceMessage _:
TryOpenUi(uid, uiKey, cmp);
break;
case CloseBoundInterfaceMessage _:
TryCloseUi(message.Session, uid, uiKey, remoteCall: true, uiComp: cmp);
break;
default:
if (cmp.OpenInterfaces.TryGetValue(uiKey, out var bui))
bui.InternalReceiveMessage(message);
break;
}
}
private bool TryOpenUi(EntityUid uid, Enum uiKey, UserInterfaceComponent? uiComp = null)
{
if (!Resolve(uid, ref uiComp))
return false;
if (uiComp.OpenInterfaces.ContainsKey(uiKey))
return false;
var data = uiComp.MappedInterfaceData[uiKey];
// TODO: This type should be cached, but I'm too lazy.
var type = _reflectionManager.LooseGetType(data.ClientType);
var boundInterface =
(BoundUserInterface) _dynamicTypeFactory.CreateInstance(type, new object[] {uid, uiKey});
boundInterface.Open();
uiComp.OpenInterfaces[uiKey] = boundInterface;
if (_playerManager.LocalSession is { } playerSession)
{
uiComp.Interfaces[uiKey]._subscribedSessions.Add(playerSession);
RaiseLocalEvent(uid, new BoundUIOpenedEvent(uiKey, uid, playerSession), true);
}
return true;
}
}
}

View File

@@ -1,416 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared.Enums;
using System.Collections;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Robust.Server.GameObjects
namespace Robust.Server.GameObjects;
public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
{
public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
{
[Dependency] private readonly IPlayerManager _playerMan = default!;
[Dependency] private readonly TransformSystem _xformSys = default!;
private EntityQuery<IgnoreUIRangeComponent> _ignoreUIRangeQuery;
private readonly List<ICommonSession> _sessionCache = new();
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<BoundUIWrapMessage>(OnMessageReceived);
_playerMan.PlayerStatusChanged += OnPlayerStatusChanged;
_ignoreUIRangeQuery = GetEntityQuery<IgnoreUIRangeComponent>();
}
public override void Shutdown()
{
base.Shutdown();
_playerMan.PlayerStatusChanged -= OnPlayerStatusChanged;
}
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs args)
{
if (args.NewStatus != SessionStatus.Disconnected)
return;
if (!OpenInterfaces.TryGetValue(args.Session, out var buis))
return;
foreach (var bui in buis.ToArray())
{
CloseShared(bui, args.Session);
}
}
/// <inheritdoc />
public override void Update(float frameTime)
{
var xformQuery = GetEntityQuery<TransformComponent>();
var query = AllEntityQuery<ActiveUserInterfaceComponent, TransformComponent>();
while (query.MoveNext(out var uid, out var activeUis, out var xform))
{
foreach (var ui in activeUis.Interfaces)
{
CheckRange(uid, activeUis, ui, xform, xformQuery);
if (!ui.StateDirty)
continue;
ui.StateDirty = false;
foreach (var (player, state) in ui.PlayerStateOverrides)
{
RaiseNetworkEvent(state, player.Channel);
}
if (ui.LastStateMsg == null)
continue;
foreach (var session in ui.SubscribedSessions)
{
if (!ui.PlayerStateOverrides.ContainsKey(session))
RaiseNetworkEvent(ui.LastStateMsg, session.Channel);
}
}
}
}
/// <summary>
/// Verify that the subscribed clients are still in range of the interface.
/// </summary>
private void CheckRange(EntityUid uid, ActiveUserInterfaceComponent activeUis, PlayerBoundUserInterface ui, TransformComponent transform, EntityQuery<TransformComponent> query)
{
if (ui.InteractionRange <= 0)
return;
// We have to cache the set of sessions because Unsubscribe modifies the original.
_sessionCache.Clear();
_sessionCache.AddRange(ui.SubscribedSessions);
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 (_ignoreUIRangeQuery.HasComponent(session.AttachedEntity))
continue;
// Handle pluggable BoundUserInterfaceCheckRangeEvent
var checkRangeEvent = new BoundUserInterfaceCheckRangeEvent(uid, ui, session);
RaiseLocalEvent(uid, ref checkRangeEvent, broadcast: true);
if (checkRangeEvent.Result == BoundUserInterfaceRangeResult.Pass)
continue;
if (checkRangeEvent.Result == BoundUserInterfaceRangeResult.Fail)
{
CloseUi(ui, session, activeUis);
continue;
}
DebugTools.Assert(checkRangeEvent.Result == BoundUserInterfaceRangeResult.Default);
if (uiMap != xform.MapID)
{
CloseUi(ui, session, activeUis);
continue;
}
var distanceSquared = (uiPos - _xformSys.GetWorldPosition(xform, query)).LengthSquared();
if (distanceSquared > ui.InteractionRangeSqrd)
CloseUi(ui, session, activeUis);
}
}
#region Get BUI
public bool HasUi(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
{
if (!Resolve(uid, ref ui))
return false;
return ui.Interfaces.ContainsKey(uiKey);
}
public PlayerBoundUserInterface GetUi(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
{
if (!Resolve(uid, ref ui))
throw new InvalidOperationException($"Cannot get {typeof(PlayerBoundUserInterface)} from an entity without {typeof(UserInterfaceComponent)}!");
return ui.Interfaces[uiKey];
}
public PlayerBoundUserInterface? GetUiOrNull(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
{
return TryGetUi(uid, uiKey, out var bui, ui)
? bui
: null;
}
/// <summary>
/// Return UIs a session has open.
/// Null if empty.
/// </summary>
public List<PlayerBoundUserInterface>? GetAllUIsForSession(ICommonSession session)
{
OpenInterfaces.TryGetValue(session, out var value);
return value;
}
#endregion
public bool IsUiOpen(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
return bui.SubscribedSessions.Count > 0;
}
public bool SessionHasOpenUi(EntityUid uid, Enum uiKey, ICommonSession session, UserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
return bui.SubscribedSessions.Contains(session);
}
/// <summary>
/// Sets a state. This can be used for stateful UI updating.
/// This state is sent to all clients, and automatically sent to all new clients when they open the UI.
/// Pretty much how NanoUI did it back in ye olde BYOND.
/// </summary>
/// <param name="state">
/// The state object that will be sent to all current and future client.
/// This can be null.
/// </param>
/// <param name="session">
/// The player session to send this new state to.
/// Set to null for sending it to every subscribed player session.
/// </param>
public bool TrySetUiState(EntityUid uid,
Enum uiKey,
BoundUserInterfaceState state,
ICommonSession? session = null,
UserInterfaceComponent? ui = null,
bool clearOverrides = true)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
SetUiState(bui, state, session, clearOverrides);
return true;
}
/// <summary>
/// Sets a state. This can be used for stateful UI updating.
/// This state is sent to all clients, and automatically sent to all new clients when they open the UI.
/// Pretty much how NanoUI did it back in ye olde BYOND.
/// </summary>
/// <param name="state">
/// The state object that will be sent to all current and future client.
/// This can be null.
/// </param>
/// <param name="session">
/// The player session to send this new state to.
/// Set to null for sending it to every subscribed player session.
/// </param>
public void SetUiState(PlayerBoundUserInterface bui, BoundUserInterfaceState state, ICommonSession? session = null, bool clearOverrides = true)
{
var msg = new BoundUIWrapMessage(GetNetEntity(bui.Owner), new UpdateBoundStateMessage(state), bui.UiKey);
if (session == null)
{
bui.LastStateMsg = msg;
if (clearOverrides)
bui.PlayerStateOverrides.Clear();
}
else
{
bui.PlayerStateOverrides[session] = msg;
}
bui.StateDirty = true;
}
#region Close
protected override void CloseShared(PlayerBoundUserInterface bui, ICommonSession session, ActiveUserInterfaceComponent? activeUis = null)
{
var owner = bui.Owner;
bui._subscribedSessions.Remove(session);
bui.PlayerStateOverrides.Remove(session);
if (OpenInterfaces.TryGetValue(session, out var buis))
buis.Remove(bui);
RaiseLocalEvent(owner, new BoundUIClosedEvent(bui.UiKey, owner, session));
if (bui._subscribedSessions.Count == 0)
DeactivateInterface(bui.Owner, bui, activeUis);
}
/// <summary>
/// Closes this all interface for any clients that have any open.
/// </summary>
public bool TryCloseAll(EntityUid uid, Shared.GameObjects.ActiveUserInterfaceComponent? aui = null)
{
if (!Resolve(uid, ref aui, false))
return false;
foreach (var ui in aui.Interfaces)
{
CloseAll(ui);
}
return true;
}
/// <summary>
/// Closes this specific interface for any clients that have it open.
/// </summary>
public bool TryCloseAll(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
CloseAll(bui);
return true;
}
/// <summary>
/// Closes this interface for any clients that have it open.
/// </summary>
public void CloseAll(PlayerBoundUserInterface bui)
{
foreach (var session in bui.SubscribedSessions.ToArray())
{
CloseUi(bui, session);
}
}
#endregion
#region SendMessage
/// <summary>
/// Send a BUI message to all connected player sessions.
/// </summary>
public bool TrySendUiMessage(EntityUid uid, Enum uiKey, BoundUserInterfaceMessage message, UserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
SendUiMessage(bui, message);
return true;
}
/// <summary>
/// Send a BUI message to all connected player sessions.
/// </summary>
public void SendUiMessage(PlayerBoundUserInterface bui, BoundUserInterfaceMessage message)
{
var msg = new BoundUIWrapMessage(GetNetEntity(bui.Owner), message, bui.UiKey);
foreach (var session in bui.SubscribedSessions)
{
RaiseNetworkEvent(msg, session.Channel);
}
}
/// <summary>
/// Send a BUI message to a specific player session.
/// </summary>
public bool TrySendUiMessage(EntityUid uid, Enum uiKey, BoundUserInterfaceMessage message, ICommonSession session, UserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
return TrySendUiMessage(bui, message, session);
}
/// <summary>
/// Send a BUI message to a specific player session.
/// </summary>
public bool TrySendUiMessage(PlayerBoundUserInterface bui, BoundUserInterfaceMessage message, ICommonSession session)
{
if (!bui.SubscribedSessions.Contains(session))
return false;
RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(bui.Owner), message, bui.UiKey), session.Channel);
return true;
}
#endregion
}
/// <summary>
/// Raised by <see cref="UserInterfaceSystem"/> to check whether an interface is still accessible by its user.
/// </summary>
[ByRefEvent]
[PublicAPI]
public struct BoundUserInterfaceCheckRangeEvent
{
/// <summary>
/// The entity owning the UI being checked for.
/// </summary>
public readonly EntityUid Target;
/// <summary>
/// The UI itself.
/// </summary>
/// <returns></returns>
public readonly PlayerBoundUserInterface UserInterface;
/// <summary>
/// The player for which the UI is being checked.
/// </summary>
public readonly ICommonSession Player;
/// <summary>
/// The result of the range check.
/// </summary>
public BoundUserInterfaceRangeResult Result;
public BoundUserInterfaceCheckRangeEvent(
EntityUid target,
PlayerBoundUserInterface userInterface,
ICommonSession player)
{
Target = target;
UserInterface = userInterface;
Player = player;
}
}
/// <summary>
/// Possible results for a <see cref="BoundUserInterfaceCheckRangeEvent"/>.
/// </summary>
public enum BoundUserInterfaceRangeResult : byte
{
/// <summary>
/// Run built-in range check.
/// </summary>
Default,
/// <summary>
/// Range check passed, UI is accessible.
/// </summary>
Pass,
/// <summary>
/// Range check failed, UI is inaccessible.
/// </summary>
Fail
}
}

View File

@@ -41,14 +41,14 @@ namespace Robust.Shared.GameObjects
/// <summary>
/// Invoked when the server uses <c>SetState</c>.
/// </summary>
protected virtual void UpdateState(BoundUserInterfaceState state)
protected internal virtual void UpdateState(BoundUserInterfaceState state)
{
}
/// <summary>
/// Invoked when the server sends an arbitrary message.
/// </summary>
protected virtual void ReceiveMessage(BoundUserInterfaceMessage message)
protected internal virtual void ReceiveMessage(BoundUserInterfaceMessage message)
{
}
@@ -57,7 +57,7 @@ namespace Robust.Shared.GameObjects
/// </summary>
public void Close()
{
UiSystem.TryCloseUi(_playerManager.LocalSession, Owner, UiKey);
UiSystem.CloseUi(Owner, UiKey, _playerManager.LocalEntity, predicted: true);
}
/// <summary>
@@ -65,7 +65,7 @@ namespace Robust.Shared.GameObjects
/// </summary>
public void SendMessage(BoundUserInterfaceMessage message)
{
UiSystem.SendUiMessage(this, message);
UiSystem.ClientSendUiMessage(Owner, UiKey, message);
}
public void SendPredictedMessage(BoundUserInterfaceMessage message)
@@ -73,20 +73,6 @@ namespace Robust.Shared.GameObjects
UiSystem.SendPredictedUiMessage(this, message);
}
internal void InternalReceiveMessage(BoundUserInterfaceMessage message)
{
switch (message)
{
case UpdateBoundStateMessage updateBoundStateMessage:
State = updateBoundStateMessage.State;
UpdateState(State);
break;
default:
ReceiveMessage(message);
break;
}
}
~BoundUserInterface()
{
Dispose(false);

View File

@@ -1,12 +1,12 @@
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
namespace Robust.Server.GameObjects;
namespace Robust.Shared.GameObjects;
/// <summary>
/// Lets any entities with this component ignore user interface range checks that would normally
/// close the UI automatically.
/// </summary>
[RegisterComponent]
[RegisterComponent, NetworkedComponent]
public sealed partial class IgnoreUIRangeComponent : Component
{
}

View File

@@ -1,53 +0,0 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Shared.Player;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.GameObjects;
/// <summary>
/// Represents an entity-bound interface that can be opened by multiple players at once.
/// </summary>
[PublicAPI]
public sealed class PlayerBoundUserInterface
{
[ViewVariables]
public float InteractionRange;
[ViewVariables]
public float InteractionRangeSqrd => InteractionRange * InteractionRange;
[ViewVariables]
public Enum UiKey { get; }
[ViewVariables]
public EntityUid Owner { get; }
internal readonly HashSet<ICommonSession> _subscribedSessions = new();
[ViewVariables]
internal BoundUIWrapMessage? LastStateMsg;
[ViewVariables(VVAccess.ReadWrite)]
public bool RequireInputValidation;
[ViewVariables]
internal bool StateDirty;
[ViewVariables]
internal readonly Dictionary<ICommonSession, BoundUIWrapMessage> PlayerStateOverrides =
new();
/// <summary>
/// All of the sessions currently subscribed to this UserInterface.
/// </summary>
[ViewVariables]
public IReadOnlySet<ICommonSession> SubscribedSessions => _subscribedSessions;
public PlayerBoundUserInterface(PrototypeData data, EntityUid owner)
{
RequireInputValidation = data.RequireInputValidation;
UiKey = data.UiKey;
Owner = owner;
InteractionRange = data.InteractionRange;
}
}

View File

@@ -1,29 +1,26 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Shared.GameStates;
using Robust.Shared.Player;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.GameObjects
namespace Robust.Shared.GameObjects;
[RegisterComponent, NetworkedComponent]
public sealed partial class ActiveUserInterfaceComponent : Component
{
[RegisterComponent]
public sealed partial class ActiveUserInterfaceComponent : Component
{
[ViewVariables]
public HashSet<PlayerBoundUserInterface> Interfaces = new();
}
}
[PublicAPI]
public sealed class ServerBoundUserInterfaceMessage
{
[ViewVariables]
public BoundUserInterfaceMessage Message { get; }
[ViewVariables]
public ICommonSession Session { get; }
[PublicAPI]
public sealed class ServerBoundUserInterfaceMessage
{
[ViewVariables]
public BoundUserInterfaceMessage Message { get; }
[ViewVariables]
public ICommonSession Session { get; }
public ServerBoundUserInterfaceMessage(BoundUserInterfaceMessage message, ICommonSession session)
{
Message = message;
Session = session;
}
public ServerBoundUserInterfaceMessage(BoundUserInterfaceMessage message, ICommonSession session)
{
Message = message;
Session = session;
}
}

View File

@@ -8,36 +8,51 @@ using Robust.Shared.ViewVariables;
namespace Robust.Shared.GameObjects
{
[RegisterComponent, NetworkedComponent]
[RegisterComponent, NetworkedComponent, Access(typeof(SharedUserInterfaceSystem))]
public sealed partial class UserInterfaceComponent : Component
{
// TODO: Obviously clean this shit up, I just moved it into shared.
/// <summary>
/// The currently open interfaces. Used clientside to store the UI.
/// </summary>
[ViewVariables, Access(Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.ReadWriteExecute)]
public readonly Dictionary<Enum, BoundUserInterface> ClientOpenInterfaces = new();
[ViewVariables] public readonly Dictionary<Enum, BoundUserInterface> OpenInterfaces = new();
[ViewVariables] public readonly Dictionary<Enum, PlayerBoundUserInterface> Interfaces = new();
public Dictionary<Enum, PrototypeData> MappedInterfaceData = new();
[DataField]
internal Dictionary<Enum, InterfaceData> Interfaces = new();
/// <summary>
/// Loaded on Init from serialized data.
/// Actors that currently have interfaces open.
/// </summary>
[DataField("interfaces")] internal List<PrototypeData> InterfaceData = new();
[DataField]
public Dictionary<Enum, List<EntityUid>> Actors = new();
/// <summary>
/// Legacy data, new BUIs should be using comp states.
/// </summary>
public Dictionary<Enum, BoundUserInterfaceState> States = new();
[Serializable, NetSerializable]
internal sealed class UserInterfaceComponentState(
Dictionary<Enum, List<NetEntity>> actors,
Dictionary<Enum, BoundUserInterfaceState> states)
: IComponentState
{
public Dictionary<Enum, List<NetEntity>> Actors = actors;
public Dictionary<Enum, BoundUserInterfaceState> States = states;
}
}
[DataDefinition]
public sealed partial class PrototypeData
public sealed partial class InterfaceData
{
[DataField("key", required: true)]
public Enum UiKey { get; private set; } = default!;
[DataField("type", required: true)]
public string ClientType { get; private set; } = default!;
/// <summary>
/// Maximum range before a BUI auto-closes. A non-positive number means there is no limit.
/// </summary>
[DataField("range")]
[DataField]
public float InteractionRange = 2f;
// TODO BUI move to content?
@@ -48,7 +63,7 @@ namespace Robust.Shared.GameObjects
/// <remarks>
/// Avoids requiring each system to individually validate client inputs. However, perhaps some BUIs are supposed to be bypass accessibility checks
/// </remarks>
[DataField("requireInputValidation")]
[DataField]
public bool RequireInputValidation = true;
}
@@ -56,18 +71,12 @@ namespace Robust.Shared.GameObjects
/// Raised whenever the server receives a BUI message from a client relating to a UI that requires input
/// validation.
/// </summary>
public sealed class BoundUserInterfaceMessageAttempt : CancellableEntityEventArgs
public sealed class BoundUserInterfaceMessageAttempt(EntityUid actor, EntityUid target, Enum uiKey)
: CancellableEntityEventArgs
{
public readonly ICommonSession Sender;
public readonly EntityUid Target;
public readonly Enum UiKey;
public BoundUserInterfaceMessageAttempt(ICommonSession sender, EntityUid target, Enum uiKey)
{
Sender = sender;
Target = target;
UiKey = uiKey;
}
public readonly EntityUid Actor = actor;
public readonly EntityUid Target = target;
public readonly Enum UiKey = uiKey;
}
[NetSerializable, Serializable]
@@ -104,7 +113,7 @@ namespace Robust.Shared.GameObjects
/// Only set when the message is raised as a directed event.
/// </summary>
[NonSerialized]
public ICommonSession Session = default!;
public EntityUid Actor = default!;
}
/// <summary>
@@ -120,17 +129,6 @@ namespace Robust.Shared.GameObjects
public NetEntity Entity { get; set; } = NetEntity.Invalid;
}
[NetSerializable, Serializable]
internal sealed class UpdateBoundStateMessage : BoundUserInterfaceMessage
{
public readonly BoundUserInterfaceState State;
public UpdateBoundStateMessage(BoundUserInterfaceState state)
{
State = state;
}
}
[NetSerializable, Serializable]
internal sealed class OpenBoundInterfaceMessage : BoundUserInterfaceMessage
{
@@ -142,59 +140,38 @@ namespace Robust.Shared.GameObjects
}
[Serializable, NetSerializable]
internal abstract class BaseBoundUIWrapMessage : EntityEventArgs
internal abstract class BaseBoundUIWrapMessage(NetEntity entity, BoundUserInterfaceMessage message, Enum uiKey)
: EntityEventArgs
{
public readonly NetEntity Entity;
public readonly BoundUserInterfaceMessage Message;
public readonly Enum UiKey;
public BaseBoundUIWrapMessage(NetEntity entity, BoundUserInterfaceMessage message, Enum uiKey)
{
Message = message;
UiKey = uiKey;
Entity = entity;
}
public readonly NetEntity Entity = entity;
public readonly BoundUserInterfaceMessage Message = message;
public readonly Enum UiKey = uiKey;
}
/// <summary>
/// Helper message raised from client to server.
/// </summary>
[Serializable, NetSerializable]
internal sealed class BoundUIWrapMessage : BaseBoundUIWrapMessage
{
public BoundUIWrapMessage(NetEntity entity, BoundUserInterfaceMessage message, Enum uiKey) : base(entity, message, uiKey)
{
}
}
/// <summary>
/// Helper message raised from client to server.
/// </summary>
[Serializable, NetSerializable]
internal sealed class PredictedBoundUIWrapMessage : BaseBoundUIWrapMessage
{
public PredictedBoundUIWrapMessage(NetEntity entity, BoundUserInterfaceMessage message, Enum uiKey) : base(entity, message, uiKey)
{
}
}
internal sealed class BoundUIWrapMessage(NetEntity entity, BoundUserInterfaceMessage message, Enum uiKey)
: BaseBoundUIWrapMessage(entity, message, uiKey);
public sealed class BoundUIOpenedEvent : BaseLocalBoundUserInterfaceEvent
{
public BoundUIOpenedEvent(Enum uiKey, EntityUid uid, ICommonSession session)
public BoundUIOpenedEvent(Enum uiKey, EntityUid uid, EntityUid actor)
{
UiKey = uiKey;
Entity = uid;
Session = session;
Actor = actor;
}
}
public sealed class BoundUIClosedEvent : BaseLocalBoundUserInterfaceEvent
{
public BoundUIClosedEvent(Enum uiKey, EntityUid uid, ICommonSession session)
public BoundUIClosedEvent(Enum uiKey, EntityUid uid, EntityUid actor)
{
UiKey = uiKey;
Entity = uid;
Session = session;
Actor = actor;
}
}
}

View File

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.Shared.GameObjects;
/// <summary>
/// Stores data about this entity and what BUIs they have open.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class UserInterfaceUserComponent : Component
{
public override bool SessionSpecific => true;
[DataField]
public Dictionary<EntityUid, List<Enum>> OpenInterfaces = new();
}
[Serializable, NetSerializable]
internal sealed class UserInterfaceUserComponentState : IComponentState
{
public Dictionary<NetEntity, List<Enum>> OpenInterfaces = new();
}

File diff suppressed because it is too large Load Diff

View File

@@ -344,6 +344,9 @@ namespace Robust.Shared.Map
var mapCoordinates = ToMap(entityManager, transformSystem);
var otherMapCoordinates = otherCoordinates.ToMap(entityManager, transformSystem);
if (mapCoordinates.MapId != otherMapCoordinates.MapId)
return false;
return mapCoordinates.InRange(otherMapCoordinates, range);
}

View File

@@ -200,12 +200,13 @@ internal abstract partial class SharedPlayerManager
if (EntManager.EnsureComponent<ActorComponent>(uid, out var actor))
{
// component already existed.
DebugTools.AssertNotNull(actor.PlayerSession);
if (!force)
return false;
kicked = actor.PlayerSession;
Detach(kicked);
if (kicked != null)
Detach(kicked);
}
if (_netMan.IsServer)

View File

@@ -12,6 +12,7 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using IgnoreUIRangeComponent = Robust.Shared.GameObjects.IgnoreUIRangeComponent;
namespace Robust.UnitTesting.Server.Maps
{