]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Predict EMPs (#39802)
authorslarticodefast <161409025+slarticodefast@users.noreply.github.com>
Sat, 4 Oct 2025 11:24:42 +0000 (13:24 +0200)
committerGitHub <noreply@github.com>
Sat, 4 Oct 2025 11:24:42 +0000 (11:24 +0000)
* predicted emps

* fixes

* fix

* review

70 files changed:
Content.Client/Emp/EmpSystem.cs
Content.Client/Power/EntitySystems/BatterySystem.cs [new file with mode: 0644]
Content.Client/Power/EntitySystems/ChargerSystem.cs [new file with mode: 0644]
Content.Client/SurveillanceCamera/SurveillanceCameraSystem.cs [new file with mode: 0644]
Content.Client/VendingMachines/VendingMachineSystem.cs
Content.IntegrationTests/Tests/Power/PowerTest.cs
Content.IntegrationTests/Tests/Power/StationPowerTests.cs
Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs
Content.Server/Clothing/Systems/ChameleonClothingSystem.cs
Content.Server/Construction/Completions/BuildMech.cs
Content.Server/Emp/EmpSystem.cs
Content.Server/Holosign/HolosignSystem.cs
Content.Server/Light/EntitySystems/EmergencyLightSystem.cs
Content.Server/Light/EntitySystems/PoweredLightSystem.cs
Content.Server/Mech/Systems/MechSystem.cs
Content.Server/Medical/SuitSensors/SuitSensorSystem.cs
Content.Server/Ninja/Systems/BatteryDrainerSystem.cs
Content.Server/Ninja/Systems/NinjaSuitSystem.cs
Content.Server/Ninja/Systems/SpaceNinjaSystem.cs
Content.Server/Power/Components/BatteryChargerComponent.cs
Content.Server/Power/Components/BatteryComponent.cs [deleted file]
Content.Server/Power/Components/ChargerComponent.cs [deleted file]
Content.Server/Power/EntitySystems/ApcSystem.cs
Content.Server/Power/EntitySystems/BatteryInterfaceSystem.cs
Content.Server/Power/EntitySystems/BatterySystem.cs
Content.Server/Power/EntitySystems/ChargerSystem.cs
Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs
Content.Server/Power/EntitySystems/RiggableSystem.cs
Content.Server/Power/SMES/SmesSystem.cs
Content.Server/Power/SetBatteryPercentCommand.cs
Content.Server/PowerCell/PowerCellSystem.Draw.cs
Content.Server/PowerCell/PowerCellSystem.cs
Content.Server/PowerSink/PowerSinkSystem.cs
Content.Server/Radio/EntitySystems/HeadsetSystem.cs
Content.Server/SensorMonitoring/BatterySensorComponent.cs
Content.Server/SensorMonitoring/BatterySensorSystem.cs
Content.Server/Silicons/StationAi/StationAiSystem.cs
Content.Server/Stunnable/Systems/StunbatonSystem.cs
Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs
Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.cs
Content.Server/Tesla/EntitySystem/TeslaCoilSystem.cs
Content.Server/VendingMachines/VendingMachineSystem.cs
Content.Server/Weapons/Ranged/Systems/GunSystem.Battery.cs
Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAEEmpInAreaComponent.cs
Content.Server/Xenoarchaeology/Artifact/XAE/XAEChargeBatterySystem.cs
Content.Shared/Anomaly/Effects/Components/ElectricityAnomalyComponent.cs
Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs
Content.Shared/Emp/EmpDisabledComponent.cs
Content.Shared/Emp/SharedEmpSystem.cs
Content.Shared/EntityEffects/Effects/EmpReactionEffect.cs
Content.Shared/Light/EntitySystems/SharedPoweredLightSystem.cs
Content.Shared/Medical/SuitSensors/SharedSuitSensorSystem.cs
Content.Shared/Medical/SuitSensors/SuitSensorComponent.cs
Content.Shared/Ninja/Components/NinjaSuitComponent.cs
Content.Shared/Ninja/Systems/SharedNinjaSuitSystem.cs
Content.Shared/Power/ChargeEvents.cs [new file with mode: 0644]
Content.Shared/Power/Components/ApcPowerReceiverBatteryComponent.cs
Content.Shared/Power/Components/BatteryComponent.cs [new file with mode: 0644]
Content.Shared/Power/Components/ChargerComponent.cs [new file with mode: 0644]
Content.Shared/Power/EntitySystems/SharedBatterySystem.cs [new file with mode: 0644]
Content.Shared/Power/EntitySystems/SharedChargerSystem.cs [new file with mode: 0644]
Content.Shared/PowerCell/SharedPowerCellSystem.cs
Content.Shared/Radio/Components/HeadsetComponent.cs
Content.Shared/Radio/EntitySystems/SharedHeadsetSystem.cs
Content.Shared/SurveillanceCamera/Components/SurveillanceCameraComponent.cs [moved from Content.Server/SurveillanceCamera/Components/SurveillanceCameraComponent.cs with 72% similarity]
Content.Shared/SurveillanceCamera/SharedSurveillanceCameraSystem.cs
Content.Shared/Trigger/Systems/EmpOnTriggerSystem.cs
Content.Shared/VendingMachines/SharedVendingMachineSystem.cs
Content.Shared/VendingMachines/VendingMachineComponent.cs
Resources/Prototypes/Entities/Effects/emp_effects.yml

index 5ed1022750dfde925e42c5d3afeab4f974c18d81..b32e370f1e666a7a5d67dd779f5d3fe541595f40 100644 (file)
@@ -7,6 +7,18 @@ public sealed class EmpSystem : SharedEmpSystem
 {
     [Dependency] private readonly IRobustRandom _random = default!;
 
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<EmpDisabledComponent, ComponentStartup>(OnStartup);
+    }
+
+    private void OnStartup(Entity<EmpDisabledComponent> ent, ref ComponentStartup args)
+    {
+        // EmpPulseEvent.Affected will spawn the first visual effect directly when the emp is used
+        ent.Comp.TargetTime = Timing.CurTime + _random.NextFloat(0.8f, 1.2f) * ent.Comp.EffectCooldown;
+    }
     public override void Update(float frameTime)
     {
         base.Update(frameTime);
@@ -16,7 +28,7 @@ public sealed class EmpSystem : SharedEmpSystem
         {
             if (Timing.CurTime > comp.TargetTime)
             {
-                comp.TargetTime = Timing.CurTime + _random.NextFloat(0.8f, 1.2f) * TimeSpan.FromSeconds(comp.EffectCooldown);
+                comp.TargetTime = Timing.CurTime + _random.NextFloat(0.8f, 1.2f) * comp.EffectCooldown;
                 Spawn(EmpDisabledEffectPrototype, transform.Coordinates);
             }
         }
diff --git a/Content.Client/Power/EntitySystems/BatterySystem.cs b/Content.Client/Power/EntitySystems/BatterySystem.cs
new file mode 100644 (file)
index 0000000..1d0b480
--- /dev/null
@@ -0,0 +1,5 @@
+using Content.Shared.Power.EntitySystems;
+
+namespace Content.Client.Power.EntitySystems;
+
+public sealed class BatterySystem : SharedBatterySystem;
diff --git a/Content.Client/Power/EntitySystems/ChargerSystem.cs b/Content.Client/Power/EntitySystems/ChargerSystem.cs
new file mode 100644 (file)
index 0000000..efadde3
--- /dev/null
@@ -0,0 +1,5 @@
+using Content.Shared.Power.EntitySystems;
+
+namespace Content.Client.Power.EntitySystems;
+
+public sealed class ChargerSystem : SharedChargerSystem;
diff --git a/Content.Client/SurveillanceCamera/SurveillanceCameraSystem.cs b/Content.Client/SurveillanceCamera/SurveillanceCameraSystem.cs
new file mode 100644 (file)
index 0000000..5dbb9f8
--- /dev/null
@@ -0,0 +1,5 @@
+using Content.Shared.SurveillanceCamera;
+
+namespace Content.Client.SurveillanceCamera;
+
+public sealed class SurveillanceCameraSystem : SharedSurveillanceCameraSystem;
index 74919128554a449f588731a1220dfa816fb464d6..f116dd5107e71e9f5dad1b7dedbf686f06a8c2a9 100644 (file)
@@ -33,6 +33,7 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
         component.EjectEnd = state.EjectEnd;
         component.DenyEnd = state.DenyEnd;
         component.DispenseOnHitEnd = state.DispenseOnHitEnd;
+        component.Broken = state.Broken;
 
         // If all we did was update amounts then we can leave BUI buttons in place.
         var fullUiUpdate = !component.Inventory.Keys.SequenceEqual(state.Inventory.Keys) ||
index a448427d050323fbbd3488c1387a3ccbc0d04827..ab8a421c03395b6a9ce32f0c4df6e9de1d38d8fd 100644 (file)
@@ -1,12 +1,11 @@
 #nullable enable
-using Content.Server.NodeContainer;
 using Content.Server.NodeContainer.EntitySystems;
-using Content.Server.NodeContainer.Nodes;
 using Content.Server.Power.Components;
 using Content.Server.Power.EntitySystems;
 using Content.Server.Power.Nodes;
 using Content.Shared.Coordinates;
 using Content.Shared.NodeContainer;
+using Content.Shared.Power.Components;
 using Robust.Shared.GameObjects;
 using Robust.Shared.Map;
 using Robust.Shared.Maths;
index c9f94987500d460b2605fe0b8a461f4588f8181d..3a0af28e48331f725e31a49f131b4dec98f1c32e 100644 (file)
@@ -5,6 +5,7 @@ using Content.Server.Maps;
 using Content.Server.Power.Components;
 using Content.Server.Power.NodeGroups;
 using Content.Server.Power.Pow3r;
+using Content.Shared.Power.Components;
 using Content.Shared.NodeContainer;
 using Robust.Shared.EntitySerialization;
 
index 44795d1fb261f6069f937cf02b4216848b37dc5f..176d4fd04c53a5996fce8764ee47ef8ab70f05ae 100644 (file)
@@ -24,6 +24,7 @@ using Content.Shared.Doors.Components;
 using Content.Shared.Hands.Components;
 using Content.Shared.Inventory;
 using Content.Shared.PDA;
+using Content.Shared.Power.Components;
 using Content.Shared.Stacks;
 using Content.Shared.Station.Components;
 using Content.Shared.Verbs;
index f734d3eb3ecfec651d61a62a29ba63275ec5655e..645b47d1802ad899db7b1eb8ef735c884925404d 100644 (file)
@@ -1,14 +1,10 @@
-using System.Linq;
-using Content.Server.Emp;
 using Content.Shared.Clothing.Components;
 using Content.Shared.Clothing.EntitySystems;
 using Content.Shared.Emp;
 using Content.Shared.IdentityManagement;
 using Content.Shared.IdentityManagement.Components;
-using Content.Shared.Inventory;
 using Content.Shared.Prototypes;
 using Robust.Shared.Prototypes;
-using Robust.Shared.Random;
 
 namespace Content.Server.Clothing.Systems;
 
@@ -16,15 +12,12 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
 {
     [Dependency] private readonly IPrototypeManager _proto = default!;
     [Dependency] private readonly IdentitySystem _identity = default!;
-    [Dependency] private readonly IRobustRandom _random = default!;
 
     public override void Initialize()
     {
         base.Initialize();
         SubscribeLocalEvent<ChameleonClothingComponent, MapInitEvent>(OnMapInit);
         SubscribeLocalEvent<ChameleonClothingComponent, ChameleonPrototypeSelectedMessage>(OnSelected);
-
-        SubscribeLocalEvent<ChameleonClothingComponent, EmpPulseEvent>(OnEmpPulse);
     }
 
     private void OnMapInit(EntityUid uid, ChameleonClothingComponent component, MapInitEvent args)
@@ -37,21 +30,6 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
         SetSelectedPrototype(uid, args.SelectedId, component: component);
     }
 
-    private void OnEmpPulse(EntityUid uid, ChameleonClothingComponent component, ref EmpPulseEvent args)
-    {
-        if (!component.AffectedByEmp)
-            return;
-
-        if (component.EmpContinuous)
-            component.NextEmpChange = _timing.CurTime + TimeSpan.FromSeconds(1f / component.EmpChangeIntensity);
-
-        var pick = GetRandomValidPrototype(component.Slot, component.RequireTag);
-        SetSelectedPrototype(uid, pick, component: component);
-
-        args.Affected = true;
-        args.Disabled = true;
-    }
-
     private void UpdateUi(EntityUid uid, ChameleonClothingComponent? component = null)
     {
         if (!Resolve(uid, ref component))
@@ -64,7 +42,7 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
     /// <summary>
     ///     Change chameleon items name, description and sprite to mimic other entity prototype.
     /// </summary>
-    public void SetSelectedPrototype(EntityUid uid, string? protoId, bool forceUpdate = false,
+    public override void SetSelectedPrototype(EntityUid uid, string? protoId, bool forceUpdate = false,
         ChameleonClothingComponent? component = null)
     {
         if (!Resolve(uid, ref component, false))
@@ -88,14 +66,6 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
         Dirty(uid, component);
     }
 
-    /// <summary>
-    ///     Get a random prototype for a given slot.
-    /// </summary>
-    public string GetRandomValidPrototype(SlotFlags slot, string? tag = null)
-    {
-        return _random.Pick(GetValidTargets(slot, tag).ToList());
-    }
-
     public override void Update(float frameTime)
     {
         base.Update(frameTime);
@@ -106,7 +76,7 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
             if (!chameleon.EmpContinuous)
                 continue;
 
-            if (_timing.CurTime < chameleon.NextEmpChange)
+            if (Timing.CurTime < chameleon.NextEmpChange)
                 continue;
 
             // randomly pick cloth element from available and apply it
index e11c79d851e40b8b30a41676e8cfc313990b8f89..c0b5921db9264e36d9c9f9c3f416a4e694df9ec5 100644 (file)
@@ -1,7 +1,7 @@
 using Content.Server.Mech.Systems;
-using Content.Server.Power.Components;
 using Content.Shared.Construction;
 using Content.Shared.Mech.Components;
+using Content.Shared.Power.Components;
 using JetBrains.Annotations;
 using Robust.Server.Containers;
 using Robust.Shared.Containers;
index 67f9cabd4253c84bd9c2a81aa321e207cf33468f..38ab09e1cfc7242db13d03f13fd6f84ca46f5c70 100644 (file)
@@ -2,16 +2,11 @@ using Content.Server.Power.EntitySystems;
 using Content.Server.Radio;
 using Content.Server.SurveillanceCamera;
 using Content.Shared.Emp;
-using Robust.Shared.Map;
 
 namespace Content.Server.Emp;
 
 public sealed class EmpSystem : SharedEmpSystem
 {
-    [Dependency] private readonly EntityLookupSystem _lookup = default!;
-
-    public const string EmpPulseEffectPrototype = "EffectEmpPulse";
-
     public override void Initialize()
     {
         base.Initialize();
@@ -22,84 +17,6 @@ public sealed class EmpSystem : SharedEmpSystem
         SubscribeLocalEvent<EmpDisabledComponent, SurveillanceCameraSetActiveAttemptEvent>(OnCameraSetActive);
     }
 
-    public override void EmpPulse(MapCoordinates coordinates, float range, float energyConsumption, float duration)
-    {
-        foreach (var uid in _lookup.GetEntitiesInRange(coordinates, range))
-        {
-            TryEmpEffects(uid, energyConsumption, duration);
-        }
-        Spawn(EmpPulseEffectPrototype, coordinates);
-    }
-
-    /// <summary>
-    ///   Triggers an EMP pulse at the given location, by first raising an <see cref="EmpAttemptEvent"/>, then a raising <see cref="EmpPulseEvent"/> on all entities in range.
-    /// </summary>
-    /// <param name="coordinates">The location to trigger the EMP pulse at.</param>
-    /// <param name="range">The range of the EMP pulse.</param>
-    /// <param name="energyConsumption">The amount of energy consumed by the EMP pulse.</param>
-    /// <param name="duration">The duration of the EMP effects.</param>
-    public void EmpPulse(EntityCoordinates coordinates, float range, float energyConsumption, float duration)
-    {
-        foreach (var uid in _lookup.GetEntitiesInRange(coordinates, range))
-        {
-            TryEmpEffects(uid, energyConsumption, duration);
-        }
-        Spawn(EmpPulseEffectPrototype, coordinates);
-    }
-
-    /// <summary>
-    ///    Attempts to apply the effects of an EMP pulse onto an entity by first raising an <see cref="EmpAttemptEvent"/>, followed by raising a <see cref="EmpPulseEvent"/> on it.
-    /// </summary>
-    /// <param name="uid">The entity to apply the EMP effects on.</param>
-    /// <param name="energyConsumption">The amount of energy consumed by the EMP.</param>
-    /// <param name="duration">The duration of the EMP effects.</param>
-    public void TryEmpEffects(EntityUid uid, float energyConsumption, float duration)
-    {
-        var attemptEv = new EmpAttemptEvent();
-        RaiseLocalEvent(uid, attemptEv);
-        if (attemptEv.Cancelled)
-            return;
-
-        DoEmpEffects(uid, energyConsumption, duration);
-    }
-
-    /// <summary>
-    ///    Applies the effects of an EMP pulse onto an entity by raising a <see cref="EmpPulseEvent"/> on it.
-    /// </summary>
-    /// <param name="uid">The entity to apply the EMP effects on.</param>
-    /// <param name="energyConsumption">The amount of energy consumed by the EMP.</param>
-    /// <param name="duration">The duration of the EMP effects.</param>
-    public void DoEmpEffects(EntityUid uid, float energyConsumption, float duration)
-    {
-        var ev = new EmpPulseEvent(energyConsumption, false, false, TimeSpan.FromSeconds(duration));
-        RaiseLocalEvent(uid, ref ev);
-
-        if (ev.Affected)
-            Spawn(EmpDisabledEffectPrototype, Transform(uid).Coordinates);
-
-        if (!ev.Disabled)
-            return;
-
-        var disabled = EnsureComp<EmpDisabledComponent>(uid);
-        disabled.DisabledUntil = Timing.CurTime + TimeSpan.FromSeconds(duration);
-    }
-
-    public override void Update(float frameTime)
-    {
-        base.Update(frameTime);
-
-        var query = EntityQueryEnumerator<EmpDisabledComponent>();
-        while (query.MoveNext(out var uid, out var comp))
-        {
-            if (comp.DisabledUntil < Timing.CurTime)
-            {
-                RemComp<EmpDisabledComponent>(uid);
-                var ev = new EmpDisabledRemoved();
-                RaiseLocalEvent(uid, ref ev);
-            }
-        }
-    }
-
     private void OnRadioSendAttempt(EntityUid uid, EmpDisabledComponent component, ref RadioSendAttemptEvent args)
     {
         args.Cancelled = true;
@@ -120,14 +37,3 @@ public sealed class EmpSystem : SharedEmpSystem
         args.Cancelled = true;
     }
 }
-
-/// <summary>
-/// Raised on an entity before <see cref="EmpPulseEvent"/>. Cancel this to prevent the emp event being raised.
-/// </summary>
-public sealed partial class EmpAttemptEvent : CancellableEntityEventArgs;
-
-[ByRefEvent]
-public record struct EmpPulseEvent(float EnergyConsumption, bool Affected, bool Disabled, TimeSpan Duration);
-
-[ByRefEvent]
-public record struct EmpDisabledRemoved();
index 58ed77ecf83d46d9bb719c45c898c3e23584618a..beb5e909c0be3822f8aacd68b0b20b5e0137198f 100644 (file)
@@ -1,8 +1,8 @@
 using Content.Shared.Examine;
 using Content.Shared.Coordinates.Helpers;
-using Content.Server.Power.Components;
 using Content.Server.PowerCell;
 using Content.Shared.Interaction;
+using Content.Shared.Power.Components;
 using Content.Shared.Storage;
 
 namespace Content.Server.Holosign;
@@ -12,7 +12,6 @@ public sealed class HolosignSystem : EntitySystem
     [Dependency] private readonly PowerCellSystem _powerCell = default!;
     [Dependency] private readonly SharedTransformSystem _transform = default!;
 
-
     public override void Initialize()
     {
         base.Initialize();
index 1b7b6146659a7e0ab078dd917f0f03195f3d8bb3..0aea245c797bd0f9b9bb711be168ef08914ab0c6 100644 (file)
@@ -8,6 +8,7 @@ using Content.Shared.Examine;
 using Content.Shared.Light;
 using Content.Shared.Light.Components;
 using Content.Shared.Power;
+using Content.Shared.Power.Components;
 using Content.Shared.Station.Components;
 using Robust.Server.GameObjects;
 using Color = Robust.Shared.Maths.Color;
index 948c44cd7577c5dd7cae3bb0cc07d259a1ed936f..d1c231e490303330097e8264e6e52514b33904cb 100644 (file)
@@ -1,4 +1,3 @@
-using Content.Server.Emp;
 using Content.Server.Ghost;
 using Content.Shared.Light.Components;
 using Content.Shared.Light.EntitySystems;
@@ -16,8 +15,6 @@ public sealed class PoweredLightSystem : SharedPoweredLightSystem
         SubscribeLocalEvent<PoweredLightComponent, MapInitEvent>(OnMapInit);
 
         SubscribeLocalEvent<PoweredLightComponent, GhostBooEvent>(OnGhostBoo);
-
-        SubscribeLocalEvent<PoweredLightComponent, EmpPulseEvent>(OnEmpPulse);
     }
 
     private void OnGhostBoo(EntityUid uid, PoweredLightComponent light, GhostBooEvent args)
@@ -55,10 +52,4 @@ public sealed class PoweredLightSystem : SharedPoweredLightSystem
         // need this to update visualizers
         UpdateLight(uid, light);
     }
-
-    private void OnEmpPulse(EntityUid uid, PoweredLightComponent component, ref EmpPulseEvent args)
-    {
-        if (TryDestroyBulb(uid, component))
-            args.Affected = true;
-    }
 }
index 7b0c2f64723bb19907b45cd15b8e1e4414608671..917f4f5035ab2eceae60876e27778cf1e2206cbd 100644 (file)
@@ -2,7 +2,6 @@ using System.Linq;
 using Content.Server.Atmos.EntitySystems;
 using Content.Server.Body.Systems;
 using Content.Server.Mech.Components;
-using Content.Server.Power.Components;
 using Content.Server.Power.EntitySystems;
 using Content.Shared.ActionBlocker;
 using Content.Shared.Damage;
@@ -14,6 +13,7 @@ using Content.Shared.Mech.Components;
 using Content.Shared.Mech.EntitySystems;
 using Content.Shared.Movement.Events;
 using Content.Shared.Popups;
+using Content.Shared.Power.Components;
 using Content.Shared.Tools;
 using Content.Shared.Tools.Components;
 using Content.Shared.Tools.Systems;
index 7af093b17834abdedd63e3559c6f58650f5ddd53..72c3f5ecd99f17ac35f7923ef642daf4f0054b1b 100644 (file)
@@ -1,8 +1,6 @@
 using Content.Server.DeviceNetwork.Systems;
-using Content.Server.Emp;
 using Content.Server.Medical.CrewMonitoring;
 using Content.Shared.DeviceNetwork.Components;
-using Content.Shared.Medical.SuitSensor;
 using Content.Shared.Medical.SuitSensors;
 using Robust.Shared.Timing;
 
@@ -14,14 +12,6 @@ public sealed class SuitSensorSystem : SharedSuitSensorSystem
     [Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
     [Dependency] private readonly SingletonDeviceNetServerSystem _singletonServerSystem = default!;
 
-    public override void Initialize()
-    {
-        base.Initialize();
-
-        SubscribeLocalEvent<SuitSensorComponent, EmpPulseEvent>(OnEmpPulse);
-        SubscribeLocalEvent<SuitSensorComponent, EmpDisabledRemoved>(OnEmpFinished);
-    }
-
     public override void Update(float frameTime)
     {
         base.Update(frameTime);
@@ -70,22 +60,4 @@ public sealed class SuitSensorSystem : SharedSuitSensorSystem
             _deviceNetworkSystem.QueuePacket(uid, sensor.ConnectedServer, payload, device: device);
         }
     }
-
-    private void OnEmpPulse(Entity<SuitSensorComponent> ent, ref EmpPulseEvent args)
-    {
-        args.Affected = true;
-        args.Disabled = true;
-
-        ent.Comp.PreviousMode = ent.Comp.Mode;
-        SetSensor(ent.AsNullable(), SuitSensorMode.SensorOff, null);
-
-        ent.Comp.PreviousControlsLocked = ent.Comp.ControlsLocked;
-        ent.Comp.ControlsLocked = true;
-    }
-
-    private void OnEmpFinished(Entity<SuitSensorComponent> ent, ref EmpDisabledRemoved args)
-    {
-        SetSensor(ent.AsNullable(), ent.Comp.PreviousMode, null);
-        ent.Comp.ControlsLocked = ent.Comp.PreviousControlsLocked;
-    }
 }
index 71e38ed3f6ecb6945fa6c495ac531cd4e7d48420..e5633866089f1664947fee2162b5fc0deedd3cdc 100644 (file)
@@ -6,7 +6,7 @@ using Content.Shared.Interaction;
 using Content.Shared.Ninja.Components;
 using Content.Shared.Ninja.Systems;
 using Content.Shared.Popups;
-using Robust.Shared.Audio;
+using Content.Shared.Power.Components;
 using Robust.Shared.Audio.Systems;
 
 namespace Content.Server.Ninja.Systems;
index 62d3d0e3ca489b439118ab3ea8705782b325e406..399d94e8f77dba3e79abd6b4d6f810ccf652e71c 100644 (file)
@@ -1,11 +1,11 @@
-using Content.Server.Emp;
 using Content.Server.Ninja.Events;
 using Content.Server.Power.Components;
 using Content.Server.PowerCell;
+using Content.Shared.Emp;
 using Content.Shared.Hands.EntitySystems;
 using Content.Shared.Ninja.Components;
 using Content.Shared.Ninja.Systems;
-using Content.Shared.Popups;
+using Content.Shared.Power.Components;
 using Content.Shared.PowerCell.Components;
 using Robust.Shared.Containers;
 
@@ -16,7 +16,7 @@ namespace Content.Server.Ninja.Systems;
 /// </summary>
 public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
 {
-    [Dependency] private readonly EmpSystem _emp = default!;
+    [Dependency] private readonly SharedEmpSystem _emp = default!;
     [Dependency] private readonly SharedHandsSystem _hands = default!;
     [Dependency] private readonly SpaceNinjaSystem _ninja = default!;
     [Dependency] private readonly PowerCellSystem _powerCell = default!;
@@ -30,7 +30,6 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
         base.Initialize();
 
         SubscribeLocalEvent<NinjaSuitComponent, ContainerIsInsertingAttemptEvent>(OnSuitInsertAttempt);
-        SubscribeLocalEvent<NinjaSuitComponent, EmpAttemptEvent>(OnEmpAttempt);
         SubscribeLocalEvent<NinjaSuitComponent, RecallKatanaEvent>(OnRecallKatana);
         SubscribeLocalEvent<NinjaSuitComponent, NinjaEmpEvent>(OnEmp);
     }
@@ -44,7 +43,7 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
         // raise event to let ninja components get starting battery
         _ninja.GetNinjaBattery(user.Owner, out var uid, out var _);
 
-        if (uid is not {} battery_uid)
+        if (uid is not { } battery_uid)
             return;
 
         var ev = new NinjaBatteryChangedEvent(battery_uid, ent.Owner);
@@ -96,17 +95,10 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
         // if a cell is able to automatically recharge, boost the score drastically depending on the recharge rate,
         // this is to ensure a ninja can still upgrade to a micro reactor cell even if they already have a medium or high.
         if (TryComp<BatterySelfRechargerComponent>(uid, out var selfcomp) && selfcomp.AutoRecharge)
-            return battcomp.MaxCharge + (selfcomp.AutoRechargeRate*AutoRechargeValue);
+            return battcomp.MaxCharge + selfcomp.AutoRechargeRate * AutoRechargeValue;
         return battcomp.MaxCharge;
     }
 
-    private void OnEmpAttempt(EntityUid uid, NinjaSuitComponent comp, EmpAttemptEvent args)
-    {
-        // ninja suit (battery) is immune to emp
-        // powercell relays the event to suit
-        args.Cancel();
-    }
-
     protected override void UserUnequippedSuit(Entity<NinjaSuitComponent> ent, Entity<SpaceNinjaComponent> user)
     {
         base.UserUnequippedSuit(ent, user);
@@ -144,6 +136,7 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
         Popup.PopupEntity(Loc.GetString(message), user, user);
     }
 
+    // TODO: Move this to shared when power cells are predicted.
     private void OnEmp(Entity<NinjaSuitComponent> ent, ref NinjaEmpEvent args)
     {
         var (uid, comp) = ent;
@@ -159,7 +152,6 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
         if (CheckDisabled(ent, user))
             return;
 
-        var coords = _transform.GetMapCoordinates(user);
-        _emp.EmpPulse(coords, comp.EmpRange, comp.EmpConsumption, comp.EmpDuration);
+        _emp.EmpPulse(Transform(user).Coordinates, comp.EmpRange, comp.EmpConsumption, comp.EmpDuration, user);
     }
 }
