diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs index e8c53de9eb..723ddbf0ff 100644 --- a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs +++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs @@ -30,7 +30,7 @@ public sealed class SurveillanceCameraMicrophoneSystem : EntitySystem // This function ensures that chat popups appear on camera views that have connected microphones. foreach (var (_, __, camera, xform) in EntityQuery()) { - if (camera.ActiveViewers.Count == 0) + if (camera.ActivePvsViewers.Count == 0) continue; // get range to camera. This way wispers will still appear as obfuscated if they are too far from the camera's microphone @@ -41,7 +41,7 @@ public sealed class SurveillanceCameraMicrophoneSystem : EntitySystem if (range < 0 || range > ev.VoiceRange) continue; - foreach (var viewer in camera.ActiveViewers) + foreach (var viewer in camera.ActivePvsViewers) { // if the player has not already received the chat message, send it to them but don't log it to the chat // window. This is simply so that it appears in camera. diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.Collide.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.Collide.cs new file mode 100644 index 0000000000..4015a65cc9 --- /dev/null +++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.Collide.cs @@ -0,0 +1,101 @@ +using Content.Shared.Power.EntitySystems; +using Content.Shared.SurveillanceCamera; +using Content.Shared.SurveillanceCamera.Components; +using Robust.Shared.Physics.Events; +using Robust.Shared.Physics.Systems; + +namespace Content.Server.SurveillanceCamera; + +public partial class SurveillanceCameraSystem +{ + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly SharedPowerReceiverSystem _power = default!; + [Dependency] private readonly EntityQuery _cameraQuery = default!; + + public void InitializeCollide() + { + SubscribeLocalEvent(OnPreventCollide); + SubscribeLocalEvent(OnStart); + SubscribeLocalEvent(OnEnd); + + SubscribeLocalEvent(OnCollideShutdown); + SubscribeLocalEvent(OnOverrideState); + } + + private void OnCollideShutdown(Entity ent, ref ComponentShutdown args) + { + // TODO: Check this on the event. + if (TerminatingOrDeleted(ent.Owner)) + return; + + // Regenerate contacts for everything we were colliding with. + var contacts = _physics.GetContacts(ent.Owner); + + while (contacts.MoveNext(out var contact)) + { + if (!contact.IsTouching) + continue; + + var other = contact.OtherEnt(ent.Owner); + + if (_cameraQuery.HasComp(other)) + { + _physics.RegenerateContacts(other); + } + } + } + + // You may be wondering what de fok this is doing here. + // At the moment there's no easy way to do collision whitelists based on components. + private void OnPreventCollide(Entity ent, ref PreventCollideEvent args) + { + if (!_cameraQuery.HasComp(args.OtherEntity)) + { + args.Cancelled = true; + } + } + + private void OnEnd(Entity ent, ref EndCollideEvent args) + { + if (args.OurFixtureId != ent.Comp.FixtureId) + return; + + if (!_cameraQuery.TryComp(args.OtherEntity, out var cameraCollider)) + return; + + // TODO: Engine bug IsTouching box2d yay. + var contacts = _physics.GetTouchingContacts(args.OtherEntity) - 1; + + if (contacts > 0) + return; + + cameraCollider.Enabled = false; + Dirty(args.OtherEntity, cameraCollider); + UpdateVisuals(args.OtherEntity); + } + + private void OnStart(Entity ent, ref StartCollideEvent args) + { + if (args.OurFixtureId != ent.Comp.FixtureId) + return; + + if (!_cameraQuery.TryComp(args.OtherEntity, out var cameraCollider)) + return; + + cameraCollider.Enabled = true; + Dirty(args.OtherEntity, cameraCollider); + UpdateVisuals(args.OtherEntity); + } + + private void OnOverrideState(Entity ent, ref SurveillanceCameraGetIsViewedExternallyEvent args) + { + if (ent.Comp.RequiresPower && !_power.IsPowered(ent.Owner)) + return; + + if (!ent.Comp.Enabled) + return; + + args.Viewed = true; + } +} + diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.cs index cdd032c79c..09c2b39447 100644 --- a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.cs +++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.cs @@ -13,15 +13,15 @@ using Content.Shared.DeviceNetwork.Components; namespace Content.Server.SurveillanceCamera; -public sealed class SurveillanceCameraSystem : SharedSurveillanceCameraSystem +public sealed partial class SurveillanceCameraSystem : SharedSurveillanceCameraSystem { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly ViewSubscriberSystem _viewSubscriberSystem = default!; [Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!; [Dependency] private readonly UserInterfaceSystem _userInterface = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly SurveillanceCameraMapSystem _cameraMapSystem = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; // Pings a surveillance camera subnet. All cameras will always respond // with a data message if they are on the same subnet. @@ -61,6 +61,8 @@ public sealed class SurveillanceCameraSystem : SharedSurveillanceCameraSystem SubscribeLocalEvent(OnPacketReceived); SubscribeLocalEvent(OnSetName); SubscribeLocalEvent(OnSetNetwork); + + InitializeCollide(); } private void OnPacketReceived(EntityUid uid, SurveillanceCameraComponent component, DeviceNetworkPacketEvent args) @@ -232,7 +234,7 @@ public sealed class SurveillanceCameraSystem : SharedSurveillanceCameraSystem var ev = new SurveillanceCameraDeactivateEvent(camera); - RemoveActiveViewers(camera, new(component.ActiveViewers), null, component); + RemoveActiveViewers(camera, new(component.ActivePvsViewers), null, component); component.Active = false; // Send a targetted event to all monitors. @@ -249,6 +251,25 @@ public sealed class SurveillanceCameraSystem : SharedSurveillanceCameraSystem UpdateVisuals(camera, component); } + /// + /// Checks whether the camera is being viewed through by anyone at all. + /// + /// The camera to check + /// True if the camera is looked through, otherwise False. + public bool IsGettingViewed(Entity ent) + { + if (!Resolve(ent, ref ent.Comp)) + return false; + + if (ent.Comp.ActivePvsViewers.Count > 0 || ent.Comp.ActiveMonitors.Count > 0) + return true; + + var ev = new SurveillanceCameraGetIsViewedExternallyEvent(); + RaiseLocalEvent(ent, ref ev); + + return ev.Viewed; + } + public override void SetActive(EntityUid camera, bool setting, SurveillanceCameraComponent? component = null) { if (!Resolve(camera, ref component)) @@ -284,7 +305,8 @@ public sealed class SurveillanceCameraSystem : SharedSurveillanceCameraSystem } _viewSubscriberSystem.AddViewSubscriber(camera, actor.PlayerSession); - component.ActiveViewers.Add(player); + + component.ActivePvsViewers.Add(player); if (monitor != null) { @@ -346,7 +368,7 @@ public sealed class SurveillanceCameraSystem : SharedSurveillanceCameraSystem if (Resolve(player, ref actor)) _viewSubscriberSystem.RemoveViewSubscriber(camera, actor.PlayerSession); - component.ActiveViewers.Remove(player); + component.ActivePvsViewers.Remove(player); if (monitor != null) { @@ -391,7 +413,7 @@ public sealed class SurveillanceCameraSystem : SharedSurveillanceCameraSystem key = SurveillanceCameraVisuals.Active; } - if (component.ActiveViewers.Count > 0 || component.ActiveMonitors.Count > 0) + if (IsGettingViewed((uid, component))) { key = SurveillanceCameraVisuals.InUse; } diff --git a/Content.Shared/Light/Components/LightOnCollideColliderComponent.cs b/Content.Shared/Light/Components/LightOnCollideColliderComponent.cs deleted file mode 100644 index 39be05a148..0000000000 --- a/Content.Shared/Light/Components/LightOnCollideColliderComponent.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Robust.Shared.GameStates; - -namespace Content.Shared.Light.Components; - -/// -/// Can activate when collided with. -/// -[RegisterComponent, NetworkedComponent] -public sealed partial class LightOnCollideColliderComponent : Component -{ - [DataField] - public string FixtureId = "lightTrigger"; -} diff --git a/Content.Shared/Light/Components/LightOnCollideComponent.cs b/Content.Shared/Light/Components/LightOnCollideComponent.cs deleted file mode 100644 index c3b4bd7396..0000000000 --- a/Content.Shared/Light/Components/LightOnCollideComponent.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Robust.Shared.GameStates; - -namespace Content.Shared.Light.Components; - -/// -/// Enables / disables pointlight whenever entities are contacting with it -/// -[RegisterComponent, NetworkedComponent] -public sealed partial class LightOnCollideComponent : Component -{ -} diff --git a/Content.Shared/Light/EntitySystems/LightCollideSystem.cs b/Content.Shared/Light/EntitySystems/LightCollideSystem.cs deleted file mode 100644 index 2de7c5591f..0000000000 --- a/Content.Shared/Light/EntitySystems/LightCollideSystem.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Content.Shared.Light.Components; -using Robust.Shared.Physics.Events; -using Robust.Shared.Physics.Systems; - -namespace Content.Shared.Light.EntitySystems; - -public sealed class LightCollideSystem : EntitySystem -{ - [Dependency] private readonly SharedPhysicsSystem _physics = default!; - [Dependency] private readonly SlimPoweredLightSystem _lights = default!; - - private EntityQuery _lightQuery; - - public override void Initialize() - { - base.Initialize(); - - _lightQuery = GetEntityQuery(); - - SubscribeLocalEvent(OnPreventCollide); - SubscribeLocalEvent(OnStart); - SubscribeLocalEvent(OnEnd); - - SubscribeLocalEvent(OnCollideShutdown); - } - - private void OnCollideShutdown(Entity ent, ref ComponentShutdown args) - { - // TODO: Check this on the event. - if (TerminatingOrDeleted(ent.Owner)) - return; - - // Regenerate contacts for everything we were colliding with. - var contacts = _physics.GetContacts(ent.Owner); - - while (contacts.MoveNext(out var contact)) - { - if (!contact.IsTouching) - continue; - - var other = contact.OtherEnt(ent.Owner); - - if (_lightQuery.HasComp(other)) - { - _physics.RegenerateContacts(other); - } - } - } - - // You may be wondering what de fok this is doing here. - // At the moment there's no easy way to do collision whitelists based on components. - private void OnPreventCollide(Entity ent, ref PreventCollideEvent args) - { - if (!_lightQuery.HasComp(args.OtherEntity)) - { - args.Cancelled = true; - } - } - - private void OnEnd(Entity ent, ref EndCollideEvent args) - { - if (args.OurFixtureId != ent.Comp.FixtureId) - return; - - if (!_lightQuery.HasComp(args.OtherEntity)) - return; - - // TODO: Engine bug IsTouching box2d yay. - var contacts = _physics.GetTouchingContacts(args.OtherEntity) - 1; - - if (contacts > 0) - return; - - _lights.SetEnabled(args.OtherEntity, false); - } - - private void OnStart(Entity ent, ref StartCollideEvent args) - { - if (args.OurFixtureId != ent.Comp.FixtureId) - return; - - if (!_lightQuery.HasComp(args.OtherEntity)) - return; - - _lights.SetEnabled(args.OtherEntity, true); - } -} diff --git a/Content.Shared/SurveillanceCamera/Components/CameraActiveOnCollideColliderComponent.cs b/Content.Shared/SurveillanceCamera/Components/CameraActiveOnCollideColliderComponent.cs new file mode 100644 index 0000000000..f92f395eb9 --- /dev/null +++ b/Content.Shared/SurveillanceCamera/Components/CameraActiveOnCollideColliderComponent.cs @@ -0,0 +1,16 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.SurveillanceCamera.Components; + +/// +/// Can activate when collided with. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class CameraActiveOnCollideColliderComponent : Component +{ + /// + /// The fixture id used for detecting the collision. + /// + [DataField] + public string FixtureId = "lightTrigger"; +} diff --git a/Content.Shared/SurveillanceCamera/Components/CameraActiveOnCollideComponent.cs b/Content.Shared/SurveillanceCamera/Components/CameraActiveOnCollideComponent.cs new file mode 100644 index 0000000000..27c4bf7c09 --- /dev/null +++ b/Content.Shared/SurveillanceCamera/Components/CameraActiveOnCollideComponent.cs @@ -0,0 +1,22 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.SurveillanceCamera.Components; + +/// +/// Marks an entity with whenever entities are contacting with it. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class CameraActiveOnCollideComponent : Component +{ + /// + /// Whether this camera is currently being collided with. + /// + [DataField, AutoNetworkedField] + public bool Enabled; + + /// + /// Whether the entity must be powered for this component to work. + /// + [DataField, AutoNetworkedField] + public bool RequiresPower = true; +} diff --git a/Content.Shared/SurveillanceCamera/Components/SurveillanceCameraComponent.cs b/Content.Shared/SurveillanceCamera/Components/SurveillanceCameraComponent.cs index 21381e753f..914ba4a63f 100644 --- a/Content.Shared/SurveillanceCamera/Components/SurveillanceCameraComponent.cs +++ b/Content.Shared/SurveillanceCamera/Components/SurveillanceCameraComponent.cs @@ -8,18 +8,23 @@ namespace Content.Shared.SurveillanceCamera.Components; [Access(typeof(SharedSurveillanceCameraSystem))] public sealed partial class SurveillanceCameraComponent : Component { - // List of active viewers. This is for bookkeeping purposes, - // so that when a camera shuts down, any entity viewing it - // will immediately have their subscription revoked. + /// + /// List of active viewers who have a PVS view subscription on this camera. + /// This is for bookkeeping purposes, + /// so that when a camera shuts down, any entity viewing it + /// will immediately have their subscription revoked. + /// [ViewVariables] - public HashSet ActiveViewers { get; } = new(); + public HashSet ActivePvsViewers { get; } = new(); - // Monitors != Viewers, as viewers are entities that are tied - // to a player session that's viewing from this camera - // - // Monitors are grouped sets of viewers, and may be - // completely different monitor types (e.g., monitor console, - // AI, etc.) + /// + /// Monitors != Viewers, as viewers are entities that are tied + /// to a player session that's viewing from this camera + /// + /// Monitors are grouped sets of viewers, and may be + /// completely different monitor types (e.g., monitor console, + /// AI, etc.) + /// [ViewVariables] public HashSet ActiveMonitors { get; } = new(); diff --git a/Content.Shared/SurveillanceCamera/SharedSurveillanceCameraSystem.cs b/Content.Shared/SurveillanceCamera/SharedSurveillanceCameraSystem.cs index b6743669e9..1be49f8fc8 100644 --- a/Content.Shared/SurveillanceCamera/SharedSurveillanceCameraSystem.cs +++ b/Content.Shared/SurveillanceCamera/SharedSurveillanceCameraSystem.cs @@ -5,7 +5,7 @@ using Robust.Shared.Serialization; namespace Content.Shared.SurveillanceCamera; -public abstract class SharedSurveillanceCameraSystem : EntitySystem +public abstract partial class SharedSurveillanceCameraSystem : EntitySystem { public override void Initialize() { @@ -68,3 +68,11 @@ public enum SurveillanceCameraVisuals : byte Xray, Emp } + +/// +/// Raised on a camera entity to find whether it is externally viewed by some entity. +/// This does not use the actual viewers or monitors camera has and is simply used to see whether the camera is "technically" +/// being looked through by somebody, such as the Station AI. +/// +[ByRefEvent] +public record struct SurveillanceCameraGetIsViewedExternallyEvent(bool Viewed = false); diff --git a/Resources/Prototypes/Actions/station_ai.yml b/Resources/Prototypes/Actions/station_ai.yml index 4dbaf07aab..9e9bc9f650 100644 --- a/Resources/Prototypes/Actions/station_ai.yml +++ b/Resources/Prototypes/Actions/station_ai.yml @@ -14,34 +14,6 @@ - type: InstantAction event: !type:JumpToCoreEvent -- type: entity - parent: BaseAction - id: ActionSurvCameraLights - name: Toggle camera lights - description: Enable surveillance camera lights near wherever you're viewing. - components: - - type: Action - priority: -5 - itemIconStyle: BigAction - icon: - sprite: Interface/Actions/actions_ai.rsi - state: camera_light - - type: InstantAction - event: !type:RelayedActionComponentChangeEvent - components: - - type: LightOnCollideCollider - - type: FixturesChange - fixtures: - lightTrigger: - shape: - !type:PhysShapeCircle - radius: 0.35 - density: 80 - hard: false - layer: - - GhostImpassable - - - type: entity parent: BaseMentalAction id: ActionAIViewLaws diff --git a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml index 5e06100098..7a6e51cb48 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml @@ -34,7 +34,6 @@ - type: ActionGrant actions: - ActionJumpToCore - - ActionSurvCameraLights - ActionAIViewLaws - type: UserInterface interfaces: @@ -487,6 +486,16 @@ description: The AI's viewer. categories: [ HideSpawnMenu, DoNotMap ] components: + - type: Fixtures + fixtures: + lightTrigger: + shape: + !type:PhysShapeCircle + radius: 8 + density: 80 + hard: false + layer: + - GhostImpassable - type: NoFTL - type: Tag tags: @@ -506,6 +515,7 @@ - state: ai_camera shader: unshaded map: ["base"] + - type: CameraActiveOnCollideCollider # The holographic representation of the AI that is projected from a holopad. - type: entity diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/WallmountMachines/surveillance_camera.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/WallmountMachines/surveillance_camera.yml index 561991cd26..a12bca758d 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/WallmountMachines/surveillance_camera.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/WallmountMachines/surveillance_camera.yml @@ -11,7 +11,7 @@ light: shape: !type:PhysShapeCircle - radius: 5 + radius: 0.5 hard: false mask: - GhostImpassable @@ -24,7 +24,8 @@ - None layer: - TabletopMachineLayer - - type: LightOnCollide + - type: CameraActiveOnCollide + requiresPower: false # TODO: Change this when AI can no longer see via unpowered cameras - type: PointLight enabled: false radius: 5