Holopad networking rework (#34112)

* Initial commit

* Finalizing main changes

* Addressed reviews

* Fixed a few issues

* Switched to using global overrides

* Removed unnecessary references
This commit is contained in:
chromiumboy
2025-01-17 14:09:59 -06:00
committed by GitHub
parent 464f68dad0
commit efd5d644e8
6 changed files with 97 additions and 257 deletions

View File

@@ -2,45 +2,38 @@ using Content.Shared.Chat.TypingIndicator;
using Content.Shared.Holopad;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using System.Linq;
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
namespace Content.Client.Holopad;
public sealed class HolopadSystem : SharedHolopadSystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HolopadHologramComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<HolopadHologramComponent, ComponentStartup>(OnComponentStartup);
SubscribeLocalEvent<HolopadHologramComponent, BeforePostShaderRenderEvent>(OnShaderRender);
SubscribeAllEvent<TypingChangedEvent>(OnTypingChanged);
SubscribeNetworkEvent<PlayerSpriteStateRequest>(OnPlayerSpriteStateRequest);
SubscribeNetworkEvent<PlayerSpriteStateMessage>(OnPlayerSpriteStateMessage);
}
private void OnComponentInit(EntityUid uid, HolopadHologramComponent component, ComponentInit ev)
private void OnComponentStartup(Entity<HolopadHologramComponent> entity, ref ComponentStartup ev)
{
if (!TryComp<SpriteComponent>(uid, out var sprite))
return;
UpdateHologramSprite(uid);
UpdateHologramSprite(entity, entity.Comp.LinkedEntity);
}
private void OnShaderRender(EntityUid uid, HolopadHologramComponent component, BeforePostShaderRenderEvent ev)
private void OnShaderRender(Entity<HolopadHologramComponent> entity, ref BeforePostShaderRenderEvent ev)
{
if (ev.Sprite.PostShader == null)
return;
ev.Sprite.PostShader.SetParameter("t", (float)_timing.CurTime.TotalSeconds * component.ScrollRate);
UpdateHologramSprite(entity, entity.Comp.LinkedEntity);
}
private void OnTypingChanged(TypingChangedEvent ev, EntitySessionEventArgs args)
@@ -57,100 +50,66 @@ public sealed class HolopadSystem : SharedHolopadSystem
RaiseNetworkEvent(netEv);
}
private void OnPlayerSpriteStateRequest(PlayerSpriteStateRequest ev)
private void UpdateHologramSprite(EntityUid hologram, EntityUid? target)
{
var targetPlayer = GetEntity(ev.TargetPlayer);
var player = _playerManager.LocalSession?.AttachedEntity;
// Ignore the request if received by a player who isn't the target
if (targetPlayer != player)
return;
if (!TryComp<SpriteComponent>(player, out var playerSprite))
return;
var spriteLayerData = new List<PrototypeLayerData>();
if (playerSprite.Visible)
{
// Record the RSI paths, state names and shader paramaters of all visible layers
for (int i = 0; i < playerSprite.AllLayers.Count(); i++)
{
if (!playerSprite.TryGetLayer(i, out var layer))
continue;
if (!layer.Visible ||
string.IsNullOrEmpty(layer.ActualRsi?.Path.ToString()) ||
string.IsNullOrEmpty(layer.State.Name))
continue;
var layerDatum = new PrototypeLayerData();
layerDatum.RsiPath = layer.ActualRsi.Path.ToString();
layerDatum.State = layer.State.Name;
if (layer.CopyToShaderParameters != null)
{
var key = (string)layer.CopyToShaderParameters.LayerKey;
if (playerSprite.LayerMapTryGet(key, out var otherLayerIdx) &&
playerSprite.TryGetLayer(otherLayerIdx, out var otherLayer) &&
otherLayer.Visible)
{
layerDatum.MapKeys = new() { key };
layerDatum.CopyToShaderParameters = new PrototypeCopyToShaderParameters()
{
LayerKey = key,
ParameterTexture = layer.CopyToShaderParameters.ParameterTexture,
ParameterUV = layer.CopyToShaderParameters.ParameterUV
};
}
}
spriteLayerData.Add(layerDatum);
}
}
// Return the recorded data to the server
var evResponse = new PlayerSpriteStateMessage(ev.TargetPlayer, spriteLayerData.ToArray());
RaiseNetworkEvent(evResponse);
}
private void OnPlayerSpriteStateMessage(PlayerSpriteStateMessage ev)
{
UpdateHologramSprite(GetEntity(ev.SpriteEntity), ev.SpriteLayerData);
}
private void UpdateHologramSprite(EntityUid uid, PrototypeLayerData[]? layerData = null)
{
if (!TryComp<SpriteComponent>(uid, out var hologramSprite))
return;
if (!TryComp<HolopadHologramComponent>(uid, out var holopadhologram))
// Get required components
if (!TryComp<SpriteComponent>(hologram, out var hologramSprite) ||
!TryComp<HolopadHologramComponent>(hologram, out var holopadhologram))
return;
// Remove all sprite layers
for (int i = hologramSprite.AllLayers.Count() - 1; i >= 0; i--)
hologramSprite.RemoveLayer(i);
if (layerData == null || layerData.Length == 0)
if (TryComp<SpriteComponent>(target, out var targetSprite))
{
layerData = new PrototypeLayerData[1];
layerData[0] = new PrototypeLayerData()
// Use the target's holographic avatar (if available)
if (TryComp<HolographicAvatarComponent>(target, out var targetAvatar) &&
targetAvatar.LayerData != null)
{
RsiPath = holopadhologram.RsiPath,
State = holopadhologram.RsiState
};
for (int i = 0; i < targetAvatar.LayerData.Length; i++)
{
var layer = targetAvatar.LayerData[i];
hologramSprite.AddLayer(targetAvatar.LayerData[i], i);
}
}
// Otherwise copy the target's current physical appearance
else
{
hologramSprite.CopyFrom(targetSprite);
}
}
for (int i = 0; i < layerData.Length; i++)
// There is no target, display a default sprite instead (if available)
else
{
var layer = layerData[i];
layer.Shader = "unshaded";
if (string.IsNullOrEmpty(holopadhologram.RsiPath) || string.IsNullOrEmpty(holopadhologram.RsiState))
return;
hologramSprite.AddLayer(layerData[i], i);
var layer = new PrototypeLayerData();
layer.RsiPath = holopadhologram.RsiPath;
layer.State = holopadhologram.RsiState;
hologramSprite.AddLayer(layer);
}
UpdateHologramShader(uid, hologramSprite, holopadhologram);
// Override specific values
hologramSprite.Color = Color.White;
hologramSprite.Offset = holopadhologram.Offset;
hologramSprite.DrawDepth = (int)DrawDepth.Mobs;
hologramSprite.NoRotation = true;
hologramSprite.DirectionOverride = Direction.South;
hologramSprite.EnableDirectionOverride = true;
// Remove shading from all layers (except displacement maps)
for (int i = 0; i < hologramSprite.AllLayers.Count(); i++)
{
if (hologramSprite.TryGetLayer(i, out var layer) && layer.ShaderPrototype != "DisplacedStencilDraw")
hologramSprite.LayerSetShader(i, "unshaded");
}
UpdateHologramShader(hologram, hologramSprite, holopadhologram);
}
private void UpdateHologramShader(EntityUid uid, SpriteComponent sprite, HolopadHologramComponent holopadHologram)