index 1ece045774c63cc3c50ef981e11604bb49d9530f..ff88926723ec6b5ca70faccabcf4377140b5d647 100644 (file)
@@ -1,26 +1,20 @@
 using Content.Server.Communications;
-using Content.Server.Chat.Managers;
 using Content.Server.CriminalRecords.Systems;
-using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Objectives.Components;
 using Content.Server.Objectives.Systems;
-using Content.Server.Power.Components;
 using Content.Server.Power.EntitySystems;
 using Content.Server.PowerCell;
 using Content.Server.Research.Systems;
-using Content.Server.Roles;
 using Content.Shared.Alert;
 using Content.Shared.Doors.Components;
 using Content.Shared.IdentityManagement;
 using Content.Shared.Mind;
 using Content.Shared.Ninja.Components;
 using Content.Shared.Ninja.Systems;
+using Content.Shared.Power.Components;
 using Content.Shared.Popups;
 using Content.Shared.Rounding;
-using Robust.Shared.Audio;
-using Robust.Shared.Player;
 using System.Diagnostics.CodeAnalysis;
-using Robust.Shared.Audio.Systems;
 
 namespace Content.Server.Ninja.Systems;
 
index 99284e44acfd40bb630bfba102f75a490f1f838d..7f3dd39524ccb987500bb1b3f863c6dac2405f6e 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Server.Power.NodeGroups;
+using Content.Shared.Power.Components;
 
 namespace Content.Server.Power.Components
 {
diff --git a/Content.Server/Power/Components/BatteryComponent.cs b/Content.Server/Power/Components/BatteryComponent.cs
deleted file mode 100644 (file)
index 96571bc..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-using Content.Server.Power.EntitySystems;
-using Content.Shared.Guidebook;
-
-namespace Content.Server.Power.Components
-{
-    /// <summary>
-    ///     Battery node on the pow3r network. Needs other components to connect to actual networks.
-    /// </summary>
-    [RegisterComponent]
-    [Virtual]
-    [Access(typeof(BatterySystem))]
-    public partial class BatteryComponent : Component
-    {
-        public string SolutionName = "battery";
-
-        /// <summary>
-        /// Maximum charge of the battery in joules (ie. watt seconds)
-        /// </summary>
-        [DataField]
-        [GuidebookData]
-        public float MaxCharge;
-
-        /// <summary>
-        /// Current charge of the battery in joules (ie. watt seconds)
-        /// </summary>
-        [DataField("startingCharge")]
-        public float CurrentCharge;
-
-        /// <summary>
-        /// The price per one joule. Default is 1 credit for 10kJ.
-        /// </summary>
-        [DataField]
-        public float PricePerJoule = 0.0001f;
-    }
-
-    /// <summary>
-    ///     Raised when a battery's charge or capacity changes (capacity affects relative charge percentage).
-    /// </summary>
-    [ByRefEvent]
-    public readonly record struct ChargeChangedEvent(float Charge, float MaxCharge);
-
-    /// <summary>
-    ///     Raised when it is necessary to get information about battery charges.
-    /// </summary>
-    [ByRefEvent]
-    public sealed class GetChargeEvent : EntityEventArgs
-    {
-        public float CurrentCharge;
-        public float MaxCharge;
-    }
-
-    /// <summary>
-    ///     Raised when it is necessary to change the current battery charge to a some value.
-    /// </summary>
-    [ByRefEvent]
-    public sealed class ChangeChargeEvent : EntityEventArgs
-    {
-        public float OriginalValue;
-        public float ResidualValue;
-
-        public ChangeChargeEvent(float value)
-        {
-            OriginalValue = value;
-            ResidualValue = value;
-        }
-    }
-}
diff --git a/Content.Server/Power/Components/ChargerComponent.cs b/Content.Server/Power/Components/ChargerComponent.cs
deleted file mode 100644 (file)
index 4a3c83a..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-using Content.Shared.Power;
-using Content.Shared.Whitelist;
-
-namespace Content.Server.Power.Components
-{
-    [RegisterComponent]
-    public sealed partial class ChargerComponent : Component
-    {
-        [ViewVariables]
-        public CellChargerStatus Status;
-
-        /// <summary>
-        /// The charge rate of the charger, in watts
-        /// </summary>
-        [DataField("chargeRate")]
-        public float ChargeRate = 20.0f;
-
-        /// <summary>
-        /// The container ID that is holds the entities being charged.
-        /// </summary>
-        [DataField("slotId", required: true)]
-        public string SlotId = string.Empty;
-
-        /// <summary>
-        /// A whitelist for what entities can be charged by this Charger.
-        /// </summary>
-        [DataField("whitelist")]
-        public EntityWhitelist? Whitelist;
-
-        /// <summary>
-        /// Indicates whether the charger is portable and thus subject to EMP effects
-        /// and bypasses checks for transform, anchored, and ApcPowerReceiverComponent.
-        /// </summary>
-        [DataField]
-        public bool Portable = false;
-    }
-}
index 29c1431179da2309cca24892c2f9f53ecddddb7e..ed7ec0e225a33b09dd099e453fe66bc59d0e745e 100644 (file)
@@ -1,11 +1,12 @@
-using Content.Server.Emp;
 using Content.Server.Popups;
 using Content.Server.Power.Components;
 using Content.Server.Power.Pow3r;
 using Content.Shared.Access.Systems;
 using Content.Shared.APC;
 using Content.Shared.Emag.Systems;
+using Content.Shared.Emp;
 using Content.Shared.Popups;
+using Content.Shared.Power;
 using Content.Shared.Rounding;
 using Robust.Server.GameObjects;
 using Robust.Shared.Audio;
@@ -203,6 +204,9 @@ public sealed class ApcSystem : EntitySystem
         return ApcExternalPowerState.Good;
     }
 
