]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Predict PoweredLights (#36541)
authormetalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Mon, 11 Aug 2025 21:06:28 +0000 (07:06 +1000)
committerGitHub <noreply@github.com>
Mon, 11 Aug 2025 21:06:28 +0000 (23:06 +0200)
* Move PoweredLight to shared

* Predict the rest of the owl

* reacher

* compinit & anim

* Fix names

* Revert this?

* Fix these

* chicken drummies

* deita

* Fix

* review

* fix

* fixes

* fix PVS weirdness

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
16 files changed:
Content.Client/Light/EntitySystems/LightBulbSystem.cs
Content.Client/Light/EntitySystems/PoweredLightSystem.cs [new file with mode: 0644]
Content.Client/Power/Components/ApcPowerReceiverComponent.cs
Content.Server/Electrocution/ElectrocutionSystem.cs
Content.Server/Light/EntitySystems/LightBulbSystem.cs
Content.Server/Light/EntitySystems/PoweredLightSystem.cs
Content.Server/Power/Components/ApcPowerReceiverComponent.cs
Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs
Content.Server/StationEvents/Events/SolarFlareRule.cs
Content.Server/Xenoarchaeology/Artifact/XAE/XAELightFlickerSystem.cs
Content.Shared/Light/Components/LightBulbComponent.cs
Content.Shared/Light/Components/LightReplacerComponent.cs
Content.Shared/Light/Components/PoweredLightComponent.cs [moved from Content.Server/Light/Components/PoweredLightComponent.cs with 54% similarity]
Content.Shared/Light/EntitySystems/SharedLightBulbSystem.cs [new file with mode: 0644]
Content.Shared/Light/EntitySystems/SharedPoweredLightSystem.cs [new file with mode: 0644]
Content.Shared/Power/Components/SharedApcPowerReceiverComponent.cs

index c028cc64c6fea9882ee1484bd477a71c0a1022dc..a3698fc199d637be1abe2972151fd41479c77a84 100644 (file)
@@ -1,36 +1,46 @@
 using Content.Shared.Light.Components;
+using Content.Shared.Light.EntitySystems;
 using Robust.Client.GameObjects;
 
-namespace Content.Client.Light.Visualizers;
+namespace Content.Client.Light.EntitySystems;
 
-public sealed class LightBulbSystem : VisualizerSystem<LightBulbComponent>
+public sealed class LightBulbSystem : SharedLightBulbSystem
 {
-    protected override void OnAppearanceChange(EntityUid uid, LightBulbComponent comp, ref AppearanceChangeEvent args)
+    [Dependency] private readonly AppearanceSystem _appearance = default!;
+    [Dependency] private readonly SpriteSystem _sprite = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<LightBulbComponent, AppearanceChangeEvent>(OnAppearanceChange);
+    }
+
+    private void OnAppearanceChange(EntityUid uid, LightBulbComponent comp, ref AppearanceChangeEvent args)
     {
         if (args.Sprite == null)
             return;
 
         // update sprite state
-        if (AppearanceSystem.TryGetData<LightBulbState>(uid, LightBulbVisuals.State, out var state, args.Component))
+        if (_appearance.TryGetData<LightBulbState>(uid, LightBulbVisuals.State, out var state, args.Component))
         {
             switch (state)
             {
                 case LightBulbState.Normal:
-                    SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.NormalSpriteState);
+                    _sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.NormalSpriteState);
                     break;
                 case LightBulbState.Broken:
-                    SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BrokenSpriteState);
+                    _sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BrokenSpriteState);
                     break;
                 case LightBulbState.Burned:
-                    SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BurnedSpriteState);
+                    _sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BurnedSpriteState);
                     break;
             }
         }
 
         // also update sprites color
-        if (AppearanceSystem.TryGetData<Color>(uid, LightBulbVisuals.Color, out var color, args.Component))
+        if (_appearance.TryGetData<Color>(uid, LightBulbVisuals.Color, out var color, args.Component))
         {
-            SpriteSystem.SetColor((uid, args.Sprite), color);
+            _sprite.SetColor((uid, args.Sprite), color);
         }
     }
 }
diff --git a/Content.Client/Light/EntitySystems/PoweredLightSystem.cs b/Content.Client/Light/EntitySystems/PoweredLightSystem.cs
new file mode 100644 (file)
index 0000000..b8a6b16
--- /dev/null
@@ -0,0 +1,5 @@
+using Content.Shared.Light.EntitySystems;
+
+namespace Content.Client.Light.EntitySystems;
+
+public sealed class PoweredLightSystem : SharedPoweredLightSystem;
index fbebcb7cf83a5e040ce77b7a53b255e10b77a903..ead686189ed89954928c85190457bf07b2c2dba8 100644 (file)
@@ -5,4 +5,5 @@ namespace Content.Client.Power.Components;
 [RegisterComponent]
 public sealed partial class ApcPowerReceiverComponent : SharedApcPowerReceiverComponent
 {
+    public override float Load { get; set; }
 }
index 05dfffa4a90fe1e9f2063c325cbaa76211049c54..1f04421c0f29c384bad17f0bf0c068f45898d90c 100644 (file)
@@ -1,5 +1,4 @@
 using Content.Server.Administration.Logs;
-using Content.Server.Light.Components;
 using Content.Server.NodeContainer;
 using Content.Server.NodeContainer.EntitySystems;
 using Content.Server.NodeContainer.NodeGroups;
@@ -16,6 +15,7 @@ using Content.Shared.IdentityManagement;
 using Content.Shared.Interaction;
 using Content.Shared.Inventory;
 using Content.Shared.Jittering;
+using Content.Shared.Light.Components;
 using Content.Shared.Maps;
 using Content.Shared.NodeContainer;
 using Content.Shared.NodeContainer.NodeGroups;
index 5714bde3e5f0c4289b7e609c3c2768a77e80eb92..ba5b795e2d1c68502f56667637f156627d0bed52 100644 (file)
@@ -1,87 +1,5 @@
-using Content.Server.Light.Components;
-using Content.Shared.Destructible;
-using Content.Shared.Light.Components;
-using Content.Shared.Throwing;
-using Robust.Server.GameObjects;
-using Robust.Shared.Audio;
-using Robust.Shared.Audio.Systems;
-using Robust.Shared.Player;
+using Content.Shared.Light.EntitySystems;
 