View File

@@ -15,6 +15,7 @@ using Content.Shared.Telephone;
using Content.Shared.UserInterface;
using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Server.GameStates;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -35,22 +36,15 @@ public sealed class HolopadSystem : SharedHolopadSystem
[Dependency] private readonly ChatSystem _chatSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly PvsOverrideSystem _pvs = default!;
private float _updateTimer = 1.0f;
private const float UpdateTime = 1.0f;
private const float MinTimeBetweenSyncRequests = 0.5f;
private TimeSpan _minTimeSpanBetweenSyncRequests;
private HashSet<EntityUid> _pendingRequestsForSpriteState = new();
private HashSet<EntityUid> _recentlyUpdatedHolograms = new();
public override void Initialize()
{
base.Initialize();
_minTimeSpanBetweenSyncRequests = TimeSpan.FromSeconds(MinTimeBetweenSyncRequests);
// Holopad UI and bound user interface messages
SubscribeLocalEvent<HolopadComponent, BeforeActivatableUIOpenEvent>(OnUIOpen);
SubscribeLocalEvent<HolopadComponent, HolopadStartNewCallMessage>(OnHolopadStartNewCall);
@@ -68,7 +62,6 @@ public sealed class HolopadSystem : SharedHolopadSystem
// Networked events
SubscribeNetworkEvent<HolopadUserTypingChangedEvent>(OnTypingChanged);
SubscribeNetworkEvent<PlayerSpriteStateMessage>(OnPlayerSpriteStateMessage);
// Component start/shutdown events
SubscribeLocalEvent<HolopadComponent, ComponentInit>(OnHolopadInit);
@@ -268,16 +261,11 @@ public sealed class HolopadSystem : SharedHolopadSystem
if (source.Comp.Hologram == null)
GenerateHologram(source);
// Receiver holopad holograms have to be generated now instead of waiting for their own event
// to fire because holographic avatars get synced immediately
if (TryComp<HolopadComponent>(args.Receiver, out var receivingHolopad) && receivingHolopad.Hologram == null)
GenerateHologram((args.Receiver, receivingHolopad));
if (source.Comp.User != null)
{
// Re-link the user to refresh the sprite data
LinkHolopadToUser(source, source.Comp.User.Value);
}
// Re-link the user to refresh the sprite data
LinkHolopadToUser(source, source.Comp.User);
}
private void OnHoloCallEnded(Entity<HolopadComponent> entity, ref TelephoneCallEndedEvent args)
@@ -323,22 +311,6 @@ public sealed class HolopadSystem : SharedHolopadSystem
}
}
private void OnPlayerSpriteStateMessage(PlayerSpriteStateMessage ev, EntitySessionEventArgs args)
{
var uid = args.SenderSession.AttachedEntity;
if (!Exists(uid))
return;
if (!_pendingRequestsForSpriteState.Remove(uid.Value))
return;
if (!TryComp<HolopadUserComponent>(uid, out var holopadUser))
return;
SyncHolopadUserWithLinkedHolograms((uid.Value, holopadUser), ev.SpriteLayerData);
}
#endregion
#region: Component start/shutdown events
@@ -485,8 +457,6 @@ public sealed class HolopadSystem : SharedHolopadSystem
}
}
}
_recentlyUpdatedHolograms.Clear();
}
public void UpdateUIState(Entity<HolopadComponent> entity, TelephoneComponent? telephone = null)
@@ -555,10 +525,16 @@ public sealed class HolopadSystem : SharedHolopadSystem
QueueDel(hologram);
}
private void LinkHolopadToUser(Entity<HolopadComponent> entity, EntityUid user)
private void LinkHolopadToUser(Entity<HolopadComponent> entity, EntityUid? user)
{
if (user == null)
{
UnlinkHolopadFromUser(entity, null);
return;
}
if (!TryComp<HolopadUserComponent>(user, out var holopadUser))
holopadUser = AddComp<HolopadUserComponent>(user);
holopadUser = AddComp<HolopadUserComponent>(user.Value);
if (user != entity.Comp.User?.Owner)
{
@@ -567,51 +543,44 @@ public sealed class HolopadSystem : SharedHolopadSystem
// Assigns the new user in their place
holopadUser.LinkedHolopads.Add(entity);
entity.Comp.User = (user, holopadUser);
entity.Comp.User = (user.Value, holopadUser);
}
if (TryComp<HolographicAvatarComponent>(user, out var avatar))
{
SyncHolopadUserWithLinkedHolograms((user, holopadUser), avatar.LayerData);
return;
}
// We have no apriori sprite data for the hologram, request
// the current appearance of the user from the client
RequestHolopadUserSpriteUpdate((user, holopadUser));
// Add the new user to PVS and sync their appearance with any
// holopads connected to the one they are using
_pvs.AddGlobalOverride(user.Value);
SyncHolopadHologramAppearanceWithTarget(entity, entity.Comp.User);
}
private void UnlinkHolopadFromUser(Entity<HolopadComponent> entity, Entity<HolopadUserComponent>? user)
{
if (user == null)
return;
entity.Comp.User = null;
SyncHolopadHologramAppearanceWithTarget(entity, null);
foreach (var linkedHolopad in GetLinkedHolopads(entity))
{
if (linkedHolopad.Comp.Hologram != null)
{
_appearanceSystem.SetData(linkedHolopad.Comp.Hologram.Value.Owner, TypingIndicatorVisuals.IsTyping, false);
// Send message with no sprite data to the client
// This will set the holgram sprite to a generic icon
var ev = new PlayerSpriteStateMessage(GetNetEntity(linkedHolopad.Comp.Hologram.Value));
RaiseNetworkEvent(ev);
}
}
if (!HasComp<HolopadUserComponent>(user))
if (user == null)
return;
user.Value.Comp.LinkedHolopads.Remove(entity);
if (!user.Value.Comp.LinkedHolopads.Any())
if (!user.Value.Comp.LinkedHolopads.Any() &&
user.Value.Comp.LifeStage < ComponentLifeStage.Stopping)
{
_pendingRequestsForSpriteState.Remove(user.Value);
_pvs.RemoveGlobalOverride(user.Value);
RemComp<HolopadUserComponent>(user.Value);
}
}
private void SyncHolopadHologramAppearanceWithTarget(Entity<HolopadComponent> entity, Entity<HolopadUserComponent>? user)
{
foreach (var linkedHolopad in GetLinkedHolopads(entity))
{
if (linkedHolopad.Comp.Hologram == null)
continue;
if (user.Value.Comp.LifeStage < ComponentLifeStage.Stopping)
RemComp<HolopadUserComponent>(user.Value);
if (user == null)
_appearanceSystem.SetData(linkedHolopad.Comp.Hologram.Value.Owner, TypingIndicatorVisuals.IsTyping, false);
linkedHolopad.Comp.Hologram.Value.Comp.LinkedEntity = user;
Dirty(linkedHolopad.Comp.Hologram.Value);
}
}
@@ -646,31 +615,6 @@ public sealed class HolopadSystem : SharedHolopadSystem
Dirty(entity);
}
private void RequestHolopadUserSpriteUpdate(Entity<HolopadUserComponent> user)
{
if (!_pendingRequestsForSpriteState.Add(user))
return;
var ev = new PlayerSpriteStateRequest(GetNetEntity(user));
RaiseNetworkEvent(ev);
}
private void SyncHolopadUserWithLinkedHolograms(Entity<HolopadUserComponent> entity, PrototypeLayerData[]? spriteLayerData)
{
foreach (var linkedHolopad in entity.Comp.LinkedHolopads)
{
foreach (var receivingHolopad in GetLinkedHolopads(linkedHolopad))
{
if (receivingHolopad.Comp.Hologram == null || !_recentlyUpdatedHolograms.Add(receivingHolopad.Comp.Hologram.Value))
continue;
var netHologram = GetNetEntity(receivingHolopad.Comp.Hologram.Value);
var ev = new PlayerSpriteStateMessage(netHologram, spriteLayerData);
RaiseNetworkEvent(ev);
}
}
}
private void ActivateProjector(Entity<HolopadComponent> entity, EntityUid user)
{
if (!TryComp<TelephoneComponent>(entity, out var receiverTelephone))

View File

@@ -2,12 +2,12 @@ using Robust.Shared.GameStates;
namespace Content.Shared.Holopad;
[RegisterComponent, NetworkedComponent]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class HolographicAvatarComponent : Component
{
/// <summary>
/// The prototype sprite layer data for the hologram
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public PrototypeLayerData[] LayerData;
}

View File

@@ -1,4 +1,5 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using System.Numerics;
namespace Content.Shared.Holopad;
@@ -6,7 +7,7 @@ namespace Content.Shared.Holopad;
/// <summary>
/// Holds data pertaining to holopad holograms
/// </summary>
[RegisterComponent, NetworkedComponent]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class HolopadHologramComponent : Component
{
/// <summary>
@@ -64,8 +65,8 @@ public sealed partial class HolopadHologramComponent : Component
public Vector2 Offset = new Vector2();
/// <summary>
/// A user that are linked to this hologram
/// An entity that is linked to this hologram
/// </summary>
[ViewVariables]
public Entity<HolopadComponent>? LinkedHolopad;
[ViewVariables, AutoNetworkedField]
public EntityUid? LinkedEntity = null;
}

View File

@@ -20,29 +20,6 @@ public sealed partial class HolopadUserComponent : Component
public HashSet<Entity<HolopadComponent>> LinkedHolopads = new();
}
/// <summary>
/// A networked event raised when the visual state of a hologram is being updated
/// </summary>
[Serializable, NetSerializable]
public sealed class HolopadHologramVisualsUpdateEvent : EntityEventArgs
{
/// <summary>
/// The hologram being updated
/// </summary>
public readonly NetEntity Hologram;
/// <summary>
/// The target the hologram is copying
/// </summary>
public readonly NetEntity? Target;
public HolopadHologramVisualsUpdateEvent(NetEntity hologram, NetEntity? target = null)
{
Hologram = hologram;
Target = target;
}
}
/// <summary>
/// A networked event raised when the visual state of a hologram is being updated
/// </summary>
@@ -65,40 +42,3 @@ public sealed class HolopadUserTypingChangedEvent : EntityEventArgs
IsTyping = isTyping;
}
}
/// <summary>
/// A networked event raised by the server to request the current visual state of a target player entity
/// </summary>
[Serializable, NetSerializable]
public sealed class PlayerSpriteStateRequest : EntityEventArgs
{
/// <summary>
/// The player entity in question
/// </summary>
public readonly NetEntity TargetPlayer;
public PlayerSpriteStateRequest(NetEntity targetPlayer)
{
TargetPlayer = targetPlayer;
}
}
/// <summary>
/// The client's response to a <see cref="PlayerSpriteStateRequest"/>
/// </summary>
[Serializable, NetSerializable]
public sealed class PlayerSpriteStateMessage : EntityEventArgs
{
public readonly NetEntity SpriteEntity;
/// <summary>
/// Data needed to reconstruct the player's sprite component layers
/// </summary>
public readonly PrototypeLayerData[]? SpriteLayerData;
public PlayerSpriteStateMessage(NetEntity spriteEntity, PrototypeLayerData[]? spriteLayerData = null)
{
SpriteEntity = spriteEntity;
SpriteLayerData = spriteLayerData;
}
}

View File

@@ -155,12 +155,7 @@
components:
- type: Transform
anchored: true
- type: Sprite
noRot: true
drawdepth: Mobs
offset: -0.02, 0.45
overrideDir: South
enableOverrideDir: true
- type: Sprite # Sprite data is dynamically set in Client.HolopadSystem
- type: Appearance
- type: TypingIndicator
proto: robot
@@ -177,6 +172,7 @@
alpha: 0.9
intensity: 2
scrollRate: 0.125
offset: -0.02, 0.45
- type: Tag
tags:
- HideContextMenu