+    // TODO: This subscription should be in shared.
+    // But I am not moving ApcComponent to shared, this PR already got soaped enough and that component uses several layers of OOP.
+    // At least the EMP visuals won't mispredict, since all APCs also have the BatteryComponent, which also has a EMP effect and is in shared.
     private void OnEmpPulse(EntityUid uid, ApcComponent component, ref EmpPulseEvent args)
     {
         if (component.MainBreakerEnabled)
index 33e3f8ff2c0b617216dd4c58183009014b69e942..83ff28646dbaba51a648247b021ec374c698c24f 100644 (file)
@@ -2,6 +2,7 @@
 using Content.Server.Power.Components;
 using Content.Shared.Database;
 using Content.Shared.Power;
+using Content.Shared.Power.Components;
 using Robust.Server.GameObjects;
 
 namespace Content.Server.Power.EntitySystems;
index f4e48f4a3dceb742fca1239dbd23acfd0af9f235..28b14f69258e061d70f2b88cc914aa86dc5d9b91 100644 (file)
@@ -1,7 +1,9 @@
-using Content.Server.Emp;
 using Content.Server.Power.Components;
 using Content.Shared.Cargo;
 using Content.Shared.Examine;
+using Content.Shared.Power;
+using Content.Shared.Power.Components;
+using Content.Shared.Power.EntitySystems;
 using Content.Shared.Rejuvenate;
 using JetBrains.Annotations;
 using Robust.Shared.Utility;
@@ -10,7 +12,7 @@ using Robust.Shared.Timing;
 namespace Content.Server.Power.EntitySystems
 {
     [UsedImplicitly]
-    public sealed class BatterySystem : EntitySystem
+    public sealed class BatterySystem : SharedBatterySystem
     {
         [Dependency] private readonly IGameTiming _timing = default!;
 
@@ -22,7 +24,6 @@ namespace Content.Server.Power.EntitySystems
             SubscribeLocalEvent<PowerNetworkBatteryComponent, RejuvenateEvent>(OnNetBatteryRejuvenate);
             SubscribeLocalEvent<BatteryComponent, RejuvenateEvent>(OnBatteryRejuvenate);
             SubscribeLocalEvent<BatteryComponent, PriceCalculationEvent>(CalculateBatteryPrice);
-            SubscribeLocalEvent<BatteryComponent, EmpPulseEvent>(OnEmpPulse);
             SubscribeLocalEvent<BatteryComponent, ChangeChargeEvent>(OnChangeCharge);
             SubscribeLocalEvent<BatteryComponent, GetChargeEvent>(OnGetCharge);
 
@@ -50,7 +51,7 @@ namespace Content.Server.Power.EntitySystems
                 if (effectiveMax == 0)
                     effectiveMax = 1;
                 var chargeFraction = batteryComponent.CurrentCharge / effectiveMax;
-                var chargePercentRounded = (int) (chargeFraction * 100);
+                var chargePercentRounded = (int)(chargeFraction * 100);
                 args.PushMarkup(
                     Loc.GetString(
                         "examinable-battery-component-examine-detail",
@@ -108,15 +109,6 @@ namespace Content.Server.Power.EntitySystems
         {
             args.Price += component.CurrentCharge * component.PricePerJoule;
         }
-
-        private void OnEmpPulse(EntityUid uid, BatteryComponent component, ref EmpPulseEvent args)
-        {
-            args.Affected = true;
-            UseCharge(uid, args.EnergyConsumption, component);
-            // Apply a cooldown to the entity's self recharge if needed to avoid it immediately self recharging after an EMP.
-            TrySetChargeCooldown(uid);
-        }
-
         private void OnChangeCharge(Entity<BatteryComponent> entity, ref ChangeChargeEvent args)
         {
             if (args.ResidualValue == 0)
@@ -131,7 +123,7 @@ namespace Content.Server.Power.EntitySystems
             args.MaxCharge += entity.Comp.MaxCharge;
         }
 
-        public float UseCharge(EntityUid uid, float value, BatteryComponent? battery = null)
+        public override float UseCharge(EntityUid uid, float value, BatteryComponent? battery = null)
         {
             if (value <= 0 || !Resolve(uid, ref battery) || battery.CurrentCharge == 0)
                 return 0;
@@ -139,7 +131,7 @@ namespace Content.Server.Power.EntitySystems
             return ChangeCharge(uid, -value, battery);
         }
 
-        public void SetMaxCharge(EntityUid uid, float value, BatteryComponent? battery = null)
+        public override void SetMaxCharge(EntityUid uid, float value, BatteryComponent? battery = null)
         {
             if (!Resolve(uid, ref battery))
                 return;
@@ -174,7 +166,7 @@ namespace Content.Server.Power.EntitySystems
         /// <summary>
         /// Changes the current battery charge by some value
         /// </summary>
-        public float ChangeCharge(EntityUid uid, float value, BatteryComponent? battery = null)
+        public override float ChangeCharge(EntityUid uid, float value, BatteryComponent? battery = null)
         {
             if (!Resolve(uid, ref battery))
                 return 0;
@@ -190,10 +182,7 @@ namespace Content.Server.Power.EntitySystems
             return delta;
         }
 
-        /// <summary>
-        /// Checks if the entity has a self recharge and puts it on cooldown if applicable.
-        /// </summary>
-        public void TrySetChargeCooldown(EntityUid uid, float value = -1)
+        public override void TrySetChargeCooldown(EntityUid uid, float value = -1)
         {
             if (!TryComp<BatterySelfRechargerComponent>(uid, out var batteryself))
                 return;
@@ -228,7 +217,7 @@ namespace Content.Server.Power.EntitySystems
         /// <summary>
         ///     If sufficient charge is available on the battery, use it. Otherwise, don't.
         /// </summary>
-        public bool TryUseCharge(EntityUid uid, float value, BatteryComponent? battery = null)
+        public override bool TryUseCharge(EntityUid uid, float value, BatteryComponent? battery = null)
         {
             if (!Resolve(uid, ref battery, false) || value > battery.CurrentCharge)
                 return false;
index c128c846fbaba52f478f0b1ee5b2fbc188fbfb99..e8dc9e99629e7cf40c14b53c978cc48fb182beeb 100644 (file)
@@ -1,8 +1,9 @@
 using Content.Server.Power.Components;
-using Content.Server.Emp;
-using Content.Server.PowerCell;
 using Content.Shared.Examine;
+using Content.Server.PowerCell;
 using Content.Shared.Power;
+using Content.Shared.Power.Components;
+using Content.Shared.Power.EntitySystems;
 using Content.Shared.PowerCell.Components;
 using Content.Shared.Emp;
 using JetBrains.Annotations;
@@ -15,7 +16,7 @@ using Content.Shared.Whitelist;
 namespace Content.Server.Power.EntitySystems;
 
 [UsedImplicitly]
-internal sealed class ChargerSystem : EntitySystem
+public sealed class ChargerSystem : SharedChargerSystem
 {
     [Dependency] private readonly ContainerSystem _container = default!;
     [Dependency] private readonly PowerCellSystem _powerCell = default!;
@@ -25,6 +26,8 @@ internal sealed class ChargerSystem : EntitySystem
 
     public override void Initialize()
     {
+        base.Initialize();
+
         SubscribeLocalEvent<ChargerComponent, ComponentStartup>(OnStartup);
         SubscribeLocalEvent<ChargerComponent, PowerChangedEvent>(OnPowerChanged);
         SubscribeLocalEvent<ChargerComponent, EntInsertedIntoContainerMessage>(OnInserted);
@@ -32,8 +35,6 @@ internal sealed class ChargerSystem : EntitySystem
         SubscribeLocalEvent<ChargerComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
         SubscribeLocalEvent<ChargerComponent, InsertIntoEntityStorageAttemptEvent>(OnEntityStorageInsertAttempt);
         SubscribeLocalEvent<ChargerComponent, ExaminedEvent>(OnChargerExamine);
-
-        SubscribeLocalEvent<ChargerComponent, EmpPulseEvent>(OnEmpPulse);
     }
 
     private void OnStartup(EntityUid uid, ChargerComponent component, ComponentStartup args)
@@ -46,7 +47,7 @@ internal sealed class ChargerSystem : EntitySystem
         using (args.PushGroup(nameof(ChargerComponent)))
         {
             // rate at which the charger charges
-            args.PushMarkup(Loc.GetString("charger-examine", ("color", "yellow"), ("chargeRate", (int) component.ChargeRate)));
+            args.PushMarkup(Loc.GetString("charger-examine", ("color", "yellow"), ("chargeRate", (int)component.ChargeRate)));
 
             // try to get contents of the charger
             if (!_container.TryGetContainer(uid, component.SlotId, out var container))
@@ -70,7 +71,7 @@ internal sealed class ChargerSystem : EntitySystem
                         continue;
 
                     var chargePercentage = (battery.CurrentCharge / battery.MaxCharge) * 100;
-                    args.PushMarkup(Loc.GetString("charger-content", ("chargePercentage", (int) chargePercentage)));
+                    args.PushMarkup(Loc.GetString("charger-content", ("chargePercentage", (int)chargePercentage)));
                 }
             }
         }
@@ -194,12 +195,6 @@ internal sealed class ChargerSystem : EntitySystem
         }
     }
 
-    private void OnEmpPulse(EntityUid uid, ChargerComponent component, ref EmpPulseEvent args)
-    {
-        args.Affected = true;
-        args.Disabled = true;
-    }
-
     private CellChargerStatus GetStatus(EntityUid uid, ChargerComponent component)
     {
         if (!component.Portable)
index be79d64fc9ace0b2a355d5860d0f10d2bb365f56..25757360b386fb2958be7dad46d916d5939a6ba2 100644 (file)
@@ -1,6 +1,4 @@
-using Content.Server.NodeContainer;
 using Content.Server.NodeContainer.EntitySystems;
-using Content.Server.NodeContainer.Nodes;
 using Content.Server.Power.Components;
 using Content.Server.Power.Nodes;
 using Content.Server.Power.NodeGroups;
@@ -9,6 +7,7 @@ using Content.Shared.GameTicking.Components;
 using Content.Shared.Pinpointer;
 using Content.Shared.Station.Components;
 using Content.Shared.Power;
+using Content.Shared.Power.Components;
 using JetBrains.Annotations;
 using Robust.Server.GameObjects;
 using Robust.Shared.Map.Components;
index 26eaca80fe7734b64bfcab4f6bd343e996972208..0f8b32865b3a98e6d1cb0a0a0d1b569a68fc2e05 100644 (file)
@@ -4,6 +4,7 @@ using Content.Server.Kitchen.Components;
 using Content.Server.Power.Components;
 using Content.Shared.Chemistry.EntitySystems;
 using Content.Shared.Database;
+using Content.Shared.Power.Components;
 using Content.Shared.Rejuvenate;
 
 namespace Content.Server.Power.EntitySystems;
index ebd56ee3e1b5308ecc0ad0cd3d154b2557f243ab..15c40b3c92351ee841225659f970b3adaae6c376 100644 (file)
@@ -1,6 +1,7 @@
 using Content.Server.Power.Components;
 using Content.Server.Power.EntitySystems;
 using Content.Shared.Power;
+using Content.Shared.Power.Components;
 using Content.Shared.Rounding;
 using Content.Shared.SMES;
 using JetBrains.Annotations;
index 7ceee20172c7fbb45fd47cac55176f48297cc27b..03d6d30f8a43cebc5298aba7f28ffdbadc6dff92 100644 (file)
@@ -1,7 +1,7 @@
 using Content.Server.Administration;
-using Content.Server.Power.Components;
 using Content.Server.Power.EntitySystems;
 using Content.Shared.Administration;
+using Content.Shared.Power.Components;
 using Robust.Shared.Console;
 
 namespace Content.Server.Power
index 9156d30b1fc74b62ad4063d2bb5fc1fd71fbac9b..40e54ba13a93e40940b8ad004bcbd19a71721b96 100644 (file)
@@ -1,4 +1,4 @@
-using Content.Server.Power.Components;
+using Content.Shared.Power;
 using Content.Shared.PowerCell;
 using Content.Shared.PowerCell.Components;
 
index 01767d6c41e8f1ef5b58abccb42279392fa95683..6c00cdd30013a5578d6a930d32bb425183ec2062 100644 (file)
@@ -1,17 +1,17 @@
-using Content.Server.Emp;
+using System.Diagnostics.CodeAnalysis;
+using Content.Server.Kitchen.Components;
 using Content.Server.Power.Components;
+using Content.Server.Power.EntitySystems;
+using Content.Shared.Containers.ItemSlots;
 using Content.Shared.Examine;
+using Content.Shared.Popups;
+using Content.Shared.Power;
+using Content.Shared.Power.Components;
 using Content.Shared.PowerCell;
 using Content.Shared.PowerCell.Components;
 using Content.Shared.Rounding;
+using Content.Shared.UserInterface;
 using Robust.Shared.Containers;
-using System.Diagnostics.CodeAnalysis;
-using Content.Server.Kitchen.Components;
-using Content.Server.Power.EntitySystems;
-using Content.Server.UserInterface;
-using Content.Shared.Containers.ItemSlots;
-using Content.Shared.Popups;
-using ActivatableUISystem = Content.Shared.UserInterface.ActivatableUISystem;
 
 namespace Content.Server.PowerCell;
 
@@ -34,7 +34,6 @@ public sealed partial class PowerCellSystem : SharedPowerCellSystem
 
         SubscribeLocalEvent<PowerCellComponent, ChargeChangedEvent>(OnChargeChanged);
         SubscribeLocalEvent<PowerCellComponent, ExaminedEvent>(OnCellExamined);
-        SubscribeLocalEvent<PowerCellComponent, EmpAttemptEvent>(OnCellEmpAttempt);
 
         SubscribeLocalEvent<PowerCellDrawComponent, ChargeChangedEvent>(OnDrawChargeChanged);
         SubscribeLocalEvent<PowerCellDrawComponent, PowerCellChangedEvent>(OnDrawCellChanged);
@@ -221,14 +220,6 @@ public sealed partial class PowerCellSystem : SharedPowerCellSystem
         OnBatteryExamined(uid, battery, args);
     }
 
-    private void OnCellEmpAttempt(EntityUid uid, PowerCellComponent component, EmpAttemptEvent args)
-    {
-        var parent = Transform(uid).ParentUid;
-        // relay the attempt event to the slot so it can cancel it
-        if (HasComp<PowerCellSlotComponent>(parent))
-            RaiseLocalEvent(parent, args);
-    }
-
     private void OnCellSlotExamined(EntityUid uid, PowerCellSlotComponent component, ExaminedEvent args)
     {
         TryGetBatteryFromSlot(uid, out var battery);
index ef08240c5cacb5603ca49c54a1086ef2f46728b2..2bf9046cc3126206c906842027104629ca3a1d16 100644 (file)
@@ -1,13 +1,13 @@
-using Content.Server.Explosion.EntitySystems;
+using Content.Server.Chat.Systems;
+using Content.Server.Explosion.EntitySystems;
 using Content.Server.Power.Components;
-using Content.Shared.Examine;
-using Robust.Shared.Utility;
-using Content.Server.Chat.Systems;
+using Content.Server.Power.EntitySystems;
 using Content.Server.Station.Systems;
-using Robust.Shared.Timing;
-using Robust.Shared.Audio;
+using Content.Shared.Examine;
+using Content.Shared.Power.Components;
 using Robust.Shared.Audio.Systems;
-using Content.Server.Power.EntitySystems;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
 
 namespace Content.Server.PowerSink
 {
index 91abc9efd5272f41afbf2bf8f4dc1bb7d9e6926e..4d29e2f413dda43b3cf839fbdb5100373bb9c7e5 100644 (file)
@@ -1,5 +1,4 @@
 using Content.Server.Chat.Systems;
-using Content.Server.Emp;
 using Content.Shared.Inventory.Events;
 using Content.Shared.Radio;
 using Content.Shared.Radio.Components;
@@ -21,8 +20,6 @@ public sealed class HeadsetSystem : SharedHeadsetSystem
         SubscribeLocalEvent<HeadsetComponent, EncryptionChannelsChangedEvent>(OnKeysChanged);
 
         SubscribeLocalEvent<WearingHeadsetComponent, EntitySpokeEvent>(OnSpeak);
-
-        SubscribeLocalEvent<HeadsetComponent, EmpPulseEvent>(OnEmpPulse);
     }
 
     private void OnKeysChanged(EntityUid uid, HeadsetComponent component, EncryptionChannelsChangedEvent args)
@@ -69,7 +66,6 @@ public sealed class HeadsetSystem : SharedHeadsetSystem
     protected override void OnGotUnequipped(EntityUid uid, HeadsetComponent component, GotUnequippedEvent args)
     {
         base.OnGotUnequipped(uid, component, args);
-        component.IsEquipped = false;
         RemComp<ActiveRadioComponent>(uid);
         RemComp<WearingHeadsetComponent>(args.Equipee);
     }
@@ -82,6 +78,9 @@ public sealed class HeadsetSystem : SharedHeadsetSystem
         if (component.Enabled == value)
             return;
 
+        component.Enabled = value;
+        Dirty(uid, component);
+
         if (!value)
         {
             RemCompDeferred<ActiveRadioComponent>(uid);
@@ -113,13 +112,4 @@ public sealed class HeadsetSystem : SharedHeadsetSystem
         if (TryComp(parent, out ActorComponent? actor))
             _netMan.ServerSendMessage(args.ChatMsg, actor.PlayerSession.Channel);
     }
-
-    private void OnEmpPulse(EntityUid uid, HeadsetComponent component, ref EmpPulseEvent args)
-    {
-        if (component.Enabled)
-        {
-            args.Affected = true;
-            args.Disabled = true;
-        }
-    }
 }
index 2f0c97e6ec67721c6bc2066ccbc1f761c4d8dc37..a5a48ddc91124579983416b9c7362d0b32b1d881 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Server.Power.Components;
+using Content.Shared.Power.Components;
 
 namespace Content.Server.SensorMonitoring;
 
index 5047cd1d297f0c6283dd53aba2e63331626df51d..bd94868c5fc6f83c70d769809bd1d0d167515612 100644 (file)
@@ -1,8 +1,8 @@
-using Content.Server.DeviceNetwork;
-using Content.Server.DeviceNetwork.Systems;
+using Content.Server.DeviceNetwork.Systems;
 using Content.Server.Power.Components;
 using Content.Shared.DeviceNetwork;
 using Content.Shared.DeviceNetwork.Events;
+using Content.Shared.Power.Components;
 
 namespace Content.Server.SensorMonitoring;
 
index 73c5670c1ead8bc948612408537ee97b2b134615..4ee2a07d72b6dc27dcab3ab843008fa6f0f46a24 100644 (file)
@@ -19,6 +19,7 @@ using Content.Shared.DoAfter;
 using Content.Shared.Mobs;
 using Content.Shared.Mobs.Systems;
 using Content.Shared.Popups;
+using Content.Shared.Power;
 using Content.Shared.Power.Components;
 using Content.Shared.Rejuvenate;
 using Content.Shared.Roles;
index f043c452ca4417bc9f1d27981a8f4e13f8b3fbae..7f8aa37036d9074ef9930d366b2b7860e99a318d 100644 (file)
@@ -7,6 +7,8 @@ using Content.Shared.Examine;
 using Content.Shared.Item.ItemToggle;
 using Content.Shared.Item.ItemToggle.Components;
 using Content.Shared.Popups;
+using Content.Shared.Power;
+using Content.Shared.Power.Components;
 using Content.Shared.Stunnable;
 
 namespace Content.Server.Stunnable.Systems
index 4029488159a8566ddc0cf58ab95e23c38926d16e..1f298f32a44372cdcde4ff653e7afbaa09a3d73d 100644 (file)
@@ -1,6 +1,7 @@
 using Content.Server.Chat.Systems;
 using Content.Shared.Speech;
 using Content.Shared.Speech.Components;
+using Content.Shared.SurveillanceCamera.Components;
 using Content.Shared.Whitelist;
 using Robust.Shared.Player;
 using static Content.Server.Chat.Systems.ChatSystem;
index 7f7dbc6c97c689e21f6557a7f8b51820ea8c0c04..7e275c78cee49b3de3d7121a61ce505c99b26f7a 100644 (file)
@@ -1,13 +1,12 @@
 using Content.Server.Administration.Logs;
 using Content.Server.DeviceNetwork.Systems;
-using Content.Server.Emp;
 using Content.Shared.ActionBlocker;
 using Content.Shared.Database;
 using Content.Shared.DeviceNetwork;
 using Content.Shared.DeviceNetwork.Events;
 using Content.Shared.Power;
 using Content.Shared.SurveillanceCamera;
-using Content.Shared.Verbs;
+using Content.Shared.SurveillanceCamera.Components;
 using Robust.Server.GameObjects;
 using Robust.Shared.Player;
 using Robust.Shared.Prototypes;
@@ -15,7 +14,7 @@ using Content.Shared.DeviceNetwork.Components;
 
 namespace Content.Server.SurveillanceCamera;
 
-public sealed class SurveillanceCameraSystem : EntitySystem
+public sealed class SurveillanceCameraSystem : SharedSurveillanceCameraSystem
 {
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
     [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
@@ -57,15 +56,13 @@ public sealed class SurveillanceCameraSystem : EntitySystem
 
     public override void Initialize()
     {
+        base.Initialize();
+
         SubscribeLocalEvent<SurveillanceCameraComponent, ComponentShutdown>(OnShutdown);
         SubscribeLocalEvent<SurveillanceCameraComponent, PowerChangedEvent>(OnPowerChanged);
         SubscribeLocalEvent<SurveillanceCameraComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
         SubscribeLocalEvent<SurveillanceCameraComponent, SurveillanceCameraSetupSetName>(OnSetName);
         SubscribeLocalEvent<SurveillanceCameraComponent, SurveillanceCameraSetupSetNetwork>(OnSetNetwork);
-        SubscribeLocalEvent<SurveillanceCameraComponent, GetVerbsEvent<AlternativeVerb>>(AddVerbs);
-
-        SubscribeLocalEvent<SurveillanceCameraComponent, EmpPulseEvent>(OnEmpPulse);
-        SubscribeLocalEvent<SurveillanceCameraComponent, EmpDisabledRemoved>(OnEmpDisabledRemoved);
     }
 
     private void OnPacketReceived(EntityUid uid, SurveillanceCameraComponent component, DeviceNetworkPacketEvent args)
@@ -131,26 +128,6 @@ public sealed class SurveillanceCameraSystem : EntitySystem
         }
     }
 
-    private void AddVerbs(EntityUid uid, SurveillanceCameraComponent component, GetVerbsEvent<AlternativeVerb> verbs)
-    {
-        if (!_actionBlocker.CanInteract(verbs.User, uid) || !_actionBlocker.CanComplexInteract(verbs.User))
-        {
-            return;
-        }
-
-        if (component.NameSet && component.NetworkSet)
-        {
-            return;
-        }
-
-        AlternativeVerb verb = new();
-        verb.Text = Loc.GetString("surveillance-camera-setup");
-        verb.Act = () => OpenSetupInterface(uid, verbs.User, component);
-        verbs.Verbs.Add(verb);
-    }
-
-
-
     private void OnPowerChanged(EntityUid camera, SurveillanceCameraComponent component, ref PowerChangedEvent args)
     {
         SetActive(camera, args.Powered, component);
@@ -173,6 +150,7 @@ public sealed class SurveillanceCameraSystem : EntitySystem
 
         component.CameraId = args.Name;
         component.NameSet = true;
+        Dirty(uid, component);
         UpdateSetupInterface(uid, component);
         _adminLogger.Add(LogType.Chat, LogImpact.Low, $"{ToPrettyString(args.Actor)} set the name of {ToPrettyString(uid)} to \"{args.Name}.\"");
     }
@@ -198,10 +176,11 @@ public sealed class SurveillanceCameraSystem : EntitySystem
 
         _deviceNetworkSystem.SetReceiveFrequency(uid, frequency.Frequency);
         component.NetworkSet = true;
+        Dirty(uid, component);
         UpdateSetupInterface(uid, component);
     }
 
-    private void OpenSetupInterface(EntityUid uid, EntityUid player, SurveillanceCameraComponent? camera = null)
+    protected override void OpenSetupInterface(EntityUid uid, EntityUid player, SurveillanceCameraComponent? camera = null)
     {
         if (!Resolve(uid, ref camera))
             return;
@@ -271,7 +250,7 @@ public sealed class SurveillanceCameraSystem : EntitySystem
         UpdateVisuals(camera, component);
     }
 
-    public void SetActive(EntityUid camera, bool setting, SurveillanceCameraComponent? component = null)
+    public override void SetActive(EntityUid camera, bool setting, SurveillanceCameraComponent? component = null)
     {
         if (!Resolve(camera, ref component))
         {
@@ -418,21 +397,6 @@ public sealed class SurveillanceCameraSystem : EntitySystem
 
         _appearance.SetData(uid, SurveillanceCameraVisualsKey.Key, key, appearance);
     }
-
-    private void OnEmpPulse(EntityUid uid, SurveillanceCameraComponent component, ref EmpPulseEvent args)
-    {
-        if (component.Active)
-        {
-            args.Affected = true;
-            args.Disabled = true;
-            SetActive(uid, false);
-        }
-    }
-
-    private void OnEmpDisabledRemoved(EntityUid uid, SurveillanceCameraComponent component, ref EmpDisabledRemoved args)
-    {
-        SetActive(uid, true);
-    }
 }
 
 public sealed class OnSurveillanceCameraViewerAddEvent : EntityEventArgs
index 4fd2f9b6ed041cc8cc9a42152c9a1cbf5de01bb9..f3cae90b40d1d9e7737b86d4dbd6aa156c8f267a 100644 (file)
@@ -1,7 +1,7 @@
-using Content.Server.Power.Components;
 using Content.Server.Power.EntitySystems;
 using Content.Server.Tesla.Components;
 using Content.Server.Lightning;
+using Content.Shared.Power.Components;
 
 namespace Content.Server.Tesla.EntitySystems;
 
index df204cf338a139d23b92b959c2750fdfbff83e96..86a7b512b6c26f5840ebdd816483759e4a11270e 100644 (file)
@@ -1,9 +1,7 @@
 using System.Linq;
 using System.Numerics;
 using Content.Server.Cargo.Systems;
-using Content.Server.Emp;
 using Content.Server.Power.Components;
-using Content.Server.Power.EntitySystems;
 using Content.Server.Vocalization.Systems;
 using Content.Shared.Cargo;
 using Content.Shared.Damage;
@@ -35,7 +33,6 @@ namespace Content.Server.VendingMachines
             SubscribeLocalEvent<VendingMachineComponent, BreakageEventArgs>(OnBreak);
             SubscribeLocalEvent<VendingMachineComponent, DamageChangedEvent>(OnDamageChanged);
             SubscribeLocalEvent<VendingMachineComponent, PriceCalculationEvent>(OnVendingPrice);
-            SubscribeLocalEvent<VendingMachineComponent, EmpPulseEvent>(OnEmpPulse);
             SubscribeLocalEvent<VendingMachineComponent, TryVocalizeEvent>(OnTryVocalize);
 
             SubscribeLocalEvent<VendingMachineComponent, ActivatableUIOpenAttemptEvent>(OnActivatableUIOpenAttempt);
@@ -86,6 +83,7 @@ namespace Content.Server.VendingMachines
         private void OnBreak(EntityUid uid, VendingMachineComponent vendComponent, BreakageEventArgs eventArgs)
         {
             vendComponent.Broken = true;
+            Dirty(uid, vendComponent);
             TryUpdateVisualState((uid, vendComponent));
         }
 
@@ -94,6 +92,7 @@ namespace Content.Server.VendingMachines
             if (!args.DamageIncreased && component.Broken)
             {
                 component.Broken = false;
+                Dirty(uid, component);
                 TryUpdateVisualState((uid, component));
                 return;
             }
@@ -257,16 +256,6 @@ namespace Content.Server.VendingMachines
             args.Price += priceSets.Max();
         }
 
-        private void OnEmpPulse(EntityUid uid, VendingMachineComponent component, ref EmpPulseEvent args)
-        {
-            if (!component.Broken && this.IsPowered(uid, EntityManager))
-            {
-                args.Affected = true;
-                args.Disabled = true;
-                component.NextEmpEject = Timing.CurTime;
-            }
-        }
-
         private void OnTryVocalize(Entity<VendingMachineComponent> ent, ref TryVocalizeEvent args)
         {
             args.Cancelled |= ent.Comp.Broken;
index 4865d08782d70088bf4861971a82807a1ae12cb7..d697e2bef18d4440e4f8c8e9e944efc40ff93a9e 100644 (file)
@@ -1,7 +1,6 @@
-using Content.Server.Power.Components;
 using Content.Shared.Damage;
 using Content.Shared.Damage.Events;
-using Content.Shared.FixedPoint;
+using Content.Shared.Power;
 using Content.Shared.PowerCell.Components;
 using Content.Shared.Projectiles;
 using Content.Shared.Weapons.Ranged;
index 1e9489e9ec90052c1ec1b601f9fa8dc8d550a82e..88faf2fdcdb2c264259da90675e88c96975ed884 100644 (file)
@@ -22,5 +22,5 @@ public sealed partial class XAEEmpInAreaComponent : Component
     /// Duration (in seconds) for which devices going to be disabled.
     /// </summary>
     [DataField]
-    public float DisableDuration = 60f;
+    public TimeSpan DisableDuration = TimeSpan.FromSeconds(60);
 }
index 5e9bf7352b08bf413d4000fe6dbb2f55fc6caa33..85ffc756272c7ff80d9bbbb6dc2158cccc464d33 100644 (file)
@@ -1,6 +1,6 @@
-using Content.Server.Power.Components;
 using Content.Server.Power.EntitySystems;
 using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
+using Content.Shared.Power.Components;
 using Content.Shared.Xenoarchaeology.Artifact;
 using Content.Shared.Xenoarchaeology.Artifact.XAE;
 
index e48e0ae6a73c32d59bad56a4d41f3f31219aff1b..c05a3f6e41c0ce18103903699afcbf45c4def9fb 100644 (file)
@@ -61,5 +61,5 @@ public sealed partial class ElectricityAnomalyComponent : Component
     /// Duration of devices being disabled by the emp pulse upon going supercritical.
     /// <summary>
     [DataField, ViewVariables(VVAccess.ReadWrite)]
-    public float EmpDisabledDuration = 60f;
+    public TimeSpan EmpDisabledDuration = TimeSpan.FromSeconds(60);
 }
index 18b79bf52bbec587f8c448c374a8b96d06120fa9..07aae61f83d04fb4c61a02b6a84313e3b5927f20 100644 (file)
@@ -2,13 +2,16 @@ using System.Linq;
 using Content.Shared.Access.Components;
 using Content.Shared.Clothing.Components;
 using Content.Shared.Contraband;
+using Content.Shared.Emp;
 using Content.Shared.Inventory;
 using Content.Shared.Inventory.Events;
 using Content.Shared.Item;
 using Content.Shared.Lock;
 using Content.Shared.Tag;
 using Content.Shared.Verbs;
+using Robust.Shared.Network;
 using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
 using Robust.Shared.Timing;
 using Robust.Shared.Utility;
 
@@ -23,8 +26,11 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
     [Dependency] private readonly SharedItemSystem _itemSystem = default!;
     [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
     [Dependency] private readonly TagSystem _tag = default!;
-    [Dependency] protected readonly IGameTiming _timing = default!;
+    [Dependency] protected readonly IGameTiming Timing = default!;
     [Dependency] private readonly LockSystem _lock = default!;
+    [Dependency] private readonly IRobustRandom _random = default!;
+    [Dependency] protected readonly SharedUserInterfaceSystem UI = default!;
+    [Dependency] private readonly INetManager _net = default!;
 
     private static readonly SlotFlags[] IgnoredSlots =
     {
@@ -32,12 +38,12 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
         SlotFlags.PREVENTEQUIP,
         SlotFlags.NONE
     };
+
     private static readonly SlotFlags[] Slots = Enum.GetValues<SlotFlags>().Except(IgnoredSlots).ToArray();
 
     private readonly Dictionary<SlotFlags, List<EntProtoId>> _data = new();
 
     public readonly Dictionary<SlotFlags, List<string>> ValidVariants = new();
-    [Dependency] protected readonly SharedUserInterfaceSystem UI = default!;
 
     private static readonly ProtoId<TagPrototype> WhitelistChameleonTag = "WhitelistChameleon";
 
@@ -47,6 +53,7 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
         SubscribeLocalEvent<ChameleonClothingComponent, GotEquippedEvent>(OnGotEquipped);
         SubscribeLocalEvent<ChameleonClothingComponent, GotUnequippedEvent>(OnGotUnequipped);
         SubscribeLocalEvent<ChameleonClothingComponent, GetVerbsEvent<InteractionVerb>>(OnVerb);
+        SubscribeLocalEvent<ChameleonClothingComponent, EmpPulseEvent>(OnEmpPulse);
 
         SubscribeLocalEvent<ChameleonClothingComponent, PrototypesReloadedEventArgs>(OnPrototypeReload);
         PrepareAllVariants();
@@ -97,21 +104,21 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
 
         // clothing sprite logic
         if (TryComp(uid, out ClothingComponent? clothing) &&
-            proto.TryGetComponent("Clothing", out ClothingComponent? otherClothing))
+            proto.TryGetComponent(out ClothingComponent? otherClothing, Factory))
         {
             _clothingSystem.CopyVisuals(uid, otherClothing, clothing);
         }
 
         // appearance data logic
         if (TryComp(uid, out AppearanceComponent? appearance) &&
-            proto.TryGetComponent("Appearance", out AppearanceComponent? appearanceOther))
+            proto.TryGetComponent(out AppearanceComponent? appearanceOther, Factory))
         {
             _appearance.AppendData(appearanceOther, uid);
             Dirty(uid, appearance);
         }
 
         // properly mark contraband
-        if (proto.TryGetComponent("Contraband", out ContrabandComponent? contra))
+        if (proto.TryGetComponent(out ContrabandComponent? contra, Factory))
         {
             EnsureComp<ContrabandComponent>(uid, out var current);
             _contraband.CopyDetails(uid, contra, current);
@@ -138,6 +145,24 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
         });
     }
 
