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)
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)
+ // Get required components
+ if (!TryComp<SpriteComponent>(hologram, out var hologramSprite) ||
+ !TryComp<HolopadHologramComponent>(hologram, out var holopadhologram))
return;
- if (!TryComp<SpriteComponent>(player, out var playerSprite))
- return;
-
- var spriteLayerData = new List<PrototypeLayerData>();
+ // Remove all sprite layers
+ for (int i = hologramSprite.AllLayers.Count() - 1; i >= 0; i--)
+ hologramSprite.RemoveLayer(i);
- if (playerSprite.Visible)
+ if (TryComp<SpriteComponent>(target, out var targetSprite))
{
- // Record the RSI paths, state names and shader paramaters of all visible layers
- for (int i = 0; i < playerSprite.AllLayers.Count(); i++)
+ // Use the target's holographic avatar (if available)
+ if (TryComp<HolographicAvatarComponent>(target, out var targetAvatar) &&
+ targetAvatar.LayerData != null)
{
- 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)
+ for (int i = 0; i < targetAvatar.LayerData.Length; i++)
{
- 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
- };
- }
+ var layer = targetAvatar.LayerData[i];
+ hologramSprite.AddLayer(targetAvatar.LayerData[i], i);
}
+ }
- spriteLayerData.Add(layerDatum);
+ // Otherwise copy the target's current physical appearance
+ else
+ {
+ hologramSprite.CopyFrom(targetSprite);
}
}
- // 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))
- return;
+ // There is no target, display a default sprite instead (if available)
+ else
+ {
+ if (string.IsNullOrEmpty(holopadhologram.RsiPath) || string.IsNullOrEmpty(holopadhologram.RsiState))
+ return;
- for (int i = hologramSprite.AllLayers.Count() - 1; i >= 0; i--)
- hologramSprite.RemoveLayer(i);
+ var layer = new PrototypeLayerData();
+ layer.RsiPath = holopadhologram.RsiPath;
+ layer.State = holopadhologram.RsiState;
- if (layerData == null || layerData.Length == 0)
- {
- layerData = new PrototypeLayerData[1];
- layerData[0] = new PrototypeLayerData()
- {
- RsiPath = holopadhologram.RsiPath,
- State = holopadhologram.RsiState
- };
+ hologramSprite.AddLayer(layer);
}
- for (int i = 0; i < layerData.Length; i++)
- {
- var layer = layerData[i];
- layer.Shader = "unshaded";
+ // 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;
- hologramSprite.AddLayer(layerData[i], i);
+ // 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(uid, hologramSprite, holopadhologram);
+ UpdateHologramShader(hologram, hologramSprite, holopadhologram);
}
private void UpdateHologramShader(EntityUid uid, SpriteComponent sprite, HolopadHologramComponent holopadHologram)
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;
[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);
// Networked events
SubscribeNetworkEvent<HolopadUserTypingChangedEvent>(OnTypingChanged);
- SubscribeNetworkEvent<PlayerSpriteStateMessage>(OnPlayerSpriteStateMessage);
// Component start/shutdown events
SubscribeLocalEvent<HolopadComponent, ComponentInit>(OnHolopadInit);
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)
}
}
- 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
}
}
}
-
- _recentlyUpdatedHolograms.Clear();
}
public void UpdateUIState(Entity<HolopadComponent> entity, TelephoneComponent? telephone = null)
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)
{
// 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 == null)
+ _appearanceSystem.SetData(linkedHolopad.Comp.Hologram.Value.Owner, TypingIndicatorVisuals.IsTyping, false);
- if (user.Value.Comp.LifeStage < ComponentLifeStage.Stopping)
- RemComp<HolopadUserComponent>(user.Value);
+ linkedHolopad.Comp.Hologram.Value.Comp.LinkedEntity = user;
+ Dirty(linkedHolopad.Comp.Hologram.Value);
}
}
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))
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;
}
using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
using System.Numerics;
namespace Content.Shared.Holopad;
/// <summary>
/// Holds data pertaining to holopad holograms
/// </summary>
-[RegisterComponent, NetworkedComponent]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class HolopadHologramComponent : Component
{
/// <summary>
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;
}
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>
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;
- }
-}
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
alpha: 0.9
intensity: 2
scrollRate: 0.125
+ offset: -0.02, 0.45
- type: Tag
tags:
- HideContextMenu