-namespace Content.Server.Light.EntitySystems
-{
-    public sealed class LightBulbSystem : EntitySystem
-    {
-        [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
-        [Dependency] private readonly SharedAudioSystem _audio = default!;
+namespace Content.Server.Light.EntitySystems;
 
-        public override void Initialize()
-        {
-            base.Initialize();
-
-            SubscribeLocalEvent<LightBulbComponent, ComponentInit>(OnInit);
-            SubscribeLocalEvent<LightBulbComponent, LandEvent>(HandleLand);
-            SubscribeLocalEvent<LightBulbComponent, BreakageEventArgs>(OnBreak);
-        }
-
-        private void OnInit(EntityUid uid, LightBulbComponent bulb, ComponentInit args)
-        {
-            // update default state of bulbs
-            SetColor(uid, bulb.Color, bulb);
-            SetState(uid, bulb.State, bulb);
-        }
-
-        private void HandleLand(EntityUid uid, LightBulbComponent bulb, ref LandEvent args)
-        {
-            PlayBreakSound(uid, bulb);
-            SetState(uid, LightBulbState.Broken, bulb);
-        }
-
-        private void OnBreak(EntityUid uid, LightBulbComponent component, BreakageEventArgs args)
-        {
-            SetState(uid, LightBulbState.Broken, component);
-        }
-
-        /// <summary>
-        ///     Set a new color for a light bulb and raise event about change
-        /// </summary>
-        public void SetColor(EntityUid uid, Color color, LightBulbComponent? bulb = null)
-        {
-            if (!Resolve(uid, ref bulb))
-                return;
-
-            bulb.Color = color;
-            UpdateAppearance(uid, bulb);
-        }
-
-        /// <summary>
-        ///     Set a new state for a light bulb (broken, burned) and raise event about change
-        /// </summary>
-        public void SetState(EntityUid uid, LightBulbState state, LightBulbComponent? bulb = null)
-        {
-            if (!Resolve(uid, ref bulb))
-                return;
-
-            bulb.State = state;
-            UpdateAppearance(uid, bulb);
-        }
-
-        public void PlayBreakSound(EntityUid uid, LightBulbComponent? bulb = null)
-        {
-            if (!Resolve(uid, ref bulb))
-                return;
-
-            _audio.PlayPvs(bulb.BreakSound, uid);
-        }
-
-        private void UpdateAppearance(EntityUid uid, LightBulbComponent? bulb = null,
-            AppearanceComponent? appearance = null)
-        {
-            if (!Resolve(uid, ref bulb, ref appearance, logMissing: false))
-                return;
-
-            // try to update appearance and color
-            _appearance.SetData(uid, LightBulbVisuals.State, bulb.State, appearance);
-            _appearance.SetData(uid, LightBulbVisuals.Color, bulb.Color, appearance);
-        }
-    }
-}
+public sealed class LightBulbSystem : SharedLightBulbSystem;
index b5b9f2043237c2cd33838be5d16e5af9d6551e48..948c44cd7577c5dd7cae3bb0cc07d259a1ed936f 100644 (file)
-using Content.Server.DeviceLinking.Systems;
-using Content.Server.DeviceNetwork;
-using Content.Server.DeviceNetwork.Systems;
 using Content.Server.Emp;
 using Content.Server.Ghost;
-using Content.Server.Light.Components;
-using Content.Server.Power.Components;
-using Content.Shared.Audio;
-using Content.Shared.Damage;
-using Content.Shared.DeviceLinking.Events;
-using Content.Shared.DoAfter;
-using Content.Shared.Hands.EntitySystems;
-using Content.Shared.Interaction;
-using Content.Shared.Light;
 using Content.Shared.Light.Components;
-using Robust.Server.GameObjects;
-using Robust.Shared.Containers;
-using Robust.Shared.Timing;
-using Robust.Shared.Audio.Systems;
-using Content.Shared.Damage.Systems;
-using Content.Shared.Damage.Components;
-using Content.Shared.DeviceNetwork;
-using Content.Shared.DeviceNetwork.Events;
-using Content.Shared.Power;
+using Content.Shared.Light.EntitySystems;
 
-namespace Content.Server.Light.EntitySystems
+namespace Content.Server.Light.EntitySystems;
+
+/// <summary>
+///     System for the PoweredLightComponents
+/// </summary>
+public sealed class PoweredLightSystem : SharedPoweredLightSystem
 {
-    /// <summary>
-    ///     System for the PoweredLightComponents
-    /// </summary>
-    public sealed class PoweredLightSystem : EntitySystem
+    public override void Initialize()
     {
-        [Dependency] private readonly IGameTiming _gameTiming = default!;
-        [Dependency] private readonly SharedAmbientSoundSystem _ambientSystem = default!;
-        [Dependency] private readonly LightBulbSystem _bulbSystem = default!;
-        [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
-        [Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
-        [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
-        [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
-        [Dependency] private readonly SharedAudioSystem _audio = default!;
-        [Dependency] private readonly PointLightSystem _pointLight = default!;
-        [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
-        [Dependency] private readonly DamageOnInteractSystem _damageOnInteractSystem = default!;
-
-        private static readonly TimeSpan ThunkDelay = TimeSpan.FromSeconds(2);
-        public const string LightBulbContainer = "light_bulb";
-
-        public override void Initialize()
-        {
-            base.Initialize();
-            SubscribeLocalEvent<PoweredLightComponent, ComponentInit>(OnInit);
-            SubscribeLocalEvent<PoweredLightComponent, MapInitEvent>(OnMapInit);
-            SubscribeLocalEvent<PoweredLightComponent, InteractUsingEvent>(OnInteractUsing);
-            SubscribeLocalEvent<PoweredLightComponent, InteractHandEvent>(OnInteractHand);
-
-            SubscribeLocalEvent<PoweredLightComponent, GhostBooEvent>(OnGhostBoo);
-            SubscribeLocalEvent<PoweredLightComponent, DamageChangedEvent>(HandleLightDamaged);
-
-            SubscribeLocalEvent<PoweredLightComponent, SignalReceivedEvent>(OnSignalReceived);
-            SubscribeLocalEvent<PoweredLightComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
-
-            SubscribeLocalEvent<PoweredLightComponent, PowerChangedEvent>(OnPowerChanged);
-
-            SubscribeLocalEvent<PoweredLightComponent, PoweredLightDoAfterEvent>(OnDoAfter);
-            SubscribeLocalEvent<PoweredLightComponent, EmpPulseEvent>(OnEmpPulse);
-        }
-
-        private void OnInit(EntityUid uid, PoweredLightComponent light, ComponentInit args)
-        {
-            light.LightBulbContainer = _containerSystem.EnsureContainer<ContainerSlot>(uid, LightBulbContainer);
-            _signalSystem.EnsureSinkPorts(uid, light.OnPort, light.OffPort, light.TogglePort);
-        }
+        base.Initialize();
+        SubscribeLocalEvent<PoweredLightComponent, MapInitEvent>(OnMapInit);
 
-        private void OnMapInit(EntityUid uid, PoweredLightComponent light, MapInitEvent args)
-        {
-            // TODO: Use ContainerFill dog
-            if (light.HasLampOnSpawn != null)
-            {
-                var entity = Spawn(light.HasLampOnSpawn, Comp<TransformComponent>(uid).Coordinates);
-                _containerSystem.Insert(entity, light.LightBulbContainer);
-            }
-            // need this to update visualizers
-            UpdateLight(uid, light);
-        }
-
-        private void OnInteractUsing(EntityUid uid, PoweredLightComponent component, InteractUsingEvent args)
-        {
-            if (args.Handled)
-                return;
-
-            args.Handled = InsertBulb(uid, args.Used, component);
-        }
-
-        private void OnInteractHand(EntityUid uid, PoweredLightComponent light, InteractHandEvent args)
-        {
-            if (args.Handled)
-                return;
-
-            // check if light has bulb to eject
-            var bulbUid = GetBulb(uid, light);
-            if (bulbUid == null)
-                return;
-
-            var userUid = args.User;
-            //removing a broken/burned bulb, so allow instant removal
-            if(TryComp<LightBulbComponent>(bulbUid.Value, out var bulb) && bulb.State != LightBulbState.Normal)
-            {
-                args.Handled = EjectBulb(uid, userUid, light) != null;
-                return;
-            }
-
-            // removing a working bulb, so require a delay
-            _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, userUid, light.EjectBulbDelay, new PoweredLightDoAfterEvent(), uid, target: uid)
-            {
-                BreakOnMove = true,
-                BreakOnDamage = true,
-            });
-
-            args.Handled = true;
-        }
-
-        #region Bulb Logic API
-        /// <summary>
-        ///     Inserts the bulb if possible.
-        /// </summary>
-        /// <returns>True if it could insert it, false if it couldn't.</returns>
-        public bool InsertBulb(EntityUid uid, EntityUid bulbUid, PoweredLightComponent? light = null)
-        {
-            if (!Resolve(uid, ref light))
-                return false;
-
-            // check if light already has bulb
-            if (GetBulb(uid, light) != null)
-                return false;
-
-            // check if bulb fits
-            if (!TryComp(bulbUid, out LightBulbComponent? lightBulb))
-                return false;
-            if (lightBulb.Type != light.BulbType)
-                return false;
-
-            // try to insert bulb in container
-            if (!_containerSystem.Insert(bulbUid, light.LightBulbContainer))
-                return false;
-
-            UpdateLight(uid, light);
-            return true;
-        }
-
-        /// <summary>
-        ///     Ejects the bulb to a mob's hand if possible.
-        /// </summary>
-        /// <returns>Bulb uid if it was successfully ejected, null otherwise</returns>
-        public EntityUid? EjectBulb(EntityUid uid, EntityUid? userUid = null, PoweredLightComponent? light = null)
-        {
-            if (!Resolve(uid, ref light))
-                return null;
-
-            // check if light has bulb
-            if (GetBulb(uid, light) is not { Valid: true } bulb)
-                return null;
-
-            // try to remove bulb from container
-            if (!_containerSystem.Remove(bulb, light.LightBulbContainer))
-                return null;
-
-            // try to place bulb in hands
-            _handsSystem.PickupOrDrop(userUid, bulb);
-
-            UpdateLight(uid, light);
-            return bulb;
-        }
-
-        /// <summary>
-        ///     Replaces the spawned prototype of a pre-mapinit powered light with a different variant.
-        /// </summary>
-        public bool ReplaceSpawnedPrototype(Entity<PoweredLightComponent> light, string bulb)
-        {
-            if (light.Comp.LightBulbContainer.ContainedEntity != null)
-                return false;
-
-            if (LifeStage(light.Owner) >= EntityLifeStage.MapInitialized)
-                return false;
-
-            light.Comp.HasLampOnSpawn = bulb;
-            return true;
-        }
-
-        /// <summary>
-        ///     Try to replace current bulb with a new one
-        ///     If succeed old bulb just drops on floor
-        /// </summary>
-        public bool ReplaceBulb(EntityUid uid, EntityUid bulb, PoweredLightComponent? light = null)
-        {
-            EjectBulb(uid, null, light);
-            return InsertBulb(uid, bulb, light);
-        }
-
-        /// <summary>
-        ///     Try to get light bulb inserted in powered light
-        /// </summary>
-        /// <returns>Bulb uid if it exist, null otherwise</returns>
-        public EntityUid? GetBulb(EntityUid uid, PoweredLightComponent? light = null)
-        {
-            if (!Resolve(uid, ref light))
-                return null;
-
-            return light.LightBulbContainer.ContainedEntity;
-        }
-
-        /// <summary>
-        ///     Try to break bulb inside light fixture
-        /// </summary>
-        public bool TryDestroyBulb(EntityUid uid, PoweredLightComponent? light = 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 || !TryComp(bulbUid.Value, out LightBulbComponent? lightBulb))
-                return false;
-            if (lightBulb.State == LightBulbState.Broken)
-                return false;
-
-            // break it
-            _bulbSystem.SetState(bulbUid.Value, LightBulbState.Broken, lightBulb);
-            _bulbSystem.PlayBreakSound(bulbUid.Value, lightBulb);
-            UpdateLight(uid, light);
-            return true;
-        }
-        #endregion
+        SubscribeLocalEvent<PoweredLightComponent, GhostBooEvent>(OnGhostBoo);
 
-        private void UpdateLight(EntityUid uid,
-            PoweredLightComponent? light = null,
-            ApcPowerReceiverComponent? powerReceiver = null,
-            AppearanceComponent? appearance = null)
-        {
-            if (!Resolve(uid, ref light, ref powerReceiver, false))
-                return;
-
-            // Optional component.
-            Resolve(uid, ref appearance, false);
-
-            // check if light has bulb
-            var bulbUid = GetBulb(uid, light);
-            if (bulbUid == null || !TryComp(bulbUid.Value, out LightBulbComponent? lightBulb))
-            {
-                SetLight(uid, false, light: light);
-                powerReceiver.Load = 0;
-                _appearance.SetData(uid, PoweredLightVisuals.BulbState, PoweredLightState.Empty, appearance);
-                return;
-            }
-
-            switch (lightBulb.State)
-            {
-                case LightBulbState.Normal:
-                    if (powerReceiver.Powered && light.On)
-                    {
-                        SetLight(uid, true, lightBulb.Color, light, lightBulb.LightRadius, lightBulb.LightEnergy, lightBulb.LightSoftness);
-                        _appearance.SetData(uid, PoweredLightVisuals.BulbState, PoweredLightState.On, appearance);
-                        var time = _gameTiming.CurTime;
-                        if (time > light.LastThunk + ThunkDelay)
-                        {
-                            light.LastThunk = time;
-                            _audio.PlayPvs(light.TurnOnSound, uid, light.TurnOnSound.Params.AddVolume(-10f));
-                        }
-                    }
-                    else
-                    {
-                        SetLight(uid, false, light: light);
-                        _appearance.SetData(uid, PoweredLightVisuals.BulbState, PoweredLightState.Off, appearance);
-                    }
-                    break;
-                case LightBulbState.Broken:
-                    SetLight(uid, false, light: light);
-                    _appearance.SetData(uid, PoweredLightVisuals.BulbState, PoweredLightState.Broken, appearance);
-                    break;
-                case LightBulbState.Burned:
-                    SetLight(uid, false, light: light);
-                    _appearance.SetData(uid, PoweredLightVisuals.BulbState, PoweredLightState.Burned, appearance);
-                    break;
-            }
-
-            powerReceiver.Load = (light.On && lightBulb.State == LightBulbState.Normal) ? lightBulb.PowerUse : 0;
-        }
-
-        /// <summary>
-        ///     Destroy the light bulb if the light took any damage.
-        /// </summary>
-        public void HandleLightDamaged(EntityUid uid, PoweredLightComponent component, DamageChangedEvent args)
-        {
-            // 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);
-            }
-        }
-
-        private void OnGhostBoo(EntityUid uid, PoweredLightComponent light, GhostBooEvent args)
-        {
-            if (light.IgnoreGhostsBoo)
-                return;
-
-            // check cooldown first to prevent abuse
-            var time = _gameTiming.CurTime;
-            if (light.LastGhostBlink != null)
-            {
-                if (time <= light.LastGhostBlink + light.GhostBlinkingCooldown)
-                    return;
-            }
-
-            light.LastGhostBlink = time;
-
-            ToggleBlinkingLight(uid, light, true);
-            uid.SpawnTimer(light.GhostBlinkingTime, () =>
-            {
-                ToggleBlinkingLight(uid, light, false);
-            });
-
-            args.Handled = true;
-        }
-
-        private void OnPowerChanged(EntityUid uid, PoweredLightComponent component, ref PowerChangedEvent args)
-        {
-            // TODO: Power moment
-            var metadata = MetaData(uid);
-
-            if (metadata.EntityPaused || TerminatingOrDeleted(uid, metadata))
-                return;
-
-            UpdateLight(uid, component);
-        }
-
-        public void ToggleBlinkingLight(EntityUid uid, PoweredLightComponent light, bool isNowBlinking)
-        {
-            if (light.IsBlinking == isNowBlinking)
-                return;
-
-            light.IsBlinking = isNowBlinking;
-
-            if (!TryComp(uid, out AppearanceComponent? appearance))
-                return;
-
-            _appearance.SetData(uid, PoweredLightVisuals.Blinking, isNowBlinking, appearance);
-        }
-
-        private void OnSignalReceived(EntityUid uid, PoweredLightComponent component, ref SignalReceivedEvent args)
-        {
-            if (args.Port == component.OffPort)
-                SetState(uid, false, component);
-            else if (args.Port == component.OnPort)
-                SetState(uid, true, component);
-            else if (args.Port == component.TogglePort)
-                ToggleLight(uid, component);
-        }
-
-        /// <summary>
-        /// Turns the light on or of when receiving a <see cref="DeviceNetworkConstants.CmdSetState"/> command.
-        /// The light is turned on or of according to the <see cref="DeviceNetworkConstants.StateEnabled"/> value
-        /// </summary>
-        private void OnPacketReceived(EntityUid uid, PoweredLightComponent component, DeviceNetworkPacketEvent args)
-        {
-            if (!args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? command) || command != DeviceNetworkConstants.CmdSetState) return;
-            if (!args.Data.TryGetValue(DeviceNetworkConstants.StateEnabled, out bool enabled)) return;
+        SubscribeLocalEvent<PoweredLightComponent, EmpPulseEvent>(OnEmpPulse);
+    }
 
-            SetState(uid, enabled, component);
-        }
+    private void OnGhostBoo(EntityUid uid, PoweredLightComponent light, GhostBooEvent args)
+    {
+        if (light.IgnoreGhostsBoo)
+            return;
 
-        private void SetLight(EntityUid uid, bool value, Color? color = null, PoweredLightComponent? light = null, float? radius = null, float? energy = null, float? softness = null)
+        // check cooldown first to prevent abuse
+        var time = GameTiming.CurTime;
+        if (light.LastGhostBlink != null)
         {
-            if (!Resolve(uid, ref light))
+            if (time <= light.LastGhostBlink + light.GhostBlinkingCooldown)
                 return;
-
-            light.CurrentLit = value;
-            _ambientSystem.SetAmbience(uid, value);
-
-            if (TryComp(uid, out PointLightComponent? pointLight))
-            {
-                _pointLight.SetEnabled(uid, value, pointLight);
-
-                if (color != null)
-                    _pointLight.SetColor(uid, color.Value, pointLight);
-                if (radius != null)
-                    _pointLight.SetRadius(uid, (float) radius, pointLight);
-                if (energy != null)
-                    _pointLight.SetEnergy(uid, (float) energy, pointLight);
-                if (softness != null)
-                    _pointLight.SetSoftness(uid, (float) softness, pointLight);
-            }
-
-            // light bulbs burn your hands!
-            if (TryComp<DamageOnInteractComponent>(uid, out var damageOnInteractComp))
-                _damageOnInteractSystem.SetIsDamageActiveTo((uid, damageOnInteractComp), value);
         }
 
-        public void ToggleLight(EntityUid uid, PoweredLightComponent? light = null)
-        {
-            if (!Resolve(uid, ref light))
-                return;
-
-            light.On = !light.On;
-            UpdateLight(uid, light);
-        }
+        light.LastGhostBlink = time;
 
-        public void SetState(EntityUid uid, bool state, PoweredLightComponent? light = null)
+        ToggleBlinkingLight(uid, light, true);
+        uid.SpawnTimer(light.GhostBlinkingTime, () =>
         {
-            if (!Resolve(uid, ref light))
-                return;
+            ToggleBlinkingLight(uid, light, false);
+        });
 
-            light.On = state;
-            UpdateLight(uid, light);
-        }
+        args.Handled = true;
+    }
 
-        private void OnDoAfter(EntityUid uid, PoweredLightComponent component, DoAfterEvent args)
+    private void OnMapInit(EntityUid uid, PoweredLightComponent light, MapInitEvent args)
+    {
+        // TODO: Use ContainerFill dog
+        if (light.HasLampOnSpawn != null)
         {
-            if (args.Handled || args.Cancelled || args.Args.Target == null)
-                return;
-
-            EjectBulb(args.Args.Target.Value, args.Args.User, component);
-
-            args.Handled = true;
+            var entity = EntityManager.SpawnEntity(light.HasLampOnSpawn, EntityManager.GetComponent<TransformComponent>(uid).Coordinates);
+            ContainerSystem.Insert(entity, light.LightBulbContainer);
         }
+        // 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;
-        }
+    private void OnEmpPulse(EntityUid uid, PoweredLightComponent component, ref EmpPulseEvent args)
+    {
+        if (TryDestroyBulb(uid, component))
+            args.Affected = true;
     }
 }
