diff --git a/Content.Client/Ghost/Commands/ToggleGhostVisibilityCommand.cs b/Content.Client/Ghost/Commands/ToggleGhostVisibilityCommand.cs index 3dacc81f219cd..10479e5e33d8e 100644 --- a/Content.Client/Ghost/Commands/ToggleGhostVisibilityCommand.cs +++ b/Content.Client/Ghost/Commands/ToggleGhostVisibilityCommand.cs @@ -4,15 +4,15 @@ namespace Content.Client.Ghost.Commands; public sealed class ToggleGhostVisibilityCommand : LocalizedEntityCommands { - [Dependency] private readonly GhostSystem _ghost = default!; + [Dependency] private readonly GhostVisibilitySystem _ghost = default!; public override string Command => "toggleghostvisibility"; public override void Execute(IConsoleShell shell, string argStr, string[] args) { if (args.Length != 0 && bool.TryParse(args[0], out var visibility)) - _ghost.ToggleGhostVisibility(visibility); + _ghost.ShowGhosts = visibility; else - _ghost.ToggleGhostVisibility(); + _ghost.ShowGhosts = !_ghost.ShowGhosts; } } diff --git a/Content.Client/Ghost/GhostSystem.cs b/Content.Client/Ghost/GhostSystem.cs index 58758f54f24de..238e45c3641a8 100644 --- a/Content.Client/Ghost/GhostSystem.cs +++ b/Content.Client/Ghost/GhostSystem.cs @@ -15,32 +15,11 @@ public sealed class GhostSystem : SharedGhostSystem [Dependency] private readonly SharedActionsSystem _actions = default!; [Dependency] private readonly PointLightSystem _pointLightSystem = default!; [Dependency] private readonly ContentEyeSystem _contentEye = default!; + [Dependency] private readonly GhostVisibilitySystem _ghostVis = default!; [Dependency] private readonly SpriteSystem _sprite = default!; public int AvailableGhostRoleCount { get; private set; } - private bool _ghostVisibility = true; - - private bool GhostVisibility - { - get => _ghostVisibility; - set - { - if (_ghostVisibility == value) - { - return; - } - - _ghostVisibility = value; - - var query = AllEntityQuery(); - while (query.MoveNext(out var uid, out _, out var sprite)) - { - _sprite.SetVisible((uid, sprite), value || uid == _playerManager.LocalEntity); - } - } - } - public GhostComponent? Player => CompOrNull(_playerManager.LocalEntity); public bool IsGhost => Player != null; @@ -55,7 +34,6 @@ public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnStartup); SubscribeLocalEvent(OnGhostRemove); SubscribeLocalEvent(OnGhostState); @@ -70,12 +48,6 @@ public override void Initialize() SubscribeLocalEvent(OnToggleGhosts); } - private void OnStartup(EntityUid uid, GhostComponent component, ComponentStartup args) - { - if (TryComp(uid, out SpriteComponent? sprite)) - _sprite.SetVisible((uid, sprite), GhostVisibility || uid == _playerManager.LocalEntity); - } - private void OnToggleLighting(EntityUid uid, EyeComponent component, ToggleLightingActionEvent args) { if (args.Handled) @@ -120,10 +92,10 @@ private void OnToggleGhosts(EntityUid uid, GhostComponent component, ToggleGhost if (args.Handled) return; - var locId = GhostVisibility ? "ghost-gui-toggle-ghost-visibility-popup-off" : "ghost-gui-toggle-ghost-visibility-popup-on"; + var locId = _ghostVis.ShowGhosts ? "ghost-gui-toggle-ghost-visibility-popup-off" : "ghost-gui-toggle-ghost-visibility-popup-on"; Popup.PopupEntity(Loc.GetString(locId), args.Performer); if (uid == _playerManager.LocalEntity) - ToggleGhostVisibility(); + _ghostVis.ShowGhosts = !_ghostVis.ShowGhosts; args.Handled = true; } @@ -138,13 +110,13 @@ private void OnGhostRemove(EntityUid uid, GhostComponent component, ComponentRem if (uid != _playerManager.LocalEntity) return; - GhostVisibility = false; + _ghostVis.ShowGhosts = false; PlayerRemoved?.Invoke(component); } private void OnGhostPlayerAttach(EntityUid uid, GhostComponent component, LocalPlayerAttachedEvent localPlayerAttachedEvent) { - GhostVisibility = true; + _ghostVis.ShowGhosts = true; PlayerAttached?.Invoke(component); } @@ -161,7 +133,7 @@ private void OnGhostState(EntityUid uid, GhostComponent component, ref AfterAuto private void OnGhostPlayerDetach(EntityUid uid, GhostComponent component, LocalPlayerDetachedEvent args) { - GhostVisibility = false; + _ghostVis.ShowGhosts = false; PlayerDetached?.Invoke(); } @@ -196,10 +168,5 @@ public void OpenGhostRoles() { _console.RemoteExecuteCommand(null, "ghostroles"); } - - public void ToggleGhostVisibility(bool? visibility = null) - { - GhostVisibility = visibility ?? !GhostVisibility; - } } } diff --git a/Content.Client/Ghost/GhostVisibilitySystem.cs b/Content.Client/Ghost/GhostVisibilitySystem.cs new file mode 100644 index 0000000000000..b474ff8386854 --- /dev/null +++ b/Content.Client/Ghost/GhostVisibilitySystem.cs @@ -0,0 +1,83 @@ +using Content.Shared.Ghost; +using Robust.Client.GameObjects; +using Robust.Shared.Player; + +namespace Content.Client.Ghost; + +public sealed class GhostVisibilitySystem : SharedGhostVisibilitySystem +{ + [Dependency] private readonly ISharedPlayerManager _player = default!; + [Dependency] private readonly SpriteSystem _sprite = default!; + + private bool _showGhosts; + + /// + /// Whether hidden/invisible ghost sprites should be drawn. + /// + /// + /// This can be used to toggle drawing other spectator ghosts. However, if the ghost is actually visible for + /// everyone they will still get drawn regardless. + /// + public bool ShowGhosts + { + get => _showGhosts; + set + { + if (_showGhosts == value) + { + return; + } + + _showGhosts = value; + var query = AllEntityQuery(); + while (query.MoveNext(out var uid, out var ghost, out var sprite)) + { + UpdateSpriteVisibility((uid, ghost, sprite)); + } + } + } + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnGhostVisState); + SubscribeLocalEvent(OnAttached); + SubscribeLocalEvent(OnDetached); + } + + private void OnDetached(Entity ent, ref PlayerDetachedEvent args) + { + UpdateVisibility(ent.AsNullable()); + } + + private void OnAttached(Entity ent, ref PlayerAttachedEvent args) + { + UpdateVisibility(ent.AsNullable()); + } + + private void OnGhostVisState(EntityUid uid, GhostVisibilityComponent component, ref AfterAutoHandleStateEvent args) + { + UpdateSpriteVisibility((uid, component)); + } + + protected override void UpdateVisibility(Entity ent) + { + if (!Resolve(ent.Owner, ref ent.Comp1)) + return; + + // Intentionally not calling the base event / modifying component data. + // Client cannot predict ghost visibility due to lack of round-end information, and because the global + // visibility is not networked. + + UpdateSpriteVisibility((ent.Owner, ent.Comp1)); + } + + private void UpdateSpriteVisibility(Entity ent) + { + if (!Resolve(ent.Owner, ref ent.Comp1, ref ent.Comp2)) + return; + + var visible = ShowGhosts || ent.Comp1.Visible || ent.Owner == _player.LocalEntity; + _sprite.SetVisible((ent.Owner, ent.Comp2), visible); + } +} diff --git a/Content.Server/Administration/Commands/ShowGhostsCommand.cs b/Content.Server/Administration/Commands/ShowGhostsCommand.cs index 2f8bf79e6980a..d473081cf728a 100644 --- a/Content.Server/Administration/Commands/ShowGhostsCommand.cs +++ b/Content.Server/Administration/Commands/ShowGhostsCommand.cs @@ -1,38 +1,14 @@ -using Content.Server.Ghost; -using Content.Server.Revenant.EntitySystems; -using Content.Shared.Administration; -using Robust.Shared.Console; +using Content.Shared.Administration; +using Content.Shared.Ghost; +using Robust.Shared.Toolshed; -namespace Content.Server.Administration.Commands -{ - [AdminCommand(AdminFlags.Admin)] - public sealed class ShowGhostsCommand : IConsoleCommand - { - [Dependency] private readonly IEntityManager _entities = default!; - - public string Command => "showghosts"; - public string Description => "makes all of the currently present ghosts visible. Cannot be reversed."; - public string Help => "showghosts "; - - public void Execute(IConsoleShell shell, string argStr, string[] args) - { - if (args.Length != 1) - { - shell.WriteError(Loc.GetString("shell-wrong-arguments-number")); - return; - } +namespace Content.Server.Administration.Commands; - if (!bool.TryParse(args[0], out var visible)) - { - shell.WriteError(Loc.GetString("shell-invalid-bool")); - return; - } - - var ghostSys = _entities.EntitySysManager.GetEntitySystem(); - var revSys = _entities.EntitySysManager.GetEntitySystem(); +[AdminCommand(AdminFlags.Admin)] +public sealed class ShowGhostsCommand : ToolshedCommand +{ + [Dependency] private readonly SharedGhostVisibilitySystem _ghostVis = default!; - ghostSys.MakeVisible(visible); - revSys.MakeVisible(visible); - } - } + [CommandImplementation] + public void ShowGhosts(bool visible) => _ghostVis.SetAllVisible(visible); } diff --git a/Content.Server/Ghost/GhostSystem.cs b/Content.Server/Ghost/GhostSystem.cs index 1a3c9031fe743..f548bb21f4292 100644 --- a/Content.Server/Ghost/GhostSystem.cs +++ b/Content.Server/Ghost/GhostSystem.cs @@ -12,7 +12,6 @@ using Content.Shared.Damage.Prototypes; using Content.Shared.Database; using Content.Shared.Examine; -using Content.Shared.Eye; using Content.Shared.FixedPoint; using Content.Shared.Follower; using Content.Shared.Ghost; @@ -54,7 +53,6 @@ public sealed class GhostSystem : SharedGhostSystem [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly ISharedPlayerManager _player = default!; [Dependency] private readonly TransformSystem _transformSystem = default!; - [Dependency] private readonly VisibilitySystem _visibilitySystem = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; @@ -65,13 +63,11 @@ public sealed class GhostSystem : SharedGhostSystem [Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly TagSystem _tag = default!; [Dependency] private readonly NameModifierSystem _nameMod = default!; private EntityQuery _ghostQuery; private EntityQuery _physicsQuery; - private static readonly ProtoId AllowGhostShownByEventTag = "AllowGhostShownByEvent"; private static readonly ProtoId AsphyxiationDamageType = "Asphyxiation"; public override void Initialize() @@ -101,20 +97,6 @@ public override void Initialize() SubscribeLocalEvent(OnActionPerform); SubscribeLocalEvent(OnGhostHearingAction); SubscribeLocalEvent(OnEntityStorageInsertAttempt); - - SubscribeLocalEvent(_ => MakeVisible(true)); - SubscribeLocalEvent(OnToggleGhostVisibilityToAll); - - SubscribeLocalEvent(OnGhostVis); - } - - private void OnGhostVis(Entity ent, ref GetVisMaskEvent args) - { - // If component not deleting they can see ghosts. - if (ent.Comp.LifeStage <= ComponentLifeStage.Running) - { - args.VisibilityMask |= (int)VisibilityFlags.Ghost; - } } private void OnGhostHearingAction(EntityUid uid, GhostComponent component, ToggleGhostHearingActionEvent args) @@ -191,37 +173,13 @@ private void OnRelayMoveInput(EntityUid uid, GhostOnMoveComponent component, ref private void OnGhostStartup(EntityUid uid, GhostComponent component, ComponentStartup args) { - // Allow this entity to be seen by other ghosts. - var visibility = EnsureComp(uid); - - if (_gameTicker.RunLevel != GameRunLevel.PostRound) - { - _visibilitySystem.AddLayer((uid, visibility), (int) VisibilityFlags.Ghost, false); - _visibilitySystem.RemoveLayer((uid, visibility), (int) VisibilityFlags.Normal, false); - _visibilitySystem.RefreshVisibility(uid, visibilityComponent: visibility); - } - - _eye.RefreshVisibilityMask(uid); + EnsureComp(uid); var time = _gameTiming.CurTime; component.TimeOfDeath = time; } private void OnGhostShutdown(EntityUid uid, GhostComponent component, ComponentShutdown args) { - // Perf: If the entity is deleting itself, no reason to change these back. - if (Terminating(uid)) - return; - - // Entity can't be seen by ghosts anymore. - if (TryComp(uid, out VisibilityComponent? visibility)) - { - _visibilitySystem.RemoveLayer((uid, visibility), (int) VisibilityFlags.Ghost, false); - _visibilitySystem.AddLayer((uid, visibility), (int) VisibilityFlags.Normal, false); - _visibilitySystem.RefreshVisibility(uid, visibilityComponent: visibility); - } - - // Entity can't see ghosts anymore. - _eye.RefreshVisibilityMask(uid); _actions.RemoveAction(uid, component.BooActionEntity); } @@ -248,24 +206,16 @@ private void OnGhostExamine(EntityUid uid, GhostComponent component, ExaminedEve private void OnMindRemovedMessage(EntityUid uid, GhostComponent component, MindRemovedMessage args) { - DeleteEntity(uid); + QueueDel(uid); } private void OnMindUnvisitedMessage(EntityUid uid, GhostComponent component, MindUnvisitedMessage args) { - DeleteEntity(uid); + QueueDel(uid); } private void OnPlayerDetached(EntityUid uid, GhostComponent component, PlayerDetachedEvent args) { - DeleteEntity(uid); - } - - private void DeleteEntity(EntityUid uid) - { - if (Deleted(uid) || Terminating(uid)) - return; - QueueDel(uid); } @@ -388,40 +338,6 @@ private void OnEntityStorageInsertAttempt(EntityUid uid, GhostComponent comp, re args.Cancelled = true; } - private void OnToggleGhostVisibilityToAll(ToggleGhostVisibilityToAllEvent ev) - { - if (ev.Handled) - return; - - ev.Handled = true; - MakeVisible(true); - } - - /// - /// When the round ends, make all players able to see ghosts. - /// - public void MakeVisible(bool visible) - { - var entityQuery = EntityQueryEnumerator(); - while (entityQuery.MoveNext(out var uid, out var _, out var vis)) - { - if (!_tag.HasTag(uid, AllowGhostShownByEventTag)) - continue; - - if (visible) - { - _visibilitySystem.AddLayer((uid, vis), (int) VisibilityFlags.Normal, false); - _visibilitySystem.RemoveLayer((uid, vis), (int) VisibilityFlags.Ghost, false); - } - else - { - _visibilitySystem.AddLayer((uid, vis), (int) VisibilityFlags.Ghost, false); - _visibilitySystem.RemoveLayer((uid, vis), (int) VisibilityFlags.Normal, false); - } - _visibilitySystem.RefreshVisibility(uid, visibilityComponent: vis); - } - } - public bool DoGhostBooEvent(EntityUid target) { var ghostBoo = new GhostBooEvent(); diff --git a/Content.Server/Ghost/GhostVisibilitySystem.cs b/Content.Server/Ghost/GhostVisibilitySystem.cs new file mode 100644 index 0000000000000..f1cb44f364ccf --- /dev/null +++ b/Content.Server/Ghost/GhostVisibilitySystem.cs @@ -0,0 +1,39 @@ +using Content.Server.GameTicking; +using Content.Shared.Ghost; + +namespace Content.Server.Ghost; + +public sealed class GhostVisibilitySystem : SharedGhostVisibilitySystem +{ + [Dependency] private readonly GameTicker _ticker = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnRunLevelChanged); + } + + private void OnRunLevelChanged(GameRunLevelChangedEvent ev) + { + // Reset global visibility. + if (ev.New is GameRunLevel.PreRoundLobby or GameRunLevel.InRound) + AllVisible = false; + + var entityQuery = EntityQueryEnumerator(); + while (entityQuery.MoveNext(out var uid, out var ghost, out var vis)) + { + UpdateVisibility((uid, ghost, vis)); + } + } + + protected override bool ShouldBeVisible(GhostVisibilityComponent comp) + { + if (comp.VisibleOverride is { } val) + return val; + + if (base.ShouldBeVisible(comp)) + return true; + + return _ticker.RunLevel == GameRunLevel.PostRound && comp.VisibleOnRoundEnd; + } +} diff --git a/Content.Server/Revenant/EntitySystems/CorporealSystem.cs b/Content.Server/Revenant/EntitySystems/CorporealSystem.cs index 5f31a2f280a5a..09afb20abd3f8 100644 --- a/Content.Server/Revenant/EntitySystems/CorporealSystem.cs +++ b/Content.Server/Revenant/EntitySystems/CorporealSystem.cs @@ -1,37 +1,24 @@ -using Content.Server.GameTicking; -using Content.Shared.Eye; +using Content.Server.Ghost; +using Content.Shared.Ghost; using Content.Shared.Revenant.Components; using Content.Shared.Revenant.EntitySystems; -using Robust.Server.GameObjects; namespace Content.Server.Revenant.EntitySystems; public sealed class CorporealSystem : SharedCorporealSystem { - [Dependency] private readonly VisibilitySystem _visibilitySystem = default!; - [Dependency] private readonly GameTicker _ticker = default!; + [Dependency] private readonly GhostVisibilitySystem _ghostVis = default!; public override void OnStartup(EntityUid uid, CorporealComponent component, ComponentStartup args) { base.OnStartup(uid, component, args); - - if (TryComp(uid, out var visibility)) - { - _visibilitySystem.RemoveLayer((uid, visibility), (int) VisibilityFlags.Ghost, false); - _visibilitySystem.AddLayer((uid, visibility), (int) VisibilityFlags.Normal, false); - _visibilitySystem.RefreshVisibility(uid, visibility); - } + var ghost = EnsureComp(uid); + _ghostVis.SetVisibleOverride((uid, ghost), true); } public override void OnShutdown(EntityUid uid, CorporealComponent component, ComponentShutdown args) { base.OnShutdown(uid, component, args); - - if (TryComp(uid, out var visibility) && _ticker.RunLevel != GameRunLevel.PostRound) - { - _visibilitySystem.AddLayer((uid, visibility), (int) VisibilityFlags.Ghost, false); - _visibilitySystem.RemoveLayer((uid, visibility), (int) VisibilityFlags.Normal, false); - _visibilitySystem.RefreshVisibility(uid, visibility); - } + _ghostVis.SetVisibleOverride(uid, null); } } diff --git a/Content.Server/Revenant/EntitySystems/RevenantSystem.cs b/Content.Server/Revenant/EntitySystems/RevenantSystem.cs index b89f10934d98f..0b88556219a02 100644 --- a/Content.Server/Revenant/EntitySystems/RevenantSystem.cs +++ b/Content.Server/Revenant/EntitySystems/RevenantSystem.cs @@ -1,14 +1,12 @@ using System.Numerics; using Content.Server.Actions; -using Content.Server.GameTicking; -using Content.Server.Store.Components; using Content.Server.Store.Systems; using Content.Shared.Alert; using Content.Shared.Damage; using Content.Shared.DoAfter; using Content.Shared.Examine; -using Content.Shared.Eye; using Content.Shared.FixedPoint; +using Content.Shared.Ghost; using Content.Shared.Interaction; using Content.Shared.Maps; using Content.Shared.Mobs.Systems; @@ -33,11 +31,9 @@ public sealed partial class RevenantSystem : EntitySystem [Dependency] private readonly AlertsSystem _alerts = default!; [Dependency] private readonly DamageableSystem _damage = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; - [Dependency] private readonly GameTicker _ticker = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly PhysicsSystem _physics = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; - [Dependency] private readonly SharedEyeSystem _eye = default!; [Dependency] private readonly StatusEffectsSystem _statusEffects = default!; [Dependency] private readonly SharedInteractionSystem _interact = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; @@ -61,20 +57,14 @@ public override void Initialize() SubscribeLocalEvent(OnExamine); SubscribeLocalEvent(OnStatusAdded); SubscribeLocalEvent(OnStatusEnded); - SubscribeLocalEvent(_ => MakeVisible(true)); - - SubscribeLocalEvent(OnRevenantGetVis); InitializeAbilities(); } - private void OnRevenantGetVis(Entity ent, ref GetVisMaskEvent args) - { - args.VisibilityMask |= (int)VisibilityFlags.Ghost; - } - private void OnStartup(EntityUid uid, RevenantComponent component, ComponentStartup args) { + EnsureComp(uid); + //update the icon ChangeEssenceAmount(uid, 0, component); @@ -82,16 +72,6 @@ private void OnStartup(EntityUid uid, RevenantComponent component, ComponentStar _appearance.SetData(uid, RevenantVisuals.Corporeal, false); _appearance.SetData(uid, RevenantVisuals.Harvesting, false); _appearance.SetData(uid, RevenantVisuals.Stunned, false); - - if (_ticker.RunLevel == GameRunLevel.PostRound && TryComp(uid, out var visibility)) - { - _visibility.AddLayer((uid, visibility), (int) VisibilityFlags.Ghost, false); - _visibility.RemoveLayer((uid, visibility), (int) VisibilityFlags.Normal, false); - _visibility.RefreshVisibility(uid, visibility); - } - - //ghost vision - _eye.RefreshVisibilityMask(uid); } private void OnMapInit(EntityUid uid, RevenantComponent component, MapInitEvent args) @@ -189,25 +169,6 @@ private void OnShop(EntityUid uid, RevenantComponent component, RevenantShopActi _store.ToggleUi(uid, uid, store); } - public void MakeVisible(bool visible) - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out _, out var vis)) - { - if (visible) - { - _visibility.AddLayer((uid, vis), (int) VisibilityFlags.Normal, false); - _visibility.RemoveLayer((uid, vis), (int) VisibilityFlags.Ghost, false); - } - else - { - _visibility.AddLayer((uid, vis), (int) VisibilityFlags.Ghost, false); - _visibility.RemoveLayer((uid, vis), (int) VisibilityFlags.Normal, false); - } - _visibility.RefreshVisibility(uid, vis); - } - } - public override void Update(float frameTime) { base.Update(frameTime); diff --git a/Content.Shared/Ghost/GhostVisibilityComponent.cs b/Content.Shared/Ghost/GhostVisibilityComponent.cs new file mode 100644 index 0000000000000..3e8f170737741 --- /dev/null +++ b/Content.Shared/Ghost/GhostVisibilityComponent.cs @@ -0,0 +1,58 @@ +using Content.Shared.Eye; +using Robust.Shared.GameStates; + +namespace Content.Shared.Ghost; + +/// +/// This component modifies visibility masks & sprite visibility for entities. +/// This exists to avoid code duplication between ghosts, and ghost-like entities (e.g., revenants). +/// +[RegisterComponent, NetworkedComponent, Access(typeof(SharedGhostVisibilitySystem))] +[AutoGenerateComponentState(true)] +public sealed partial class GhostVisibilityComponent : Component +{ + /// + /// Whether the ghost is currently visible. + /// + [DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadOnly)] + public bool Visible; + + /// + /// Optional override for normal ghost visibility rules. Can be used to make a ghost always visible. + /// + [DataField] + public bool? VisibleOverride; + + /// + /// Whether the ghost can be revealed by global visibility settings (e.g., wizard shenanigans). + /// + /// + /// Admin ghosts should not be getting revealed by the wizard. + /// Similarly, revenants should be able to continue functioning. + /// + [DataField] + public bool IgnoreGlobalVisibility = true; + // This defaults to true / is opt-in because that's how it was before I refactored it. IMO it should be opt-out. + + /// + /// Whether the ghost will be revealed after the round ends. + /// + /// + /// Admin ghosts should not be getting revealed at the end of the round. + /// + [DataField] + public bool VisibleOnRoundEnd; + // This defaults to false / is opt-in because that's how it was before I refactored it. IMO it should be opt-out. + + /// + /// Visibility layers to add or remove from this entity when ghost-visibility is toggled. + /// + [DataField] + public VisibilityFlags Layer = VisibilityFlags.Ghost; + + /// + /// Eye visibility mask to add to this entity.. + /// + [DataField] + public VisibilityFlags Mask = VisibilityFlags.Ghost; +} diff --git a/Content.Shared/Ghost/SharedGhostSystem.cs b/Content.Shared/Ghost/SharedGhostSystem.cs index 7d3561a79f997..3c0ec7162d6b2 100644 --- a/Content.Shared/Ghost/SharedGhostSystem.cs +++ b/Content.Shared/Ghost/SharedGhostSystem.cs @@ -85,7 +85,6 @@ public void SetCanReturnToBody(GhostComponent component, bool value) SetCanReturnToBody((component.Owner, component), value); } - /// /// Sets whether the ghost is allowed to interact with other entities. /// diff --git a/Content.Shared/Ghost/SharedGhostVisibilitySystem.cs b/Content.Shared/Ghost/SharedGhostVisibilitySystem.cs new file mode 100644 index 0000000000000..d5ad18cd62552 --- /dev/null +++ b/Content.Shared/Ghost/SharedGhostVisibilitySystem.cs @@ -0,0 +1,137 @@ +using Content.Shared.Eye; + +namespace Content.Shared.Ghost; + +/// +/// System for the . +/// Prevents ghosts from interacting when is false. +/// +public abstract class SharedGhostVisibilitySystem : EntitySystem +{ + public bool AllVisible { get; protected set; } + + [Dependency] private readonly SharedEyeSystem _eye = default!; + [Dependency] private readonly SharedVisibilitySystem _visibility = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnGhostVisStartup); + SubscribeLocalEvent(OnGhostVisShutdown); + SubscribeLocalEvent(OnGhostVis); + SubscribeLocalEvent(OnToggleGhostVisibilityToAll); + } + + private void OnGhostVisStartup(EntityUid uid, GhostVisibilityComponent component, ComponentStartup args) + { + EnsureComp(uid); + UpdateVisibility((uid, component)); + _eye.RefreshVisibilityMask(uid); + } + + private void OnGhostVisShutdown(EntityUid uid, GhostVisibilityComponent component, ComponentShutdown args) + { + UpdateVisibility((uid, component)); + _eye.RefreshVisibilityMask(uid); + } + + private void OnGhostVis(Entity ent, ref GetVisMaskEvent args) + { + // If component not deleting they can see ghosts. + if (ent.Comp.LifeStage <= ComponentLifeStage.Running) + args.VisibilityMask |= (int)ent.Comp.Mask; + } + + protected virtual void UpdateVisibility(Entity ent) + { + if (Terminating(ent.Owner)) + return; + + if (!Resolve(ent.Owner, ref ent.Comp1)) + return; + + SetVisible(ent, ShouldBeVisible(ent.Comp1)); + } + + protected virtual void SetVisible(Entity ghost, bool visible) + { + if (!Resolve(ghost.Owner, ref ghost.Comp1)) + return; + + if (ghost.Comp1.Visible == visible && ghost.Comp1.LifeStage >= ComponentLifeStage.Running) + return; + + // VisibilityComponent might not exist yet, and will not get added on client + Resolve(ghost.Owner, ref ghost.Comp2, false); + + if (visible) + { + _visibility.RemoveLayer((ghost.Owner, ghost.Comp2), (ushort)ghost.Comp1.Layer, false); + _visibility.AddLayer((ghost.Owner, ghost.Comp2), (ushort)VisibilityFlags.Normal, false); + } + else + { + _visibility.AddLayer((ghost.Owner, ghost.Comp2), (ushort)ghost.Comp1.Layer, false); + _visibility.RemoveLayer((ghost.Owner, ghost.Comp2), (ushort)VisibilityFlags.Normal, false); + } + + _visibility.RefreshVisibility(ghost.Owner, ghost.Comp2); + + ghost.Comp1.Visible = visible; + Dirty(ghost.Owner, ghost.Comp1); + } + + /// + /// Make a ghost visible regardless of the usual ghost visibility rules. + /// + public void SetVisibleOverride(Entity ghost, bool? visOverride) + { + if (!Resolve(ghost.Owner, ref ghost.Comp1)) + return; + + if (ghost.Comp1.VisibleOverride == visOverride) + return; + + ghost.Comp1.VisibleOverride = visOverride; + UpdateVisibility(ghost); + } + + protected virtual bool ShouldBeVisible(GhostVisibilityComponent comp) + { + if (comp.VisibleOverride is {} val) + return val; + + return AllVisible && !comp.IgnoreGlobalVisibility; + } + + /// + /// Set visibility of all whitelisted "observer" ghosts. + /// + public void SetAllVisible(bool visible) + { + if (AllVisible == visible) + return; + + AllVisible = visible; + + var entityQuery = EntityQueryEnumerator(); + while (entityQuery.MoveNext(out var uid, out var ghost, out var vis)) + { + if (!ghost.IgnoreGlobalVisibility) + UpdateVisibility((uid, ghost, vis)); + } + } + + /// Handle wizard ghost visibility action + private void OnToggleGhostVisibilityToAll(ToggleGhostVisibilityToAllEvent ev) + { + if (ev.Handled) + return; + + ev.Handled = true; + + // TODO make this actually toggle? + SetAllVisible(true); + } + +} diff --git a/Resources/Locale/en-US/commands/ghost-visibility-commands.ftl b/Resources/Locale/en-US/commands/ghost-visibility-commands.ftl index a4eaed21f65fa..6a5a65180bb18 100644 --- a/Resources/Locale/en-US/commands/ghost-visibility-commands.ftl +++ b/Resources/Locale/en-US/commands/ghost-visibility-commands.ftl @@ -1,6 +1,9 @@ -cmd-toggleghostvisibility-desc = Toggles ghost visibility on the client. +#client-side +cmd-toggleghostvisibility-desc = Toggles ghost visibility on the client. cmd-toggleghostvisibility-help = Usage: toggleghostvisibility [bool] - cmd-toggleselfghost-desc = Toggles seeing your own ghost. cmd-toggleselfghost-help = Usage: toggleselfghost cmd-toggleselfghost-must-be-ghost = Entity must be a ghost. + +#server-side +cmd-showghosts-desc = Set visibility of all normal observer ghosts diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml b/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml index 9b0bfb05ca9ca..d5fd67af96dcd 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml @@ -8,6 +8,8 @@ components: - type: Emag emagType: All + - type: GhostVisibility + visibleOnRoundEnd: true - type: Input context: "ghost" - type: Spectral diff --git a/Resources/Prototypes/Entities/Mobs/Player/observer.yml b/Resources/Prototypes/Entities/Mobs/Player/observer.yml index cb0cfdb693964..82949bb52e11b 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/observer.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/observer.yml @@ -55,6 +55,7 @@ skipChecks: true - type: Ghost - type: GhostHearing + - type: Visibility - type: ShowElectrocutionHUD - type: IntrinsicRadioReceiver - type: ActiveRadio @@ -75,9 +76,9 @@ id: MobObserver components: - type: Spectral - - type: Tag - tags: - - AllowGhostShownByEvent + - type: GhostVisibility + ignoreGlobalVisibility: false # affected by wizard show-ghosts action + visibleOnRoundEnd: true - type: entity parent: BaseMentalAction diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml index 174374beb8eca..f3d68ba0e7a22 100644 --- a/Resources/Prototypes/tags.yml +++ b/Resources/Prototypes/tags.yml @@ -18,9 +18,6 @@ - type: Tag id: AllowBiomeLoading # Entities with this tag will load terrain, even if a ghost. -- type: Tag - id: AllowGhostShownByEvent - - type: Tag id: Ambrosia