Try fix client-side BUI error spam (#5208)

This commit is contained in:
Leon Friedrich
2024-06-05 17:21:27 +12:00
committed by GitHub
parent 6b4d74f46e
commit 074a4faa92
6 changed files with 117 additions and 145 deletions

View File

@@ -1,15 +1,10 @@
using Robust.Server.GameStates;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Player;
namespace Robust.Server.GameObjects;
public sealed class ServerMetaDataSystem : MetaDataSystem
{
[Dependency] private readonly PvsSystem _pvsSystem = default!;
private EntityQuery<MetaDataComponent> _mQuery;
public override void Initialize()
{
base.Initialize();
@@ -18,7 +13,6 @@ public sealed class ServerMetaDataSystem : MetaDataSystem
EntityManager.ComponentAdded += OnComponentAdded;
EntityManager.ComponentRemoved += OnComponentRemoved;
_mQuery = GetEntityQuery<MetaDataComponent>();
}
public override void Shutdown()

View File

@@ -5,7 +5,7 @@ using Robust.Shared.ViewVariables;
namespace Robust.Shared.GameObjects;
[RegisterComponent, NetworkedComponent]
[RegisterComponent]
public sealed partial class ActiveUserInterfaceComponent : Component
{
}

View File

@@ -24,7 +24,7 @@ namespace Robust.Shared.GameObjects
/// Actors that currently have interfaces open.
/// </summary>
[DataField]
public Dictionary<Enum, List<EntityUid>> Actors = new();
public Dictionary<Enum, HashSet<EntityUid>> Actors = new();
/// <summary>
/// Legacy data, new BUIs should be using comp states.

View File

@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.Shared.GameObjects;
@@ -9,17 +7,13 @@ namespace Robust.Shared.GameObjects;
/// <summary>
/// Stores data about this entity and what BUIs they have open.
/// </summary>
[RegisterComponent, NetworkedComponent]
/// <remarks>
/// This component is implicitly networked via <see cref="UserInterfaceComponent"/>.
/// I.e., the other component is authoritative about what UIs are open
/// </remarks>
[RegisterComponent]
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();
}

View File

@@ -1,12 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.InteropServices;
using JetBrains.Annotations;
using Robust.Shared.Collections;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Reflection;
@@ -67,9 +67,6 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<UserInterfaceUserComponent, ComponentShutdown>(OnActorShutdown);
SubscribeLocalEvent<UserInterfaceUserComponent, ComponentGetStateAttemptEvent>(OnGetStateAttempt);
SubscribeLocalEvent<UserInterfaceUserComponent, ComponentGetState>(OnActorGetState);
SubscribeLocalEvent<UserInterfaceUserComponent, ComponentHandleState>(OnActorHandleState);
}
/// <summary>
@@ -133,42 +130,6 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
CloseUserUis((ent.Owner, ent.Comp));
}
private void OnGetStateAttempt(Entity<UserInterfaceUserComponent> ent, ref ComponentGetStateAttemptEvent args)
{
if (args.Cancelled || args.Player?.AttachedEntity != ent.Owner)
args.Cancelled = true;
}
private void OnActorGetState(Entity<UserInterfaceUserComponent> ent, ref ComponentGetState args)
{
var interfaces = new Dictionary<NetEntity, List<Enum>>();
foreach (var (buid, data) in ent.Comp.OpenInterfaces)
{
interfaces[GetNetEntity(buid)] = data;
}
args.State = new UserInterfaceUserComponentState()
{
OpenInterfaces = interfaces,
};
}
private void OnActorHandleState(Entity<UserInterfaceUserComponent> ent, ref ComponentHandleState args)
{
if (args.Current is not UserInterfaceUserComponentState state)
return;
// TODO: Allocate less.
ent.Comp.OpenInterfaces.Clear();
foreach (var (nent, data) in state.OpenInterfaces)
{
var openEnt = EnsureEntity<ActorComponent>(nent, ent.Owner);
ent.Comp.OpenInterfaces[openEnt] = data;
}
}
#endregion
private void OnPlayerAttached(PlayerAttachedEvent ev)
@@ -182,6 +143,9 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
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))
@@ -203,6 +167,9 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
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))
@@ -215,11 +182,14 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
private void OnUserInterfaceClosed(Entity<UserInterfaceComponent> ent, ref CloseBoundInterfaceMessage args)
{
CloseUi(ent, args.Actor, args.UiKey);
CloseUiInternal(ent!, args.UiKey, args.Actor);
}
private void CloseUi(Entity<UserInterfaceComponent> ent, EntityUid actor, Enum key)
private void CloseUiInternal(Entity<UserInterfaceComponent?> ent, Enum key, EntityUid actor)
{
if (!_uiQuery.Resolve(ent.Owner, ref ent.Comp, false))
return;
var actors = ent.Comp.Actors[key];
actors.Remove(actor);
@@ -229,16 +199,16 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
Dirty(ent);
// 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))
if (!TerminatingOrDeleted(actor)
&& _userQuery.TryComp(actor, out var actorComp)
&& actorComp.OpenInterfaces.TryGetValue(ent.Owner, out var keys))
{
if (actorComp.OpenInterfaces.TryGetValue(ent.Owner, out var keys))
keys.Remove(key);
if (keys.Count == 0)
{
keys.Remove(key);
if (keys.Count == 0)
actorComp.OpenInterfaces.Remove(ent.Owner);
Dirty(actor, actorComp);
actorComp.OpenInterfaces.Remove(ent.Owner);
if (actorComp.OpenInterfaces.Count == 0)
RemCompDeferred<UserInterfaceUserComponent>(actor);
}
}
@@ -257,23 +227,29 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
private void OnUserInterfaceOpen(Entity<UserInterfaceComponent> ent, ref OpenBoundInterfaceMessage args)
{
OpenUiInternal(ent!, args.UiKey, args.Actor);
}
private void OpenUiInternal(Entity<UserInterfaceComponent?> 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<ActiveUserInterfaceComponent>(ent.Owner);
var actor = args.Actor;
var actorComp = EnsureComp<UserInterfaceUserComponent>(actor);
// Let state handling open the UI clientside.
actorComp.OpenInterfaces.GetOrNew(ent.Owner).Add(args.UiKey);
ent.Comp.Actors.GetOrNew(args.UiKey).Add(actor);
actorComp.OpenInterfaces.GetOrNew(ent.Owner).Add(key);
ent.Comp.Actors.GetOrNew(key).Add(actor);
Dirty(ent);
Dirty(actor, actorComp);
var ev = new BoundUIOpenedEvent(args.UiKey, ent.Owner, args.Actor);
var ev = new BoundUIOpenedEvent(key, ent.Owner, actor);
RaiseLocalEvent(ent.Owner, ev);
// If we're client we want this handled immediately.
EnsureClientBui(ent, args.UiKey, ent.Comp.Interfaces[args.UiKey]);
EnsureClientBui(ent!, key, ent.Comp.Interfaces[key]);
}
private void OnUserInterfaceStartup(Entity<UserInterfaceComponent> ent, ref ComponentStartup args)
@@ -299,7 +275,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
actors.AddRange(acts);
foreach (var actor in actors)
{
CloseUi(ent, actor, key);
CloseUiInternal(ent!, key, actor);
DebugTools.Assert(!acts.Contains(actor));
}
@@ -311,20 +287,29 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
private void OnUserInterfaceGetState(Entity<UserInterfaceComponent> ent, ref ComponentGetState args)
{
// TODO delta states.
// I.e., don't resend the whole BUI state just because a new user opened it.
var actors = new Dictionary<Enum, List<NetEntity>>();
var states = new Dictionary<Enum, BoundUserInterfaceState>();
args.State = new UserInterfaceComponent.UserInterfaceComponentState(actors, ent.Comp.States);
foreach (var (key, acts) in ent.Comp.Actors)
// Ensure that only the player that currently has the UI open gets to know what they have it open.
if (args.ReplayState)
{
actors[key] = GetNetEntityList(acts);
foreach (var (key, acts) in ent.Comp.Actors)
{
actors[key] = GetNetEntityList(acts);
}
}
foreach (var (key, state) in ent.Comp.States)
else if (args.Player.AttachedEntity is { } player)
{
states[key] = state;
var netPlayer = new List<NetEntity> { GetNetEntity(player) };
foreach (var (key, acts) in ent.Comp.Actors)
{
if (acts.Contains(player))
actors[key] = netPlayer;
}
}
args.State = new UserInterfaceComponent.UserInterfaceComponentState(actors, states);
}
private void OnUserInterfaceHandleState(Entity<UserInterfaceComponent> ent, ref ComponentHandleState args)
@@ -332,48 +317,50 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
if (args.Current is not UserInterfaceComponent.UserInterfaceComponentState state)
return;
var toRemove = new ValueList<Enum>();
foreach (var (key, actors) in state.Actors)
{
ref var existing = ref CollectionsMarshal.GetValueRefOrAddDefault(ent.Comp.Actors, key, out _);
existing ??= new List<EntityUid>();
existing.Clear();
existing.AddRange(EnsureEntityList<UserInterfaceComponent>(actors, ent.Owner));
}
foreach (var key in ent.Comp.Actors.Keys)
{
if (state.Actors.ContainsKey(key))
continue;
toRemove.Add(key);
if (!state.Actors.ContainsKey(key))
CloseUi(ent!, key);
}
foreach (var key in toRemove)
var toRemoveActors = new ValueList<EntityUid>();
var newSet = new HashSet<EntityUid>();
foreach (var (key, stateActors) in state.Actors)
{
ent.Comp.Actors.Remove(key);
var actors = ent.Comp.Actors.GetOrNew(key);
newSet.Clear();
foreach (var netEntity in stateActors)
{
var uid = EnsureEntity<UserInterfaceComponent>(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);
}
}
toRemove.Clear();
// State handling
foreach (var key in ent.Comp.States.Keys)
{
if (state.States.ContainsKey(key))
continue;
toRemove.Add(key);
if (!state.States.ContainsKey(key))
ent.Comp.States.Remove(key);
}
foreach (var key in toRemove)
{
ent.Comp.States.Remove(key);
}
toRemove.Clear();
var attachedEnt = _player.LocalEntity;
// Check if the UI is open by us, otherwise dispose of it.
@@ -386,11 +373,6 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
}
bui.Dispose();
toRemove.Add(key);
}
foreach (var key in toRemove)
{
ent.Comp.ClientOpenInterfaces.Remove(key);
}
@@ -519,16 +501,14 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
if (!_uiQuery.Resolve(entity.Owner, ref entity.Comp, false))
return;
if (!entity.Comp.Actors.TryGetValue(key, out var actors))
if (!entity.Comp.Actors.TryGetValue(key, out var actorSet))
return;
for (var i = actors.Count - 1; i >= 0; i--)
var actors = actorSet.ToArray();
foreach (var actor in actors)
{
var actor = actors[i];
CloseUi(entity, key, actor);
CloseUiInternal(entity, key, actor);
}
DebugTools.Assert(actors.Count == 0);
}
/// <summary>
@@ -563,19 +543,16 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
return;
// Rely upon the client telling us.
if (predicted)
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 CloseBoundInterfaceMessage(), key));
}
}
else
{
OnMessageReceived(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new CloseBoundInterfaceMessage(), key), actor.Value);
CloseUiInternal(entity, key, actor.Value);
return;
}
if (!_timing.IsFirstTimePredicted)
return;
EntityManager.RaisePredictiveEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new CloseBoundInterfaceMessage(), key));
}
/// <summary>
@@ -800,17 +777,15 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
if (actor.Comp.OpenInterfaces.Count == 0)
return;
var copied = new Dictionary<EntityUid, List<Enum>>(actor.Comp.OpenInterfaces);
var enumCopy = new ValueList<Enum>();
foreach (var (uid, enums) in copied)
foreach (var (uid, enums) in actor.Comp.OpenInterfaces)
{
enumCopy.Clear();
enumCopy.AddRange(enums);
foreach (var key in enumCopy)
{
CloseUi(uid, key, actor.Owner);
CloseUiInternal(uid, key, actor.Owner);
}
}
}
@@ -823,9 +798,16 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
if (!_uiQuery.Resolve(entity.Owner, ref entity.Comp, false))
return;
entity.Comp.Actors.Clear();
entity.Comp.States.Clear();
Dirty(entity);
var toClose = new ValueList<EntityUid>();
foreach (var (key, actors) in entity.Comp.Actors)
{
toClose.Clear();
toClose.AddRange(actors);
foreach (var actor in toClose)
{
CloseUiInternal(entity, key, actor);
}
}
}
/// <summary>
@@ -838,7 +820,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
foreach (var key in entity.Comp.Interfaces.Keys)
{
CloseUi(entity, key, actor);
CloseUiInternal(entity, key, actor);
}
}

View File

@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Timing;
@@ -36,6 +37,7 @@ namespace Robust.Shared.GameStates
/// If true, this state is intended for replays or some other server spectator entity, not for specific
/// clients.
/// </summary>
[MemberNotNullWhen(false, nameof(Player))]
public bool ReplayState => Player == null;
/// <summary>