index aa8feff3e033e47febd9a983141cad32118789b4..bfd096a2539940612abeb6b9b24cf2494a54d732 100644 (file)
@@ -14,9 +14,12 @@ namespace Content.Server.Power.Components
         /// <summary>
         ///     Amount of charge this needs from an APC per second to function.
         /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
         [DataField("powerLoad")]
-        public float Load { get => NetworkLoad.DesiredPower; set => NetworkLoad.DesiredPower = value; }
+        public override float Load
+        {
+            get => NetworkLoad.DesiredPower;
+            set => NetworkLoad.DesiredPower = value;
+        }
 
         public ApcPowerProviderComponent? Provider = null;
 
index 0ef7b8f5332fde56408046e2f341c9ece7dd981f..3f91b0fa71fa0c54db57e3dfe74d53b5ed7801f7 100644 (file)
@@ -20,6 +20,7 @@ using Content.Shared.DoAfter;
 using Content.Shared.Emag.Systems;
 using Content.Shared.FixedPoint;
 using Content.Shared.Humanoid;
+using Content.Shared.Light.Components;
 using Content.Shared.Maps;
 using Content.Shared.Mobs;
 using Content.Shared.Mobs.Components;
index 19f6e393d20149f039f471c4b1f72843ce1df88b..b6530d867fa3ba45d935df39d5170e5d94589c75 100644 (file)
@@ -8,6 +8,7 @@ using Content.Shared.Radio.Components;
 using Content.Shared.Doors.Components;
 using Content.Shared.Doors.Systems;
 using Content.Shared.GameTicking.Components;
