diff --git a/Content.Client/Antag/AntagStatusIconSystem.cs b/Content.Client/Antag/AntagStatusIconSystem.cs index bf3955b49a..5d87837893 100644 --- a/Content.Client/Antag/AntagStatusIconSystem.cs +++ b/Content.Client/Antag/AntagStatusIconSystem.cs @@ -1,6 +1,8 @@ -using Content.Shared.Ghost; +using Content.Shared.Antag; +using Content.Shared.Revolutionary.Components; using Content.Shared.StatusIcon; using Content.Shared.StatusIcon.Components; +using Content.Shared.Zombies; using Robust.Client.Player; using Robust.Shared.Prototypes; @@ -9,24 +11,44 @@ namespace Content.Client.Antag; /// /// Used for assigning specified icons for antags. /// -public abstract class AntagStatusIconSystem : SharedStatusIconSystem - where T : IComponent +public sealed class AntagStatusIconSystem : SharedStatusIconSystem { [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IPlayerManager _player = default!; + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(GetRevIcon); + SubscribeLocalEvent(GetIcon); + SubscribeLocalEvent(GetIcon); + } /// - /// Will check if the local player has the same component as the one who called it and give the status icon. + /// Adds a Status Icon on an entity if the player is supposed to see it. /// - /// The status icon that your antag uses - /// The GetStatusIcon event. - protected virtual void GetStatusIcon(string antagStatusIcon, ref GetStatusIconsEvent args) + private void GetIcon(EntityUid uid, T comp, ref GetStatusIconsEvent ev) where T: IAntagStatusIconComponent { - var ent = _player.LocalPlayer?.ControlledEntity; + var ent = _player.LocalSession?.AttachedEntity; + + var canEv = new CanDisplayStatusIconsEvent(ent); + RaiseLocalEvent(uid, ref canEv); + + if (!canEv.Cancelled) + ev.StatusIcons.Add(_prototype.Index(comp.StatusIcon)); + } - if (!HasComp(ent) && !HasComp(ent)) + + /// + /// Adds the Rev Icon on an entity if the player is supposed to see it. This additional function is needed to deal + /// with a special case where if someone is a head rev we only want to display the headrev icon. + /// + private void GetRevIcon(EntityUid uid, RevolutionaryComponent comp, ref GetStatusIconsEvent ev) + { + if (HasComp(uid)) return; - args.StatusIcons.Add(_prototype.Index(antagStatusIcon)); + GetIcon(uid, comp, ref ev); + } } diff --git a/Content.Client/Revolutionary/RevolutionarySystem.cs b/Content.Client/Revolutionary/RevolutionarySystem.cs index 0818b14bc0..682c73f93e 100644 --- a/Content.Client/Revolutionary/RevolutionarySystem.cs +++ b/Content.Client/Revolutionary/RevolutionarySystem.cs @@ -1,5 +1,6 @@ +using Content.Shared.Antag; using Content.Shared.Revolutionary.Components; -using Content.Client.Antag; +using Content.Shared.Ghost; using Content.Shared.StatusIcon.Components; namespace Content.Client.Revolutionary; @@ -7,29 +8,37 @@ namespace Content.Client.Revolutionary; /// /// Used for the client to get status icons from other revs. /// -public sealed class RevolutionarySystem : AntagStatusIconSystem +public sealed class RevolutionarySystem : EntitySystem { + public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(GetRevIcon); - SubscribeLocalEvent(GetHeadRevIcon); + SubscribeLocalEvent(OnCanShowRevIcon); + SubscribeLocalEvent(OnCanShowRevIcon); } /// - /// Checks if the person who triggers the GetStatusIcon event is also a Rev or a HeadRev. + /// Determine whether a client should display the rev icon. /// - private void GetRevIcon(EntityUid uid, RevolutionaryComponent comp, ref GetStatusIconsEvent args) + private void OnCanShowRevIcon(EntityUid uid, T comp, ref CanDisplayStatusIconsEvent args) where T : IAntagStatusIconComponent { - if (!HasComp(uid)) - { - GetStatusIcon(comp.RevStatusIcon, ref args); - } + args.Cancelled = !CanDisplayIcon(args.User, comp.IconVisibleToGhost); } - private void GetHeadRevIcon(EntityUid uid, HeadRevolutionaryComponent comp, ref GetStatusIconsEvent args) + /// + /// The criteria that determine whether a client should see Rev/Head rev icons. + /// + private bool CanDisplayIcon(EntityUid? uid, bool visibleToGhost) { - GetStatusIcon(comp.HeadRevStatusIcon, ref args); + if (HasComp(uid) || HasComp(uid)) + return true; + + if (visibleToGhost && HasComp(uid)) + return true; + + return HasComp(uid); } + } diff --git a/Content.Client/Zombies/ZombieSystem.cs b/Content.Client/Zombies/ZombieSystem.cs index 6d0355f6f8..bd89e978c7 100644 --- a/Content.Client/Zombies/ZombieSystem.cs +++ b/Content.Client/Zombies/ZombieSystem.cs @@ -1,5 +1,5 @@ using System.Linq; -using Content.Client.Antag; +using Content.Shared.Ghost; using Content.Shared.Humanoid; using Content.Shared.StatusIcon.Components; using Content.Shared.Zombies; @@ -7,15 +7,14 @@ namespace Content.Client.Zombies; -public sealed class ZombieSystem : AntagStatusIconSystem +public sealed class ZombieSystem : EntitySystem { - public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnStartup); - SubscribeLocalEvent(OnGetStatusIcon); + SubscribeLocalEvent(OnCanDisplayStatusIcons); } private void OnStartup(EntityUid uid, ZombieComponent component, ComponentStartup args) @@ -32,8 +31,17 @@ private void OnStartup(EntityUid uid, ZombieComponent component, ComponentStartu } } - private void OnGetStatusIcon(EntityUid uid, ZombieComponent component, ref GetStatusIconsEvent args) + /// + /// Determines whether a player should be able to see the StatusIcon for zombies. + /// + private void OnCanDisplayStatusIcons(EntityUid uid, ZombieComponent component, ref CanDisplayStatusIconsEvent args) { - GetStatusIcon(component.ZombieStatusIcon, ref args); + if (HasComp(args.User) || HasComp(args.User)) + return; + + if (component.IconVisibleToGhost && HasComp(args.User)) + return; + + args.Cancelled = true; } } diff --git a/Content.Shared/Antag/IAntagStatusIconComponent.cs b/Content.Shared/Antag/IAntagStatusIconComponent.cs new file mode 100644 index 0000000000..981937c916 --- /dev/null +++ b/Content.Shared/Antag/IAntagStatusIconComponent.cs @@ -0,0 +1,12 @@ +using Content.Shared.StatusIcon; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Antag; + +public interface IAntagStatusIconComponent +{ + public ProtoId StatusIcon { get; set; } + + public bool IconVisibleToGhost { get; set; } +} + diff --git a/Content.Shared/Revolutionary/Components/HeadRevolutionaryComponent.cs b/Content.Shared/Revolutionary/Components/HeadRevolutionaryComponent.cs index 48d7c23097..d2c8374fef 100644 --- a/Content.Shared/Revolutionary/Components/HeadRevolutionaryComponent.cs +++ b/Content.Shared/Revolutionary/Components/HeadRevolutionaryComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Antag; using Robust.Shared.GameStates; using Content.Shared.StatusIcon; using Robust.Shared.Prototypes; @@ -8,17 +9,22 @@ namespace Content.Shared.Revolutionary.Components; /// Component used for marking a Head Rev for conversion and winning/losing. /// [RegisterComponent, NetworkedComponent, Access(typeof(SharedRevolutionarySystem))] -public sealed partial class HeadRevolutionaryComponent : Component +public sealed partial class HeadRevolutionaryComponent : Component, IAntagStatusIconComponent { /// /// The status icon corresponding to the head revolutionary. /// [DataField, ViewVariables(VVAccess.ReadWrite)] - public ProtoId HeadRevStatusIcon = "HeadRevolutionaryFaction"; + public ProtoId StatusIcon { get; set; } = "HeadRevolutionaryFaction"; /// /// How long the stun will last after the user is converted. /// [DataField, ViewVariables(VVAccess.ReadWrite)] public TimeSpan StunTime = TimeSpan.FromSeconds(3); + + public override bool SessionSpecific => true; + + [DataField] + public bool IconVisibleToGhost { get; set; } = true; } diff --git a/Content.Shared/Revolutionary/Components/RevolutionaryComponent.cs b/Content.Shared/Revolutionary/Components/RevolutionaryComponent.cs index e55c87786b..587c148dd7 100644 --- a/Content.Shared/Revolutionary/Components/RevolutionaryComponent.cs +++ b/Content.Shared/Revolutionary/Components/RevolutionaryComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Antag; using Robust.Shared.GameStates; using Content.Shared.StatusIcon; using Robust.Shared.Prototypes; @@ -8,11 +9,16 @@ namespace Content.Shared.Revolutionary.Components; /// Used for marking regular revs as well as storing icon prototypes so you can see fellow revs. /// [RegisterComponent, NetworkedComponent, Access(typeof(SharedRevolutionarySystem))] -public sealed partial class RevolutionaryComponent : Component +public sealed partial class RevolutionaryComponent : Component, IAntagStatusIconComponent { /// /// The status icon prototype displayed for revolutionaries /// [DataField, ViewVariables(VVAccess.ReadWrite)] - public ProtoId RevStatusIcon = "RevolutionaryFaction"; + public ProtoId StatusIcon { get; set; } = "RevolutionaryFaction"; + + public override bool SessionSpecific => true; + + [DataField] + public bool IconVisibleToGhost { get; set; } = true; } diff --git a/Content.Shared/Revolutionary/Components/ShowRevIconsComponent.cs b/Content.Shared/Revolutionary/Components/ShowRevIconsComponent.cs new file mode 100644 index 0000000000..11e20db3f8 --- /dev/null +++ b/Content.Shared/Revolutionary/Components/ShowRevIconsComponent.cs @@ -0,0 +1,11 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Revolutionary.Components; + +/// +/// Determines whether Someone can see rev/headrev icons on revs. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class ShowRevIconsComponent: Component +{ +} diff --git a/Content.Shared/Revolutionary/SharedRevolutionarySystem.cs b/Content.Shared/Revolutionary/SharedRevolutionarySystem.cs index 1399b116e0..ddaf73fcc9 100644 --- a/Content.Shared/Revolutionary/SharedRevolutionarySystem.cs +++ b/Content.Shared/Revolutionary/SharedRevolutionarySystem.cs @@ -1,8 +1,11 @@ +using Content.Shared.Ghost; using Content.Shared.IdentityManagement; using Content.Shared.Mindshield.Components; using Content.Shared.Popups; using Content.Shared.Revolutionary.Components; using Content.Shared.Stunnable; +using Robust.Shared.GameStates; +using Robust.Shared.Player; namespace Content.Shared.Revolutionary; @@ -14,7 +17,13 @@ public sealed class SharedRevolutionarySystem : EntitySystem public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(MindShieldImplanted); + SubscribeLocalEvent(OnRevCompGetStateAttempt); + SubscribeLocalEvent(OnRevCompGetStateAttempt); + SubscribeLocalEvent(DirtyRevComps); + SubscribeLocalEvent(DirtyRevComps); + SubscribeLocalEvent(DirtyRevComps); } /// @@ -37,4 +46,64 @@ private void MindShieldImplanted(EntityUid uid, MindShieldComponent comp, MapIni _popupSystem.PopupEntity(Loc.GetString("rev-break-control", ("name", name)), uid); } } + + /// + /// Determines if a HeadRev component should be sent to the client. + /// + private void OnRevCompGetStateAttempt(EntityUid uid, HeadRevolutionaryComponent comp, ref ComponentGetStateAttemptEvent args) + { + args.Cancelled = !CanGetState(args.Player, comp.IconVisibleToGhost); + } + + /// + /// Determines if a Rev component should be sent to the client. + /// + private void OnRevCompGetStateAttempt(EntityUid uid, RevolutionaryComponent comp, ref ComponentGetStateAttemptEvent args) + { + args.Cancelled = !CanGetState(args.Player, comp.IconVisibleToGhost); + } + + /// + /// The criteria that determine whether a Rev/HeadRev component should be sent to a client. + /// + /// The Player the component will be sent to. + /// Whether the component permits the icon to be visible to observers. + /// + private bool CanGetState(ICommonSession? player, bool visibleToGhosts) + { + //Apparently this can be null in replays so I am just returning true. + if (player is null) + return true; + + var uid = player.AttachedEntity; + + if (HasComp(uid) || HasComp(uid)) + return true; + + if (visibleToGhosts && HasComp(uid)) + return true; + + return HasComp(uid); + } + /// + /// Dirties all the Rev components so they are sent to clients. + /// + /// We need to do this because if a rev component was not earlier sent to a client and for example the client + /// becomes a rev then we need to send all the components to it. To my knowledge there is no way to do this on a + /// per client basis so we are just dirtying all the components. + /// + private void DirtyRevComps(EntityUid someUid, T someComp, ComponentStartup ev) + { + var revComps = AllEntityQuery(); + while (revComps.MoveNext(out var uid, out var comp)) + { + Dirty(uid, comp); + } + + var headRevComps = AllEntityQuery(); + while (headRevComps.MoveNext(out var uid, out var comp)) + { + Dirty(uid, comp); + } + } } diff --git a/Content.Shared/StatusIcon/Components/StatusIconComponent.cs b/Content.Shared/StatusIcon/Components/StatusIconComponent.cs index 9efe9731c8..385f9760c6 100644 --- a/Content.Shared/StatusIcon/Components/StatusIconComponent.cs +++ b/Content.Shared/StatusIcon/Components/StatusIconComponent.cs @@ -25,3 +25,15 @@ public sealed partial class StatusIconComponent : Component /// [ByRefEvent] public record struct GetStatusIconsEvent(List StatusIcons, bool InContainer); + +/// +/// Event raised on the Client-side to determine whether to display a status icon on an entity. +/// +/// The player that will see the icons +[ByRefEvent] +public record struct CanDisplayStatusIconsEvent(EntityUid? User = null) +{ + public EntityUid? User = User; + + public bool Cancelled = false; +} diff --git a/Content.Shared/Zombies/ShowZombieIconsComponent.cs b/Content.Shared/Zombies/ShowZombieIconsComponent.cs new file mode 100644 index 0000000000..a2bc85c074 --- /dev/null +++ b/Content.Shared/Zombies/ShowZombieIconsComponent.cs @@ -0,0 +1,12 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Zombies; + +/// +/// Makes it so an entity can view ZombieAntagIcons. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class ShowZombieIconsComponent: Component +{ + +} diff --git a/Content.Shared/Zombies/ZombieComponent.cs b/Content.Shared/Zombies/ZombieComponent.cs index bb1f6bec5f..5ae441b132 100644 --- a/Content.Shared/Zombies/ZombieComponent.cs +++ b/Content.Shared/Zombies/ZombieComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Antag; using Content.Shared.Chat.Prototypes; using Content.Shared.Chemistry.Reagent; using Content.Shared.Damage; @@ -13,7 +14,7 @@ namespace Content.Shared.Zombies; [RegisterComponent, NetworkedComponent] -public sealed partial class ZombieComponent : Component +public sealed partial class ZombieComponent : Component, IAntagStatusIconComponent { /// /// The baseline infection chance you have if you are completely nude @@ -93,8 +94,11 @@ public sealed partial class ZombieComponent : Component [DataField("nextTick", customTypeSerializer:typeof(TimeOffsetSerializer))] public TimeSpan NextTick; - [DataField("zombieStatusIcon", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string ZombieStatusIcon = "ZombieFaction"; + [DataField("zombieStatusIcon")] + public ProtoId StatusIcon { get; set; } = "ZombieFaction"; + + [DataField] + public bool IconVisibleToGhost { get; set; } = true; /// /// Healing each second diff --git a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml index 1583801e98..364d26dddb 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml @@ -89,6 +89,8 @@ - type: Stripping - type: SolutionScanner - type: IgnoreUIRange + - type: ShowRevIcons + - type: ShowZombieIcons - type: Inventory templateId: aghost - type: InventorySlots