+    private void OnEmpPulse(EntityUid uid, ChameleonClothingComponent component, ref EmpPulseEvent args)
+    {
+        if (!component.AffectedByEmp)
+            return;
+
+        if (component.EmpContinuous)
+            component.NextEmpChange = Timing.CurTime + TimeSpan.FromSeconds(1f / component.EmpChangeIntensity);
+
+        if (_net.IsServer) // needs RandomPredicted
+        {
+            var pick = GetRandomValidPrototype(component.Slot, component.RequireTag);
+            SetSelectedPrototype(uid, pick, component: component);
+        }
+
+        args.Affected = true;
+        args.Disabled = true;
+    }
+
     protected virtual void UpdateSprite(EntityUid uid, EntityPrototype proto) { }
 
     /// <summary>
@@ -157,7 +182,7 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
             return false;
 
         // check if it's valid clothing
-        if (!proto.TryGetComponent("Clothing", out ClothingComponent? clothing))
+        if (!proto.TryGetComponent(out ClothingComponent? clothing, Factory))
             return false;
         if (!clothing.Slots.HasFlag(chameleonSlot))
             return false;
@@ -187,6 +212,14 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
         return validTargets;
     }
 
+    /// <summary>
+    ///     Get a random prototype for a given slot.
+    /// </summary>
+    public string GetRandomValidPrototype(SlotFlags slot, string? tag = null)
+    {
+        return _random.Pick(GetValidTargets(slot, tag).ToList());
+    }
+
     protected void PrepareAllVariants()
     {
         _data.Clear();
@@ -215,4 +248,9 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
             }
         }
     }