+using Content.Shared.Light.Components;
 
 namespace Content.Server.StationEvents.Events;
 
index 4c4073b123191f8882549b97cbdd9adecc54761f..6e7c4fc4ad9b7ed7381105b1e0b28b04a72c3223 100644 (file)
@@ -1,6 +1,6 @@
 using Content.Server.Ghost;
-using Content.Server.Light.Components;
 using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
+using Content.Shared.Light.Components;
 using Content.Shared.Xenoarchaeology.Artifact;
 using Content.Shared.Xenoarchaeology.Artifact.XAE;
 using Robust.Shared.Random;
index 35b04be89743e75476eaa59ccf6db86ce5dc1c39..fcd3840a0cb514ca70d7163bb17d1f3a5ad74ebe 100644 (file)
@@ -8,69 +8,61 @@ namespace Content.Shared.Light.Components;
 /// Component that represents a light bulb. Can be broken, or burned, which turns them mostly useless.
 /// TODO: Breaking and burning should probably be moved to another component eventually.
 /// </summary>
-[RegisterComponent, NetworkedComponent]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
 public sealed partial class LightBulbComponent : Component
 {
     /// <summary>
     /// The color of the lightbulb and the light it produces.
     /// </summary>
-    [DataField("color")]
-    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField, AutoNetworkedField]
     public Color Color = Color.White;
 
     /// <summary>
     /// The type of lightbulb. Tube/bulb/etc...
     /// </summary>
     [DataField("bulb")]
-    [ViewVariables(VVAccess.ReadWrite)]
     public LightBulbType Type = LightBulbType.Tube;
 
     /// <summary>
     /// The initial state of the lightbulb.
     /// </summary>
-    [DataField("startingState")]
+    [DataField("startingState"), AutoNetworkedField]
     public LightBulbState State = LightBulbState.Normal;
 
     /// <summary>
     /// The temperature the air around the lightbulb is exposed to when the lightbulb burns out.
     /// </summary>
     [DataField("BurningTemperature")]
-    [ViewVariables(VVAccess.ReadWrite)]
     public int BurningTemperature = 1400;
 
     /// <summary>
     /// Relates to how bright the light produced by the lightbulb is.
     /// </summary>
-    [DataField("lightEnergy")]
-    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public float LightEnergy = 0.8f;
 
     /// <summary>
     /// The maximum radius of the point light source this light produces.
     /// </summary>
-    [DataField("lightRadius")]
-    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public float LightRadius = 10;
 
     /// <summary>
     /// Relates to the falloff constant of the light produced by the lightbulb.
     /// </summary>
-    [DataField("lightSoftness")]
-    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public float LightSoftness = 1;
 
     /// <summary>
     /// The amount of power used by the lightbulb when it's active.
     /// </summary>
     [DataField("PowerUse")]
-    [ViewVariables(VVAccess.ReadWrite)]
     public int PowerUse = 60;
 
     /// <summary>
     /// The sound produced when the lightbulb breaks.
     /// </summary>
-    [DataField("breakSound")]
-    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public SoundSpecifier BreakSound = new SoundCollectionSpecifier("GlassBreak", AudioParams.Default.WithVolume(-6f));
 
     #region Appearance
@@ -78,22 +70,19 @@ public sealed partial class LightBulbComponent : Component
     /// <summary>
     /// The sprite state used when the lightbulb is intact.
     /// </summary>
-    [DataField("normalSpriteState")]
-    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public string NormalSpriteState = "normal";
 
     /// <summary>
     /// The sprite state used when the lightbulb is broken.
     /// </summary>
-    [DataField("brokenSpriteState")]
-    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public string BrokenSpriteState = "broken";
 
     /// <summary>
     /// The sprite state used when the lightbulb is burned.
     /// </summary>
-    [DataField("burnedSpriteState")]
-    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public string BurnedSpriteState = "burned";
 
     #endregion Appearance
index 3ce9647c884324cc8ef889803ab4f68fc463eef3..1276ff9edc7f4773e25d7cb9f6dd40d9097b8deb 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Shared.Light.Components;
 using Content.Shared.Light.EntitySystems;
 using Content.Shared.Storage;
 using Robust.Shared.Audio;
similarity index 54%
rename from Content.Server/Light/Components/PoweredLightComponent.cs
rename to Content.Shared/Light/Components/PoweredLightComponent.cs
index 1a6f610516e815a1c1ff68df451fac203578f821..ad9a9d401c31fd1bbb898c6cc889292799d228b3 100644 (file)
@@ -1,80 +1,87 @@
-using Content.Server.Light.EntitySystems;
-using Content.Shared.Damage;
 using Content.Shared.DeviceLinking;
-using Content.Shared.Light.Components;
+using Content.Shared.Light.EntitySystems;
 using Robust.Shared.Audio;
 using Robust.Shared.Containers;
+using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
 