+
+    // TODO: Predict and use component states for the UI
+    public virtual void SetSelectedPrototype(EntityUid uid, string? protoId, bool forceUpdate = false,
+        ChameleonClothingComponent? component = null)
+    { }
 }
index 9e5a56de8378c71d3c6050e88098567d13211d53..71748c0be3fdffd6a1bef2c87088431bf1297f7c 100644 (file)
@@ -5,24 +5,30 @@ namespace Content.Shared.Emp;
 
 /// <summary>
 /// While entity has this component it is "disabled" by EMP.
-/// Add desired behaviour in other systems
+/// Add desired behaviour in other systems.
 /// </summary>
-[RegisterComponent, NetworkedComponent, AutoGenerateComponentPause]
+[RegisterComponent, NetworkedComponent]
+[AutoGenerateComponentState, AutoGenerateComponentPause]
 [Access(typeof(SharedEmpSystem))]
 public sealed partial class EmpDisabledComponent : Component
 {
     /// <summary>
-    /// Moment of time when component is removed and entity stops being "disabled"
+    /// Moment of time when the component is removed and entity stops being "disabled".
     /// </summary>
-    [DataField("timeLeft", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
-    [AutoPausedField]
-    public TimeSpan DisabledUntil;
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+    [AutoNetworkedField, AutoPausedField]
+    public TimeSpan DisabledUntil = TimeSpan.Zero;
 
-    [DataField("effectCoolDown"), ViewVariables(VVAccess.ReadWrite)]
-    public float EffectCooldown = 3f;
+    /// <summary>
+    /// Default time between visual effect spawns.
+    /// This gets a random multiplier.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public TimeSpan EffectCooldown = TimeSpan.FromSeconds(3);
 
     /// <summary>
-    /// When next effect will be spawned
+    /// When next effect will be spawned.
+    /// TODO: Particle system.
     /// </summary>
     [AutoPausedField]
     public TimeSpan TargetTime = TimeSpan.Zero;
index deb2afd7092bfe09a89933d347b0d941aaa32761..7e6ea58dbc93977e77c6d637e5c9899c924e306c 100644 (file)
@@ -1,21 +1,58 @@
 using Content.Shared.Examine;
+using Content.Shared.Rejuvenate;
+using Robust.Shared.Audio;
+using Robust.Shared.Audio.Systems;
 using Robust.Shared.Map;
 using Robust.Shared.Timing;
+using Robust.Shared.Network;
+using Robust.Shared.Prototypes;
 
 namespace Content.Shared.Emp;
 
 public abstract class SharedEmpSystem : EntitySystem
 {
     [Dependency] protected readonly IGameTiming Timing = default!;
+    [Dependency] private readonly EntityLookupSystem _lookup = default!;
+    [Dependency] private readonly INetManager _net = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly SharedTransformSystem _transform = default!;
+
+    private HashSet<EntityUid> _entSet = new();
 
     public override void Initialize()
     {
         base.Initialize();
 
         SubscribeLocalEvent<EmpDisabledComponent, ExaminedEvent>(OnExamine);
+        SubscribeLocalEvent<EmpDisabledComponent, ComponentRemove>(OnRemove);
+        SubscribeLocalEvent<EmpDisabledComponent, RejuvenateEvent>(OnRejuvenate);
     }
 
-    protected const string EmpDisabledEffectPrototype = "EffectEmpDisabled";
+    public static readonly EntProtoId EmpPulseEffectPrototype = "EffectEmpPulse";
+    public static readonly EntProtoId EmpDisabledEffectPrototype = "EffectEmpDisabled";
+    public static readonly SoundSpecifier EmpSound = new SoundPathSpecifier("/Audio/Effects/Lightning/lightningbolt.ogg");
+
+    /// <summary>
+    /// Triggers an EMP pulse at the given location, by first raising an <see cref="EmpAttemptEvent"/>, then by raising <see cref="EmpPulseEvent"/> on all entities in range.
+    /// </summary>
+    /// <param name="coordinates">The location to trigger the EMP pulse at.</param>
+    /// <param name="range">The range of the EMP pulse.</param>
+    /// <param name="energyConsumption">The amount of energy consumed by the EMP pulse. In Joule.</param>
+    /// <param name="duration">The duration of the EMP effects.</param>
+    /// <param name="user">The player that caused the effect. Used for predicted audio.</param>
+    public void EmpPulse(MapCoordinates mapCoordinates, float range, float energyConsumption, TimeSpan duration, EntityUid? user = null)
+    {
+        foreach (var uid in _lookup.GetEntitiesInRange(mapCoordinates, range))
+        {
+            TryEmpEffects(uid, energyConsumption, duration, user);
+        }
+        // TODO: replace with PredictedSpawn once it works with animated sprites
+        if (_net.IsServer)
+            Spawn(EmpPulseEffectPrototype, mapCoordinates);
+
+        var coordinates = _transform.ToCoordinates(mapCoordinates);
+        _audio.PlayPredicted(EmpSound, coordinates, user);
+    }
 
     /// <summary>
     /// Triggers an EMP pulse at the given location, by first raising an <see cref="EmpAttemptEvent"/>, then a raising <see cref="EmpPulseEvent"/> on all entities in range.
@@ -24,12 +61,119 @@ public abstract class SharedEmpSystem : EntitySystem
     /// <param name="range">The range of the EMP pulse.</param>
     /// <param name="energyConsumption">The amount of energy consumed by the EMP pulse.</param>
     /// <param name="duration">The duration of the EMP effects.</param>
-    public virtual void EmpPulse(MapCoordinates coordinates, float range, float energyConsumption, float duration)
+    /// <param name="user">The player that caused the effect. Used for predicted audio.</param>
+    public void EmpPulse(EntityCoordinates coordinates, float range, float energyConsumption, TimeSpan duration, EntityUid? user = null)
+    {
+        _entSet.Clear();
+        _lookup.GetEntitiesInRange(coordinates, range, _entSet);
+        foreach (var uid in _entSet)
+        {
+            TryEmpEffects(uid, energyConsumption, duration, user);
+        }
+        // TODO: replace with PredictedSpawn once it works with animated sprites
+        if (_net.IsServer)
+            Spawn(EmpPulseEffectPrototype, coordinates);
+
+        _audio.PlayPredicted(EmpSound, coordinates, user);
+    }
+
+    /// <summary>
+    /// Attempts to apply the effects of an EMP pulse onto an entity by first raising an <see cref="EmpAttemptEvent"/>, followed by raising a <see cref="EmpPulseEvent"/> on it.
+    /// </summary>
+    /// <param name="uid">The entity to apply the EMP effects on.</param>
+    /// <param name="energyConsumption">The amount of energy consumed by the EMP.</param>
+    /// <param name="duration">The duration of the EMP effects.</param>
+    /// <param name="user">The player that caused the EMP. For prediction purposes.</param>
+    /// <returns>If the entity was affected by the EMP.</returns>
+    public bool TryEmpEffects(EntityUid uid, float energyConsumption, TimeSpan duration, EntityUid? user = null)
     {
+        var attemptEv = new EmpAttemptEvent();
+        RaiseLocalEvent(uid, ref attemptEv);
+        if (attemptEv.Cancelled)
+            return false;
+
+        return DoEmpEffects(uid, energyConsumption, duration, user);
+    }
+
+    /// <summary>
+    /// Applies the effects of an EMP pulse onto an entity by raising a <see cref="EmpPulseEvent"/> on it.
+    /// </summary>
+    /// <param name="uid">The entity to apply the EMP effects on.</param>
+    /// <param name="energyConsumption">The amount of energy consumed by the EMP.</param>
+    /// <param name="duration">The duration of the EMP effects.</param>
+    /// <param name="user">The player that caused the EMP. For prediction purposes.</param>
+    /// <returns>If the entity was affected by the EMP.</returns>
+    public bool DoEmpEffects(EntityUid uid, float energyConsumption, TimeSpan duration, EntityUid? user = null)
+    {
+        var ev = new EmpPulseEvent(energyConsumption, false, false, duration, user);
+        RaiseLocalEvent(uid, ref ev);
+
+        // TODO: replace with PredictedSpawn once it works with animated sprites
+        if (ev.Affected && _net.IsServer)
+            Spawn(EmpDisabledEffectPrototype, Transform(uid).Coordinates);
+
+        if (!ev.Disabled)
+            return ev.Affected;
+
+        var disabled = EnsureComp<EmpDisabledComponent>(uid);
+        disabled.DisabledUntil = Timing.CurTime + duration;
+        Dirty(uid, disabled);
+
+        return ev.Affected;
+    }
+
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        var curTime = Timing.CurTime;
+        var query = EntityQueryEnumerator<EmpDisabledComponent>();
+        while (query.MoveNext(out var uid, out var comp))
+        {
+            if (curTime < comp.DisabledUntil)
+                continue;
+
+            RemComp<EmpDisabledComponent>(uid);
+        }
     }
 
     private void OnExamine(Entity<EmpDisabledComponent> ent, ref ExaminedEvent args)
     {
         args.PushMarkup(Loc.GetString("emp-disabled-comp-on-examine"));
     }
+
+    private void OnRemove(Entity<EmpDisabledComponent> ent, ref ComponentRemove args)
+    {
+        var ev = new EmpDisabledRemovedEvent();
+        RaiseLocalEvent(ent, ref ev);
+    }
+
+    private void OnRejuvenate(Entity<EmpDisabledComponent> ent, ref RejuvenateEvent args)
+    {
+        RemCompDeferred<EmpDisabledComponent>(ent);
+    }
 }
+
+/// <summary>
+/// Raised on an entity before <see cref="EmpPulseEvent"/>. Cancel this to prevent the emp event being raised.
+/// </summary>
+[ByRefEvent]
+public record struct EmpAttemptEvent(bool Cancelled);
+
+/// <summary>
+/// Raised on an entity when it gets hit by an EMP Pulse.
+/// </summary>
+/// <param name="EnergyConsumption">The amount of energy to remove from batteries. In Joule.</param>
+/// <param name="Affected">Set this is true in the subscription to spawn a visual effect at the entity's location.</param>
+/// <param name="Disabled">Set this to ture in the subscription to add <see cref="EmpDisabledComponent"/> to the entity.</param>
+/// <param name="Duration">The duration the entity will be disabled.</param>
+/// <param name="User">The player that caused the EMP. For prediction purposes.</param>
+
+[ByRefEvent]
+public record struct EmpPulseEvent(float EnergyConsumption, bool Affected, bool Disabled, TimeSpan Duration, EntityUid? User);
+
+/// <summary>
+/// Raised on an entity after <see cref="EmpDisabledComponent"/> is removed.
+/// </summary>
+[ByRefEvent]
+public record struct EmpDisabledRemovedEvent();
index eee0aeb51bd2bdf95ad49a645d73170fcf39292a..0f9eacc58dec280d108cd4a7b053ecbf6330d965 100644 (file)
@@ -27,7 +27,7 @@ public sealed partial class EmpReactionEffect : EventEntityEffect<EmpReactionEff
     ///     Amount of time entities will be disabled
     /// </summary>
     [DataField("duration")]
-    public float DisableDuration = 15;
+    public TimeSpan DisableDuration = TimeSpan.FromSeconds(15);
 
     protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
             => Loc.GetString("reagent-effect-guidebook-emp-reaction-effect", ("chance", Probability));
index 65097f0d0616fbad420600b76b4b37fed62ebd56..9ca0a1821b02e853e01ee6fcd407e81bd5deda18 100644 (file)
@@ -7,6 +7,7 @@ using Content.Shared.DeviceLinking.Events;
 using Content.Shared.DeviceNetwork;
 using Content.Shared.DeviceNetwork.Events;
 using Content.Shared.DoAfter;
+using Content.Shared.Emp;
 using Content.Shared.Hands.EntitySystems;
 using Content.Shared.Interaction;
 using Content.Shared.Light.Components;
@@ -52,6 +53,7 @@ public abstract class SharedPoweredLightSystem : EntitySystem
         SubscribeLocalEvent<PoweredLightComponent, PowerChangedEvent>(OnPowerChanged);
         SubscribeLocalEvent<PoweredLightComponent, PoweredLightDoAfterEvent>(OnDoAfter);
         SubscribeLocalEvent<PoweredLightComponent, DamageChangedEvent>(HandleLightDamaged);
+        SubscribeLocalEvent<PoweredLightComponent, EmpPulseEvent>(OnEmpPulse);
     }
 
     private void OnInit(EntityUid uid, PoweredLightComponent light, ComponentInit args)
@@ -230,29 +232,21 @@ public abstract class SharedPoweredLightSystem : EntitySystem
     /// <summary>
     ///     Try to break bulb inside light fixture
     /// </summary>
-    public bool TryDestroyBulb(EntityUid uid, PoweredLightComponent? light = null)
+    public bool TryDestroyBulb(EntityUid uid, PoweredLightComponent? light = null, EntityUid? user = null)
     {
         if (!Resolve(uid, ref light, false))
             return false;
 
-        // if we aren't mapinited,
-        // just null the spawned bulb
-        if (LifeStage(uid) < EntityLifeStage.MapInitialized)
-        {
-            light.HasLampOnSpawn = null;
-            return true;
-        }
-
         // check bulb state
         var bulbUid = GetBulb(uid, light);
-        if (bulbUid == null || !EntityManager.TryGetComponent(bulbUid.Value, out LightBulbComponent? lightBulb))
+        if (bulbUid == null || !TryComp<LightBulbComponent>(bulbUid.Value, out var lightBulb))
             return false;
         if (lightBulb.State == LightBulbState.Broken)
             return false;
 
         // break it
         _bulbSystem.SetState(bulbUid.Value, LightBulbState.Broken, lightBulb);
-        _bulbSystem.PlayBreakSound(bulbUid.Value, lightBulb);
+        _bulbSystem.PlayBreakSound(bulbUid.Value, lightBulb, user);
         UpdateLight(uid, light);
         return true;
     }
@@ -327,13 +321,18 @@ public abstract class SharedPoweredLightSystem : EntitySystem
     /// <summary>
     ///     Destroy the light bulb if the light took any damage.
     /// </summary>
+    /// <remarks>
+    ///     TODO: This should be an IThresholdBehaviour once DestructibleSystem is predicted.
+    /// </remarks>
     public void HandleLightDamaged(EntityUid uid, PoweredLightComponent component, DamageChangedEvent args)
     {
+        if (GameTiming.ApplyingState) // The destruction is already networked on its own.
+            return;
+
         // Was it being repaired, or did it take damage?
         if (args.DamageIncreased)
         {
-            // Eventually, this logic should all be done by this (or some other) system, not a component.
-            TryDestroyBulb(uid, component);
+            TryDestroyBulb(uid, component, args.Origin);
         }
     }
 
@@ -348,6 +347,12 @@ public abstract class SharedPoweredLightSystem : EntitySystem
         UpdateLight(uid, component);
     }
 