-namespace Content.Server.Light.Components
+namespace Content.Shared.Light.Components
 {
     /// <summary>
     ///     Component that represents a wall light. It has a light bulb that can be replaced when broken.
     /// </summary>
-    [RegisterComponent, Access(typeof(PoweredLightSystem))]
+    [RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause, Access(typeof(SharedPoweredLightSystem))]
     public sealed partial class PoweredLightComponent : Component
     {
-        [DataField("burnHandSound")]
+        /*
+         * Stop adding more fields, use components or I will shed you.
+         */
+
+        [DataField]
         public SoundSpecifier BurnHandSound = new SoundPathSpecifier("/Audio/Effects/lightburn.ogg");
 
-        [DataField("turnOnSound")]
+        [DataField]
         public SoundSpecifier TurnOnSound = new SoundPathSpecifier("/Audio/Machines/light_tube_on.ogg");
 
-        [DataField("hasLampOnSpawn", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
-        public string? HasLampOnSpawn = null;
+        // Should be using containerfill?
+        [DataField]
+        public EntProtoId? HasLampOnSpawn = null;
 
         [DataField("bulb")]
         public LightBulbType BulbType;
 
-        [DataField("on")]
+        [DataField, AutoNetworkedField]
         public bool On = true;
 
-        [DataField("ignoreGhostsBoo")]
+        [DataField]
         public bool IgnoreGhostsBoo;
 
-        [DataField("ghostBlinkingTime")]
+        [DataField]
         public TimeSpan GhostBlinkingTime = TimeSpan.FromSeconds(10);
 
-        [DataField("ghostBlinkingCooldown")]
+        [DataField]
         public TimeSpan GhostBlinkingCooldown = TimeSpan.FromSeconds(60);
 
         [ViewVariables]
         public ContainerSlot LightBulbContainer = default!;
-        [ViewVariables]
+
+        [AutoNetworkedField]
         public bool CurrentLit;
-        [ViewVariables]
+
+        [DataField, AutoNetworkedField]
         public bool IsBlinking;
-        [ViewVariables]
+
+        [DataField, AutoNetworkedField, AutoPausedField]
         public TimeSpan LastThunk;
-        [ViewVariables]
+
+        [DataField, AutoPausedField]
         public TimeSpan? LastGhostBlink;
 
-        [DataField("onPort", customTypeSerializer: typeof(PrototypeIdSerializer<SinkPortPrototype>))]
-        public string OnPort = "On";
+        [DataField]
+        public ProtoId<SinkPortPrototype> OnPort = "On";
 
-        [DataField("offPort", customTypeSerializer: typeof(PrototypeIdSerializer<SinkPortPrototype>))]
-        public string OffPort = "Off";
+        [DataField]
+        public ProtoId<SinkPortPrototype> OffPort = "Off";
 
-        [DataField("togglePort", customTypeSerializer: typeof(PrototypeIdSerializer<SinkPortPrototype>))]
-        public string TogglePort = "Toggle";
+        [DataField]
+        public ProtoId<SinkPortPrototype> TogglePort = "Toggle";
 
         /// <summary>
         /// How long it takes to eject a bulb from this
         /// </summary>
-        [DataField("ejectBulbDelay")]
+        [DataField]
         public float EjectBulbDelay = 2;
 
         /// <summary>
         /// Shock damage done to a mob that hits the light with an unarmed attack
         /// </summary>
-        [DataField("unarmedHitShock")]
+        [DataField]
         public int UnarmedHitShock = 20;
 
         /// <summary>
         /// Stun duration applied to a mob that hits the light with an unarmed attack
         /// </summary>
-        [DataField("unarmedHitStun")]
+        [DataField]
         public TimeSpan UnarmedHitStun = TimeSpan.FromSeconds(5);
     }
 }
diff --git a/Content.Shared/Light/EntitySystems/SharedLightBulbSystem.cs b/Content.Shared/Light/EntitySystems/SharedLightBulbSystem.cs
new file mode 100644 (file)
index 0000000..34c986b
--- /dev/null
@@ -0,0 +1,84 @@
+using Content.Shared.Destructible;
+using Content.Shared.Light.Components;
+using Content.Shared.Throwing;
+using Robust.Shared.Audio.Systems;
+
+namespace Content.Shared.Light.EntitySystems;
+
+public abstract class SharedLightBulbSystem : EntitySystem
+{
+    [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<LightBulbComponent, ComponentInit>(OnInit);
+        SubscribeLocalEvent<LightBulbComponent, LandEvent>(HandleLand);
+        SubscribeLocalEvent<LightBulbComponent, BreakageEventArgs>(OnBreak);
+    }
+
+    private void OnInit(EntityUid uid, LightBulbComponent bulb, ComponentInit args)
+    {
+        // update default state of bulbs
+        SetColor(uid, bulb.Color, bulb);
+        SetState(uid, bulb.State, bulb);
+    }
+
+    private void HandleLand(EntityUid uid, LightBulbComponent bulb, ref LandEvent args)
+    {
+        PlayBreakSound(uid, bulb);
+        SetState(uid, LightBulbState.Broken, bulb);
+    }
+
+    private void OnBreak(EntityUid uid, LightBulbComponent component, BreakageEventArgs args)
+    {
+        SetState(uid, LightBulbState.Broken, component);
+    }
+
+    /// <summary>
+    ///     Set a new color for a light bulb and raise event about change
+    /// </summary>
+    public void SetColor(EntityUid uid, Color color, LightBulbComponent? bulb = null)
+    {
+        if (!Resolve(uid, ref bulb) || bulb.Color.Equals(color))
+            return;
+
+        bulb.Color = color;
+        Dirty(uid, bulb);
+        UpdateAppearance(uid, bulb);
+    }
+
+    /// <summary>
+    ///     Set a new state for a light bulb (broken, burned) and raise event about change
+    /// </summary>
+    public void SetState(EntityUid uid, LightBulbState state, LightBulbComponent? bulb = null)
+    {
+        if (!Resolve(uid, ref bulb) || bulb.State == state)
+            return;
+
+        bulb.State = state;
+        Dirty(uid, bulb);
+        UpdateAppearance(uid, bulb);
+    }
+
+    public void PlayBreakSound(EntityUid uid, LightBulbComponent? bulb = null, EntityUid? user = null)
+    {
+        if (!Resolve(uid, ref bulb))
+            return;
+
+        _audio.PlayPredicted(bulb.BreakSound, uid, user: user);
+    }
+
+    private void UpdateAppearance(EntityUid uid, LightBulbComponent? bulb = null,
+        AppearanceComponent? appearance = null)
+    {
+        if (!Resolve(uid, ref bulb, ref appearance, logMissing: false))
+            return;
+
+        // try to update appearance and color
+        _appearance.SetData(uid, LightBulbVisuals.State, bulb.State, appearance);
+        _appearance.SetData(uid, LightBulbVisuals.Color, bulb.Color, appearance);
+    }
+}
diff --git a/Content.Shared/Light/EntitySystems/SharedPoweredLightSystem.cs b/Content.Shared/Light/EntitySystems/SharedPoweredLightSystem.cs
new file mode 100644 (file)
index 0000000..e1dc4be
--- /dev/null
@@ -0,0 +1,417 @@
+using Content.Shared.Audio;
+using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
+using Content.Shared.Damage.Systems;
+using Content.Shared.DeviceLinking;
+using Content.Shared.DeviceLinking.Events;
+using Content.Shared.DeviceNetwork;
+using Content.Shared.DeviceNetwork.Events;
+using Content.Shared.DoAfter;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.Interaction;
+using Content.Shared.Light.Components;
+using Content.Shared.Power;
+using Content.Shared.Power.Components;
+using Content.Shared.Power.EntitySystems;
+using Content.Shared.Storage.EntitySystems;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Containers;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.Light.EntitySystems;
+
+public abstract class SharedPoweredLightSystem : EntitySystem
+{
+    [Dependency] protected readonly IGameTiming GameTiming = default!;
+    [Dependency] private readonly DamageOnInteractSystem _damageOnInteractSystem = default!;
+    [Dependency] private readonly SharedAmbientSoundSystem _ambientSystem = default!;
+    [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] protected readonly SharedContainerSystem ContainerSystem = default!;
+    [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
+    [Dependency] private readonly SharedLightBulbSystem _bulbSystem = default!;
+    [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
+    [Dependency] private readonly SharedPowerReceiverSystem _receiver = default!;
+    [Dependency] private readonly SharedPointLightSystem _pointLight = default!;
+    [Dependency] private readonly SharedStorageSystem _storage = default!;
+    [Dependency] private readonly SharedDeviceLinkSystem _deviceLink = default!;
+
+    private static readonly TimeSpan ThunkDelay = TimeSpan.FromSeconds(2);
+    public const string LightBulbContainer = "light_bulb";
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<PoweredLightComponent, ComponentInit>(OnInit);
+        SubscribeLocalEvent<PoweredLightComponent, EntRemovedFromContainerMessage>(OnRemoved);
+        SubscribeLocalEvent<PoweredLightComponent, EntInsertedIntoContainerMessage>(OnInserted);
+        SubscribeLocalEvent<PoweredLightComponent, InteractUsingEvent>(OnInteractUsing);
+        SubscribeLocalEvent<PoweredLightComponent, InteractHandEvent>(OnInteractHand);
+        SubscribeLocalEvent<PoweredLightComponent, SignalReceivedEvent>(OnSignalReceived);
+        SubscribeLocalEvent<PoweredLightComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
+        SubscribeLocalEvent<PoweredLightComponent, PowerChangedEvent>(OnPowerChanged);
+        SubscribeLocalEvent<PoweredLightComponent, PoweredLightDoAfterEvent>(OnDoAfter);
+        SubscribeLocalEvent<PoweredLightComponent, DamageChangedEvent>(HandleLightDamaged);
+    }
+
+    private void OnInit(EntityUid uid, PoweredLightComponent light, ComponentInit args)
+    {
+        light.LightBulbContainer = ContainerSystem.EnsureContainer<ContainerSlot>(uid, LightBulbContainer);
+        _deviceLink.EnsureSinkPorts(uid, light.OnPort, light.OffPort, light.TogglePort);
+    }
+
+    private void OnRemoved(Entity<PoweredLightComponent> light, ref EntRemovedFromContainerMessage args)
+    {
+        if (args.Container.ID != LightBulbContainer)
+            return;
+
+        UpdateLight(light, light);
+    }
+
+    private void OnInserted(Entity<PoweredLightComponent> light, ref EntInsertedIntoContainerMessage args)
+    {
+        if (args.Container.ID != LightBulbContainer)
+            return;
+
+        UpdateLight(light, light);
+    }
+
+    private void OnInteractUsing(EntityUid uid, PoweredLightComponent component, InteractUsingEvent args)
+    {
+        if (args.Handled)
+            return;
+
+        args.Handled = InsertBulb(uid, args.Used, component, user: args.User, playAnimation: true);
+    }
+
+    private void OnInteractHand(EntityUid uid, PoweredLightComponent light, InteractHandEvent args)
+    {
+        if (args.Handled)
+            return;
+
+        // check if light has bulb to eject
+        var bulbUid = GetBulb(uid, light);
+        if (bulbUid == null)
+            return;
+
+        var userUid = args.User;
+        //removing a broken/burned bulb, so allow instant removal
+        if (TryComp<LightBulbComponent>(bulbUid.Value, out var bulb) && bulb.State != LightBulbState.Normal)
+        {
+            args.Handled = EjectBulb(uid, userUid, light) != null;
+            return;
+        }
+
+        // removing a working bulb, so require a delay
+        _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, userUid, light.EjectBulbDelay, new PoweredLightDoAfterEvent(), uid, target: uid)
+        {
+            BreakOnMove = true,
+            BreakOnDamage = true,
+        });
+
+        args.Handled = true;
+    }
+
+    private void OnSignalReceived(Entity<PoweredLightComponent> ent, ref SignalReceivedEvent args)
+    {
+        if (args.Port == ent.Comp.OffPort)
+            SetState(ent, false, ent.Comp);
+        else if (args.Port == ent.Comp.OnPort)
+            SetState(ent, true, ent.Comp);
+        else if (args.Port == ent.Comp.TogglePort)
+            ToggleLight(ent, ent.Comp);
+    }
+
+    /// <summary>
+    /// Turns the light on or of when receiving a <see cref="DeviceNetworkConstants.CmdSetState"/> command.
+    /// The light is turned on or of according to the <see cref="DeviceNetworkConstants.StateEnabled"/> value
+    /// </summary>
+    private void OnPacketReceived(EntityUid uid, PoweredLightComponent component, DeviceNetworkPacketEvent args)
+    {
+        if (!args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? command) || command != DeviceNetworkConstants.CmdSetState) return;
+        if (!args.Data.TryGetValue(DeviceNetworkConstants.StateEnabled, out bool enabled)) return;
+
+        SetState(uid, enabled, component);
+    }
+
+    /// <summary>
+    ///     Inserts the bulb if possible.
+    /// </summary>
+    /// <returns>True if it could insert it, false if it couldn't.</returns>
+    public bool InsertBulb(EntityUid uid, EntityUid bulbUid, PoweredLightComponent? light = null, EntityUid? user = null, bool playAnimation = false)
+    {
+        if (!Resolve(uid, ref light))
+            return false;
+
+        // check if light already has bulb
+        if (GetBulb(uid, light) != null)
+            return false;
+
+        // check if bulb fits
+        if (!TryComp<LightBulbComponent>(bulbUid, out var lightBulb))
+            return false;
+
+        if (lightBulb.Type != light.BulbType)
+            return false;
+
+        // try to insert bulb in container
+        if (!ContainerSystem.Insert(bulbUid, light.LightBulbContainer))
+            return false;
+
+        if (playAnimation && TryComp(user, out TransformComponent? xform))
+        {
+            var itemXform = Transform(uid);
+            _storage.PlayPickupAnimation(bulbUid, xform.Coordinates, itemXform.Coordinates, itemXform.LocalRotation, user: user);
+        }
+
+        return true;
+    }
+
+    /// <summary>
+    ///     Ejects the bulb to a mob's hand if possible.
+    /// </summary>
+    /// <returns>Bulb uid if it was successfully ejected, null otherwise</returns>
+    public EntityUid? EjectBulb(EntityUid uid, EntityUid? userUid = null, PoweredLightComponent? light = null)
+    {
+        if (!Resolve(uid, ref light))
+            return null;
+
+        // check if light has bulb
+        if (GetBulb(uid, light) is not { Valid: true } bulb)
+            return null;
+
+        // try to remove bulb from container
+        if (!ContainerSystem.Remove(bulb, light.LightBulbContainer))
+            return null;
+
+        // try to place bulb in hands
+        _handsSystem.PickupOrDrop(userUid, bulb);
+
+        return bulb;
+    }
+
+    /// <summary>
+    ///     Replaces the spawned prototype of a pre-mapinit powered light with a different variant.
+    /// </summary>
+    public bool ReplaceSpawnedPrototype(Entity<PoweredLightComponent> light, string bulb)
+    {
+        if (light.Comp.LightBulbContainer.ContainedEntity != null)
+            return false;
+
+        if (LifeStage(light.Owner) >= EntityLifeStage.MapInitialized)
+            return false;
+
+        light.Comp.HasLampOnSpawn = bulb;
+        return true;
+    }
+
+    /// <summary>
+    ///     Try to replace current bulb with a new one
+    ///     If succeed old bulb just drops on floor
+    /// </summary>
+    public bool ReplaceBulb(EntityUid uid, EntityUid bulb, PoweredLightComponent? light = null)
+    {
+        EjectBulb(uid, null, light);
+        return InsertBulb(uid, bulb, light);
+    }
+
+    /// <summary>
+    ///     Try to get light bulb inserted in powered light
+    /// </summary>
+    /// <returns>Bulb uid if it exist, null otherwise</returns>
+    public EntityUid? GetBulb(EntityUid uid, PoweredLightComponent? light = null)
+    {
+        if (!Resolve(uid, ref light))
+            return null;
+
+        return light.LightBulbContainer?.ContainedEntity;
+    }
+
+    /// <summary>
+    ///     Try to break bulb inside light fixture
+    /// </summary>
+    public bool TryDestroyBulb(EntityUid uid, PoweredLightComponent? light = 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))
+            return false;
+        if (lightBulb.State == LightBulbState.Broken)
+            return false;
+
+        // break it
+        _bulbSystem.SetState(bulbUid.Value, LightBulbState.Broken, lightBulb);
+        _bulbSystem.PlayBreakSound(bulbUid.Value, lightBulb);
+        UpdateLight(uid, light);
+        return true;
+    }
+
+    protected void UpdateLight(EntityUid uid,
+        PoweredLightComponent? light = null,
+        SharedApcPowerReceiverComponent? powerReceiver = null,
+        AppearanceComponent? appearance = null,
+        EntityUid? user = null)
+    {
+        if (!Resolve(uid, ref light, false))
+            return;
+
+        if (!_receiver.ResolveApc(uid, ref powerReceiver))
+            return;
+
+        // Optional component.
+        Resolve(uid, ref appearance, false);
+
+        // check if light has bulb
+        var bulbUid = GetBulb(uid, light);
+        if (bulbUid == null || !TryComp<LightBulbComponent>(bulbUid.Value, out var lightBulb))
+        {
+            SetLight(uid, false, light: light);
+            powerReceiver.Load = 0;
+            _appearance.SetData(uid, PoweredLightVisuals.BulbState, PoweredLightState.Empty, appearance);
+            return;
+        }
+
+        switch (lightBulb.State)
+        {
+            case LightBulbState.Normal:
+                if (powerReceiver.Powered && light.On)
+                {
+                    SetLight(uid, true, lightBulb.Color, light, lightBulb.LightRadius, lightBulb.LightEnergy, lightBulb.LightSoftness);
+                    _appearance.SetData(uid, PoweredLightVisuals.BulbState, PoweredLightState.On, appearance);
+                    var time = GameTiming.CurTime;
+                    if (time > light.LastThunk + ThunkDelay)
+                    {
+                        light.LastThunk = time;
+                        Dirty(uid, light);
+                        _audio.PlayPredicted(light.TurnOnSound, uid, user: user, light.TurnOnSound.Params.AddVolume(-10f));
+                    }
+                }
+                else
+                {
+                    SetLight(uid, false, light: light);
+                    _appearance.SetData(uid, PoweredLightVisuals.BulbState, PoweredLightState.Off, appearance);
+                }
+                break;
+            case LightBulbState.Broken:
+                SetLight(uid, false, light: light);
+                _appearance.SetData(uid, PoweredLightVisuals.BulbState, PoweredLightState.Broken, appearance);
+                break;
+            case LightBulbState.Burned:
+                SetLight(uid, false, light: light);
+                _appearance.SetData(uid, PoweredLightVisuals.BulbState, PoweredLightState.Burned, appearance);
+                break;
+        }
+
+        powerReceiver.Load = (light.On && lightBulb.State == LightBulbState.Normal) ? lightBulb.PowerUse : 0;
+    }
+
+    /// <summary>
+    ///     Destroy the light bulb if the light took any damage.
+    /// </summary>
+    public void HandleLightDamaged(EntityUid uid, PoweredLightComponent component, DamageChangedEvent args)
+    {
+        // 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);
+        }
+    }
+
+    private void OnPowerChanged(EntityUid uid, PoweredLightComponent component, ref PowerChangedEvent args)
+    {
+        // TODO: Power moment
+        var metadata = MetaData(uid);
+
+        if (metadata.EntityPaused || TerminatingOrDeleted(uid, metadata))
+            return;
+
+        UpdateLight(uid, component);
+    }
+
+    public void ToggleBlinkingLight(EntityUid uid, PoweredLightComponent light, bool isNowBlinking)
+    {
+        if (light.IsBlinking == isNowBlinking)
+            return;
+
+        light.IsBlinking = isNowBlinking;
+        Dirty(uid, light);
+
+        if (!TryComp<AppearanceComponent>(uid, out var appearance))
+            return;
+
+        _appearance.SetData(uid, PoweredLightVisuals.Blinking, isNowBlinking, appearance);
+    }
+
+    private void SetLight(EntityUid uid, bool value, Color? color = null, PoweredLightComponent? light = null, float? radius = null, float? energy = null, float? softness = null)
+    {
+        if (!Resolve(uid, ref light))
+            return;
+
+        if (light.CurrentLit != value)
+        {
+            light.CurrentLit = value;
+            Dirty(uid, light);
+        }
+
+        _ambientSystem.SetAmbience(uid, value);
+
+        if (_pointLight.TryGetLight(uid, out var pointLight))
+        {
+            _pointLight.SetEnabled(uid, value, pointLight);
+
+            if (color != null)
+                _pointLight.SetColor(uid, color.Value, pointLight);
+            if (radius != null)
+                _pointLight.SetRadius(uid, (float)radius, pointLight);
+            if (energy != null)
+                _pointLight.SetEnergy(uid, (float)energy, pointLight);
+            if (softness != null)
+                _pointLight.SetSoftness(uid, (float)softness, pointLight);
+        }
+
+        // light bulbs burn your hands!
+        if (TryComp<DamageOnInteractComponent>(uid, out var damageOnInteractComp))
+            _damageOnInteractSystem.SetIsDamageActiveTo((uid, damageOnInteractComp), value);
+    }
+
+    public void ToggleLight(EntityUid uid, PoweredLightComponent? light = null)
+    {
+        if (!Resolve(uid, ref light))
+            return;
+
+        light.On = !light.On;
+        UpdateLight(uid, light);
+    }
+
+    public void SetState(EntityUid uid, bool state, PoweredLightComponent? light = null)
+    {
+        if (!Resolve(uid, ref light))
+            return;
+
+        light.On = state;
+        Dirty(uid, light);
+        UpdateLight(uid, light);
+    }
+
+    private void OnDoAfter(EntityUid uid, PoweredLightComponent component, DoAfterEvent args)
+    {
+        if (args.Handled || args.Cancelled || args.Args.Target == null)
+            return;
+
+        EjectBulb(args.Args.Target.Value, args.Args.User, component);
+
+        args.Handled = true;
+    }
+}
index 80bacd24bd609a8286be9dd95d9c82e955a0cf76..96189a92aec993f89ec84d39afbee0dee6c12e78 100644 (file)
@@ -19,4 +19,7 @@ public abstract partial class SharedApcPowerReceiverComponent : Component
     /// </summary>
     [ViewVariables(VVAccess.ReadWrite)]
     public virtual bool PowerDisabled { get; set; }
+
+    // Doesn't actually do anything on the client just here for shared code.
+    public abstract float Load { get; set; }
 }