+    private void OnEmpPulse(EntityUid uid, PoweredLightComponent component, ref EmpPulseEvent args)
+    {
+        if (TryDestroyBulb(uid, component))
+            args.Affected = true;
+    }
+
     public void ToggleBlinkingLight(EntityUid uid, PoweredLightComponent light, bool isNowBlinking)
     {
         if (light.IsBlinking == isNowBlinking)
index 2ed1089ba896b5f25e5b9fea89b032e57cfa7d0c..6930b029b797d32bb9e25587ae4d1c531cc17d23 100644 (file)
@@ -5,6 +5,7 @@ using Content.Shared.Clothing;
 using Content.Shared.Damage;
 using Content.Shared.DeviceNetwork;
 using Content.Shared.DoAfter;
+using Content.Shared.Emp;
 using Content.Shared.Examine;
 using Content.Shared.GameTicking;
 using Content.Shared.Interaction;
@@ -49,6 +50,8 @@ public abstract class SharedSuitSensorSystem : EntitySystem
         SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawn);
         SubscribeLocalEvent<SuitSensorComponent, ClothingGotEquippedEvent>(OnEquipped);
         SubscribeLocalEvent<SuitSensorComponent, ClothingGotUnequippedEvent>(OnUnequipped);
+        SubscribeLocalEvent<SuitSensorComponent, EmpPulseEvent>(OnEmpPulse);
+        SubscribeLocalEvent<SuitSensorComponent, EmpDisabledRemovedEvent>(OnEmpFinished);
         SubscribeLocalEvent<SuitSensorComponent, ExaminedEvent>(OnExamine);
         SubscribeLocalEvent<SuitSensorComponent, GetVerbsEvent<Verb>>(OnVerb);
         SubscribeLocalEvent<SuitSensorComponent, EntGotInsertedIntoContainerMessage>(OnInsert);
@@ -133,6 +136,25 @@ public abstract class SharedSuitSensorSystem : EntitySystem
         Dirty(ent);
     }
 
+    private void OnEmpPulse(Entity<SuitSensorComponent> ent, ref EmpPulseEvent args)
+    {
+        args.Affected = true;
+        args.Disabled = true;
+
+        ent.Comp.PreviousMode = ent.Comp.Mode;
+        SetSensor(ent.AsNullable(), SuitSensorMode.SensorOff, null);
+
+        ent.Comp.PreviousControlsLocked = ent.Comp.ControlsLocked;
+        ent.Comp.ControlsLocked = true;
+        // SetSensor already calls Dirty
+    }
+
+    private void OnEmpFinished(Entity<SuitSensorComponent> ent, ref EmpDisabledRemovedEvent args)
+    {
+        SetSensor(ent.AsNullable(), ent.Comp.PreviousMode, null);
+        ent.Comp.ControlsLocked = ent.Comp.PreviousControlsLocked;
+    }
+
     private void OnExamine(Entity<SuitSensorComponent> ent, ref ExaminedEvent args)
     {
         if (!args.IsInDetailsRange)
index b20b7af2c9e4b922d89537d314b66d11c5a76959..19bee1b01734bc4e318a0653058d5278a7200d74 100644 (file)
@@ -85,13 +85,13 @@ public sealed partial class SuitSensorComponent : Component
     /// <summary>
     /// The previous mode of the suit. This is used to restore the state when an EMP effect ends.
     /// </summary>
-    [DataField, ViewVariables]
+    [DataField, AutoNetworkedField, ViewVariables]
     public SuitSensorMode PreviousMode = SuitSensorMode.SensorOff;
 
     /// <summary>
     ///  The previous locked status of the controls.  This is used to restore the state when an EMP effect ends.
     ///  This keeps prisoner jumpsuits/internal implants from becoming unlocked after an EMP.
     /// </summary>
-    [DataField, ViewVariables]
+    [DataField, AutoNetworkedField, ViewVariables]
     public bool PreviousControlsLocked = false;
 }
index 8b477b2aa5f96aad227fc1e2fda3e1d409a47d7f..b1467b4146952f1da32d4bb00534039208cf7bca 100644 (file)
@@ -72,10 +72,10 @@ public sealed partial class NinjaSuitComponent : Component
     public float EmpConsumption = 100000f;
 
     /// <summary>
-    /// How long the EMP effects last for, in seconds
+    /// How long the EMP effects last for
     /// </summary>
     [DataField]
-    public float EmpDuration = 60f;
+    public TimeSpan EmpDuration = TimeSpan.FromSeconds(60);
 }
 
 public sealed partial class RecallKatanaEvent : InstantActionEvent;
index 3800d15b2675f8c265005700da056b3142309df9..f316b16f6be580889699e36cb8cd447019634789 100644 (file)
@@ -1,7 +1,7 @@
 using Content.Shared.Actions;
 using Content.Shared.Clothing;
 using Content.Shared.Clothing.Components;
-using Content.Shared.Clothing.EntitySystems;
+using Content.Shared.Emp;
 using Content.Shared.Inventory.Events;
 using Content.Shared.Item.ItemToggle;
 using Content.Shared.Item.ItemToggle.Components;
@@ -18,8 +18,8 @@ namespace Content.Shared.Ninja.Systems;
 public abstract class SharedNinjaSuitSystem : EntitySystem
 {
     [Dependency] private readonly ActionContainerSystem _actionContainer = default!;
-    [Dependency] private readonly SharedAudioSystem _audio = default!;
     [Dependency] private readonly ItemToggleSystem _toggle = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
     [Dependency] protected readonly SharedPopupSystem Popup = default!;
     [Dependency] private readonly SharedSpaceNinjaSystem _ninja = default!;
     [Dependency] private readonly UseDelaySystem _useDelay = default!;
@@ -36,6 +36,7 @@ public abstract class SharedNinjaSuitSystem : EntitySystem
         SubscribeLocalEvent<NinjaSuitComponent, CreateItemAttemptEvent>(OnCreateStarAttempt);
         SubscribeLocalEvent<NinjaSuitComponent, ItemToggleActivateAttemptEvent>(OnActivateAttempt);
         SubscribeLocalEvent<NinjaSuitComponent, GotUnequippedEvent>(OnUnequipped);
+        SubscribeLocalEvent<NinjaSuitComponent, EmpAttemptEvent>(OnEmpAttempt);
     }
 
     private void OnEquipped(Entity<NinjaSuitComponent> ent, ref ClothingGotEquippedEvent args)
@@ -168,7 +169,14 @@ public abstract class SharedNinjaSuitSystem : EntitySystem
         // mark the user as not wearing a suit
         _ninja.AssignSuit(user, null);
         // disable glove abilities
-        if (user.Comp.Gloves is {} uid)
+        if (user.Comp.Gloves is { } uid)
             _toggle.TryDeactivate(uid, user: user);
     }
+
+    private void OnEmpAttempt(Entity<NinjaSuitComponent> ent, ref EmpAttemptEvent args)
+    {
+        // ninja suit (battery) is immune to emp
+        // powercell relays the event to suit
+        args.Cancelled = true;
+    }
 }
diff --git a/Content.Shared/Power/ChargeEvents.cs b/Content.Shared/Power/ChargeEvents.cs
new file mode 100644 (file)
index 0000000..db412e9
--- /dev/null
@@ -0,0 +1,33 @@
+namespace Content.Shared.Power;
+
+/// <summary>
+/// Raised when a battery's charge or capacity changes (capacity affects relative charge percentage).
+/// </summary>
+[ByRefEvent]
+public readonly record struct ChargeChangedEvent(float Charge, float MaxCharge);
+
+/// <summary>
+/// Raised when it is necessary to get information about battery charges.
+/// </summary>
+[ByRefEvent]
+public sealed class GetChargeEvent : EntityEventArgs
+{
+    public float CurrentCharge;
+    public float MaxCharge;
+}
+
+/// <summary>
+/// Raised when it is necessary to change the current battery charge to a some value.
+/// </summary>
+[ByRefEvent]
+public sealed class ChangeChargeEvent : EntityEventArgs
+{
+    public float OriginalValue;
+    public float ResidualValue;
+
+    public ChangeChargeEvent(float value)
+    {
+        OriginalValue = value;
+        ResidualValue = value;
+    }
+}
index 02c8328fa15d318fa8e8063b750ccd2a8f55cc01..92efabd7abc3555aa9113bee14efea2913f02543 100644 (file)
@@ -6,7 +6,7 @@ namespace Content.Shared.Power.Components;
 /// <summary>
 /// Attached to APC powered entities that possess a rechargeable internal battery.
 /// If external power is interrupted, the entity will draw power from this battery instead.
-/// Requires <see cref="Content.Server.Power.Components.ApcPowerReceiverComponent"/> and <see cref="Content.Server.Power.Components.BatteryComponent"/> to function.
+/// Requires <see cref="Content.Server.Power.Components.ApcPowerReceiverComponent"/> and <see cref="BatteryComponent"/> to function.
 /// </summary>
 [RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
 [Access(typeof(SharedPowerNetSystem), typeof(SharedPowerReceiverSystem))]
diff --git a/Content.Shared/Power/Components/BatteryComponent.cs b/Content.Shared/Power/Components/BatteryComponent.cs
new file mode 100644 (file)
index 0000000..6a2d2a5
--- /dev/null
@@ -0,0 +1,34 @@
+using Content.Shared.Power.EntitySystems;
+using Content.Shared.Guidebook;
+
+namespace Content.Shared.Power.Components;
+
+/// <summary>
+/// Battery node on the pow3r network. Needs other components to connect to actual networks.
+/// </summary>
+[RegisterComponent]
+[Virtual]
+[Access(typeof(SharedBatterySystem))]
+public partial class BatteryComponent : Component
+{
+    public string SolutionName = "battery";
+
+    /// <summary>
+    /// Maximum charge of the battery in joules (ie. watt seconds)
+    /// </summary>
+    [DataField]
+    [GuidebookData]
+    public float MaxCharge;
+
+    /// <summary>
+    /// Current charge of the battery in joules (ie. watt seconds)
+    /// </summary>
+    [DataField("startingCharge")]
+    public float CurrentCharge;
+
+    /// <summary>
+    /// The price per one joule. Default is 1 credit for 10kJ.
+    /// </summary>
+    [DataField]
+    public float PricePerJoule = 0.0001f;
+}
diff --git a/Content.Shared/Power/Components/ChargerComponent.cs b/Content.Shared/Power/Components/ChargerComponent.cs
new file mode 100644 (file)
index 0000000..a3f2f8f
--- /dev/null
@@ -0,0 +1,36 @@
+using Content.Shared.Whitelist;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Power.Components;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class ChargerComponent : Component
+{
+    [ViewVariables]
+    public CellChargerStatus Status;
+
+    /// <summary>
+    /// The charge rate of the charger, in watts
+    /// </summary>
+    [DataField]
+    public float ChargeRate = 20.0f;
+
+    /// <summary>
+    /// The container ID that is holds the entities being charged.
+    /// </summary>
+    [DataField(required: true)]
+    public string SlotId = string.Empty;
+
+    /// <summary>
+    /// A whitelist for what entities can be charged by this Charger.
+    /// </summary>
+    [DataField]
+    public EntityWhitelist? Whitelist;
+
+    /// <summary>
+    /// Indicates whether the charger is portable and thus subject to EMP effects
+    /// and bypasses checks for transform, anchored, and ApcPowerReceiverComponent.
+    /// </summary>
+    [DataField]
+    public bool Portable = false;
+}
diff --git a/Content.Shared/Power/EntitySystems/SharedBatterySystem.cs b/Content.Shared/Power/EntitySystems/SharedBatterySystem.cs
new file mode 100644 (file)
index 0000000..90931ef
--- /dev/null
@@ -0,0 +1,44 @@
+using Content.Shared.Emp;
+using Content.Shared.Power.Components;
+
+namespace Content.Shared.Power.EntitySystems;
+
+public abstract class SharedBatterySystem : EntitySystem
+{
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<BatteryComponent, EmpPulseEvent>(OnEmpPulse);
+    }
+
+    private void OnEmpPulse(EntityUid uid, BatteryComponent component, ref EmpPulseEvent args)
+    {
+        args.Affected = true;
+        UseCharge(uid, args.EnergyConsumption, component);
+        // Apply a cooldown to the entity's self recharge if needed to avoid it immediately self recharging after an EMP.
+        TrySetChargeCooldown(uid);
+    }
+
+    public virtual float UseCharge(EntityUid uid, float value, BatteryComponent? battery = null)
+    {
+        return 0f;
+    }
+
+    public virtual void SetMaxCharge(EntityUid uid, float value, BatteryComponent? battery = null) { }
+
+    public virtual float ChangeCharge(EntityUid uid, float value, BatteryComponent? battery = null)
+    {
+        return 0f;
+    }
+
+    /// <summary>
+    /// Checks if the entity has a self recharge and puts it on cooldown if applicable.
+    /// </summary>
+    public virtual void TrySetChargeCooldown(EntityUid uid, float value = -1) { }
+
+    public virtual bool TryUseCharge(EntityUid uid, float value, BatteryComponent? battery = null)
+    {
+        return false;
+    }
+}
diff --git a/Content.Shared/Power/EntitySystems/SharedChargerSystem.cs b/Content.Shared/Power/EntitySystems/SharedChargerSystem.cs
new file mode 100644 (file)
index 0000000..a150436
--- /dev/null
@@ -0,0 +1,20 @@
+using Content.Shared.Emp;
+using Content.Shared.Power.Components;
+
+namespace Content.Shared.Power.EntitySystems;
+
+public abstract class SharedChargerSystem : EntitySystem
+{
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<ChargerComponent, EmpPulseEvent>(OnEmpPulse);
+    }
+
+    private void OnEmpPulse(EntityUid uid, ChargerComponent component, ref EmpPulseEvent args)
+    {
+        args.Affected = true;
+        args.Disabled = true;
+    }
+}
index 3398563e556f5e15afd14ef5436b7a4113dbbfef..0b71a3c24df64780b655af82702037e0c8993e7d 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.Containers.ItemSlots;
+using Content.Shared.Emp;
 using Content.Shared.PowerCell.Components;
 using Content.Shared.Rejuvenate;
 using Robust.Shared.Containers;
@@ -22,6 +23,8 @@ public abstract class SharedPowerCellSystem : EntitySystem
         SubscribeLocalEvent<PowerCellSlotComponent, EntInsertedIntoContainerMessage>(OnCellInserted);
         SubscribeLocalEvent<PowerCellSlotComponent, EntRemovedFromContainerMessage>(OnCellRemoved);
         SubscribeLocalEvent<PowerCellSlotComponent, ContainerIsInsertingAttemptEvent>(OnCellInsertAttempt);
+
+        SubscribeLocalEvent<PowerCellComponent, EmpAttemptEvent>(OnCellEmpAttempt);
     }
 
     private void OnMapInit(Entity<PowerCellDrawComponent> ent, ref MapInitEvent args)
@@ -71,6 +74,14 @@ public abstract class SharedPowerCellSystem : EntitySystem
         RaiseLocalEvent(uid, new PowerCellChangedEvent(true), false);
     }
 
+    private void OnCellEmpAttempt(EntityUid uid, PowerCellComponent component, EmpAttemptEvent args)
+    {
+        var parent = Transform(uid).ParentUid;
+        // relay the attempt event to the slot so it can cancel it
+        if (HasComp<PowerCellSlotComponent>(parent))
+            RaiseLocalEvent(parent, ref args);
+    }
+
     public void SetDrawEnabled(Entity<PowerCellDrawComponent?> ent, bool enabled)
     {
         if (!Resolve(ent, ref ent.Comp, false) || ent.Comp.Enabled == enabled)
index 66a0ae80275ae441c66b192421b48a427a8055b8..28911fb6741662f440df0aafd807be69d83824c7 100644 (file)
@@ -1,18 +1,20 @@
 using Content.Shared.Inventory;
+using Robust.Shared.GameStates;
 
 namespace Content.Shared.Radio.Components;
 
 /// <summary>
-///     This component relays radio messages to the parent entity's chat when equipped.
+/// This component relays radio messages to the parent entity's chat when equipped.
 /// </summary>
-[RegisterComponent]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
 public sealed partial class HeadsetComponent : Component
 {
-    [DataField("enabled")]
+    [DataField, AutoNetworkedField]
     public bool Enabled = true;
 
+    [DataField, AutoNetworkedField]
     public bool IsEquipped = false;
 
-    [DataField("requiredSlot")]
+    [DataField, AutoNetworkedField]
     public SlotFlags RequiredSlot = SlotFlags.EARS;
 }
index 8083c8cd3a12e7710fa66f4ac983e95edc8c9671..5526e8f133cfa9f0beb8489af294777acbc6d5e2 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Shared.Emp;
 using Content.Shared.Inventory;
 using Content.Shared.Inventory.Events;
 using Content.Shared.Radio.Components;
@@ -9,9 +10,11 @@ public abstract class SharedHeadsetSystem : EntitySystem
     public override void Initialize()
     {
         base.Initialize();
+
         SubscribeLocalEvent<HeadsetComponent, InventoryRelayedEvent<GetDefaultRadioChannelEvent>>(OnGetDefault);
         SubscribeLocalEvent<HeadsetComponent, GotEquippedEvent>(OnGotEquipped);
         SubscribeLocalEvent<HeadsetComponent, GotUnequippedEvent>(OnGotUnequipped);
+        SubscribeLocalEvent<HeadsetComponent, EmpPulseEvent>(OnEmpPulse);
     }
 
     private void OnGetDefault(EntityUid uid, HeadsetComponent component, InventoryRelayedEvent<GetDefaultRadioChannelEvent> args)
@@ -23,16 +26,27 @@ public abstract class SharedHeadsetSystem : EntitySystem
         }
 
         if (TryComp(uid, out EncryptionKeyHolderComponent? keyHolder))
-            args.Args.Channel ??= keyHolder.DefaultChannel; 
+            args.Args.Channel ??= keyHolder.DefaultChannel;
     }
 
     protected virtual void OnGotEquipped(EntityUid uid, HeadsetComponent component, GotEquippedEvent args)
     {
         component.IsEquipped = args.SlotFlags.HasFlag(component.RequiredSlot);
+        Dirty(uid, component);
     }
 
     protected virtual void OnGotUnequipped(EntityUid uid, HeadsetComponent component, GotUnequippedEvent args)
     {
         component.IsEquipped = false;
+        Dirty(uid, component);
+    }
+
+    private void OnEmpPulse(Entity<HeadsetComponent> ent, ref EmpPulseEvent args)
+    {
+        if (ent.Comp.Enabled)
+        {
+            args.Affected = true;
+            args.Disabled = true;
+        }
     }
 }
similarity index 72%
rename from Content.Server/SurveillanceCamera/Components/SurveillanceCameraComponent.cs
rename to Content.Shared/SurveillanceCamera/Components/SurveillanceCameraComponent.cs
index b0d41e52c742e61355435fe6e0e1b1d021c401ab..1c5ba8ad17bccd71df34d70d8a45dcbda2094412 100644 (file)
@@ -1,10 +1,11 @@
 using Content.Shared.DeviceNetwork;
+using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
 
-namespace Content.Server.SurveillanceCamera;
+namespace Content.Shared.SurveillanceCamera.Components;
 
-[RegisterComponent]
-[Access(typeof(SurveillanceCameraSystem))]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SharedSurveillanceCameraSystem))]
 public sealed partial class SurveillanceCameraComponent : Component
 {
     // List of active viewers. This is for bookkeeping purposes,
@@ -24,23 +25,20 @@ public sealed partial class SurveillanceCameraComponent : Component
 
     // If this camera is active or not. Deactivating a camera
     // will not allow it to obtain any new viewers.
-    [ViewVariables]
-    public bool Active { get; set; } = true;
+    [DataField]
+    public bool Active = true;
 
     // This one isn't easy to deal with. Will require a UI
     // to change/set this so mapping these in isn't
     // the most terrible thing possible.
-    [ViewVariables(VVAccess.ReadWrite)]
     [DataField("id")]
-    public string CameraId { get; set;  } = "camera";
+    public string CameraId = "camera";
 
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("nameSet")]
-    public bool NameSet { get; set; }
+    [DataField, AutoNetworkedField]
+    public bool NameSet;
 
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("networkSet")]
-    public bool NetworkSet { get; set; }
+    [DataField, AutoNetworkedField]
+    public bool NetworkSet;
 
     // This has to be device network frequency prototypes.
     [DataField("setupAvailableNetworks")]
index e76d5516e8576451d77c401a303fe9f5db84e311..b6743669e9eb397e0109f42f393d5e826df46028 100644 (file)
@@ -1,8 +1,56 @@
-using Robust.Shared.GameStates;
+using Content.Shared.Emp;
+using Content.Shared.SurveillanceCamera.Components;
+using Content.Shared.Verbs;
 using Robust.Shared.Serialization;
 
 namespace Content.Shared.SurveillanceCamera;
 
+public abstract class SharedSurveillanceCameraSystem : EntitySystem
+{
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<SurveillanceCameraComponent, GetVerbsEvent<AlternativeVerb>>(AddVerbs);
+        SubscribeLocalEvent<SurveillanceCameraComponent, EmpPulseEvent>(OnEmpPulse);
+        SubscribeLocalEvent<SurveillanceCameraComponent, EmpDisabledRemovedEvent>(OnEmpDisabledRemoved);
+    }
+
+    private void AddVerbs(EntityUid uid, SurveillanceCameraComponent component, GetVerbsEvent<AlternativeVerb> args)
+    {
+        if (!args.CanInteract || !args.CanComplexInteract)
+            return;
+
+        if (component.NameSet && component.NetworkSet)
+            return;
+
+        AlternativeVerb verb = new()
+        {
+            Text = Loc.GetString("surveillance-camera-setup"),
+            Act = () => OpenSetupInterface(uid, args.User, component)
+        };
+        args.Verbs.Add(verb);
+    }
+
+    private void OnEmpPulse(EntityUid uid, SurveillanceCameraComponent component, ref EmpPulseEvent args)
+    {
+        if (component.Active)
+        {
+            args.Affected = true;
+            args.Disabled = true;
+            SetActive(uid, false);
+        }
+    }
+
+    private void OnEmpDisabledRemoved(EntityUid uid, SurveillanceCameraComponent component, ref EmpDisabledRemovedEvent args)
+    {
+        SetActive(uid, true);
+    }
+
+    // TODO: predict the rest of the server side system
+    public virtual void SetActive(EntityUid camera, bool setting, SurveillanceCameraComponent? component = null) { }
+
+    protected virtual void OpenSetupInterface(EntityUid uid, EntityUid player, SurveillanceCameraComponent? camera = null) { }
+}
+
 [Serializable, NetSerializable]
 public enum SurveillanceCameraVisualsKey : byte
 {
index 136c4474a225a339c8b6abe36b9d3236b39a8491..50fb7a25e48057e1c774f973417a7e0da838648a 100644 (file)
@@ -6,7 +6,6 @@ namespace Content.Shared.Trigger.Systems;
 public sealed class EmpOnTriggerSystem : EntitySystem
 {
     [Dependency] private readonly SharedEmpSystem _emp = default!;
-    [Dependency] private readonly SharedTransformSystem _transform = default!;
 
     public override void Initialize()
     {
@@ -25,7 +24,7 @@ public sealed class EmpOnTriggerSystem : EntitySystem
         if (target == null)
             return;
 
-        _emp.EmpPulse(_transform.GetMapCoordinates(target.Value), ent.Comp.Range, ent.Comp.EnergyConsumption, (float)ent.Comp.DisableDuration.TotalSeconds);
+        _emp.EmpPulse(Transform(target.Value).Coordinates, ent.Comp.Range, ent.Comp.EnergyConsumption, ent.Comp.DisableDuration, args.User);
         args.Handled = true;
     }
 }
index 141183873d1d7f462a12d8c7b582b6a747807198..610f4d0efecedc46a777b138dce8db5256ed56f7 100644 (file)
@@ -1,19 +1,19 @@
-using Content.Shared.Emag.Components;
-using Robust.Shared.Prototypes;
 using System.Linq;
 using Content.Shared.Access.Components;
 using Content.Shared.Access.Systems;
 using Content.Shared.Advertise.Components;
 using Content.Shared.Advertise.Systems;
 using Content.Shared.DoAfter;
+using Content.Shared.Emag.Components;
 using Content.Shared.Emag.Systems;
+using Content.Shared.Emp;
 using Content.Shared.Interaction;
 using Content.Shared.Popups;
 using Content.Shared.Power.EntitySystems;
 using Robust.Shared.Audio;
 using Robust.Shared.Audio.Systems;
 using Robust.Shared.GameStates;
-using Robust.Shared.Network;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
 using Robust.Shared.Timing;
 
@@ -41,6 +41,7 @@ public abstract partial class SharedVendingMachineSystem : EntitySystem
         SubscribeLocalEvent<VendingMachineComponent, ComponentGetState>(OnVendingGetState);
         SubscribeLocalEvent<VendingMachineComponent, MapInitEvent>(OnMapInit);
         SubscribeLocalEvent<VendingMachineComponent, GotEmaggedEvent>(OnEmagged);
+        SubscribeLocalEvent<VendingMachineComponent, EmpPulseEvent>(OnEmpPulse);
         SubscribeLocalEvent<VendingMachineComponent, RestockDoAfterEvent>(OnRestockDoAfter);
 
         SubscribeLocalEvent<VendingMachineRestockComponent, AfterInteractEvent>(OnAfterInteract);
@@ -83,6 +84,7 @@ public abstract partial class SharedVendingMachineSystem : EntitySystem
             EjectEnd = component.EjectEnd,
             DenyEnd = component.DenyEnd,
             DispenseOnHitEnd = component.DispenseOnHitEnd,
+            Broken = component.Broken,
         };
     }
 
@@ -145,6 +147,16 @@ public abstract partial class SharedVendingMachineSystem : EntitySystem
         RestockInventoryFromPrototype(uid, component, component.InitialStockQuality);
     }
 
+    private void OnEmpPulse(Entity<VendingMachineComponent> ent, ref EmpPulseEvent args)
+    {
+        if (!ent.Comp.Broken && _receiver.IsPowered(ent.Owner))
+        {
+            args.Affected = true;
+            args.Disabled = true;
+            ent.Comp.NextEmpEject = Timing.CurTime;
+        }
+    }
+
     protected virtual void EjectItem(EntityUid uid, VendingMachineComponent? vendComponent = null, bool forceEject = false) { }
 
     /// <summary>
index 6a9d650898d98c8367dc97925e9803836b41af52..29ff3c654add04c6ba2493e18c4ff0fae0577447 100644 (file)
@@ -67,6 +67,7 @@ namespace Content.Shared.VendingMachines
 
         public string? NextItemToEject;
 
+        [DataField]
         public bool Broken;
 
         /// <summary>
@@ -300,5 +301,7 @@ namespace Content.Shared.VendingMachines
         public TimeSpan? DenyEnd;
 
         public TimeSpan? DispenseOnHitEnd;
+
+        public bool Broken;
     }
 }
index d1096b85f5e83ac10e04046bbcef04ec99f1c2b0..6919f776f23d51be63a21b95ae45e5a1c6cf79cd 100644 (file)
@@ -8,17 +8,14 @@
     drawdepth: Effects
     noRot: true
     layers:
-      - shader: unshaded
-        map: ["enum.EffectLayers.Unshaded"]
-        sprite: Effects/emp.rsi
-        state: emp_pulse
+    - shader: unshaded
+      map: ["enum.EffectLayers.Unshaded"]
+      sprite: Effects/emp.rsi
+      state: emp_pulse
   - type: EffectVisuals
   - type: Tag
     tags:
-      - HideContextMenu
-  - type: EmitSoundOnSpawn
-    sound: 
-      path: /Audio/Effects/Lightning/lightningbolt.ogg
+    - HideContextMenu
   - type: AnimationPlayer
 
 - type: entity
     drawdepth: Effects
     noRot: true
     layers:
-      - shader: unshaded
-        map: ["enum.EffectLayers.Unshaded"]
-        sprite: Effects/emp.rsi
-        state: emp_disable
+    - shader: unshaded
+      map: ["enum.EffectLayers.Unshaded"]
+      sprite: Effects/emp.rsi
+      state: emp_disable
   - type: EffectVisuals
   - type: Tag
     tags:
-      - HideContextMenu
+    - HideContextMenu
   - type: AnimationPlayer