Predict PoweredLights (#36541)

* 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>
This commit is contained in:
metalgearsloth
2025-08-12 07:06:28 +10:00
committed by GitHub
parent 3d71ddd1de
commit fd52f698c7
16 changed files with 630 additions and 566 deletions

View 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);
}
}
}

View File

@@ -0,0 +1,5 @@
using Content.Shared.Light.EntitySystems;
namespace Content.Client.Light.EntitySystems;
public sealed class PoweredLightSystem : SharedPoweredLightSystem;

View File

@@ -5,4 +5,5 @@ namespace Content.Client.Power.Components;
[RegisterComponent]
public sealed partial class ApcPowerReceiverComponent : SharedApcPowerReceiverComponent
{
public override float Load { get; set; }
}

View 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;

View 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;

View File

@@ -1,318 +1,32 @@
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
{
[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);
}
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
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;
var time = GameTiming.CurTime;
if (light.LastGhostBlink != null)
{
if (time <= light.LastGhostBlink + light.GhostBlinkingCooldown)
@@ -330,111 +44,21 @@ namespace Content.Server.Light.EntitySystems
args.Handled = true;
}
private void OnPowerChanged(EntityUid uid, PoweredLightComponent component, ref PowerChangedEvent args)
private void OnMapInit(EntityUid uid, PoweredLightComponent light, MapInitEvent args)
{
// TODO: Power moment
var metadata = MetaData(uid);
if (metadata.EntityPaused || TerminatingOrDeleted(uid, metadata))
return;
UpdateLight(uid, component);
// TODO: Use ContainerFill dog
if (light.HasLampOnSpawn != null)
{
var entity = EntityManager.SpawnEntity(light.HasLampOnSpawn, EntityManager.GetComponent<TransformComponent>(uid).Coordinates);
ContainerSystem.Insert(entity, light.LightBulbContainer);
}
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;
SetState(uid, enabled, component);
}
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;
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;
// need this to update visualizers
UpdateLight(uid, light);
}
public void SetState(EntityUid uid, bool state, PoweredLightComponent? light = null)
{
if (!Resolve(uid, ref light))
return;
light.On = state;
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;
}
private void OnEmpPulse(EntityUid uid, PoweredLightComponent component, ref EmpPulseEvent args)
{
if (TryDestroyBulb(uid, component))
args.Affected = true;
}
}
}

View 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;

View 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;

View 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;

View 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;

View 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

View File

@@ -1,3 +1,4 @@
using Content.Shared.Light.Components;
using Content.Shared.Light.EntitySystems;
using Content.Shared.Storage;
using Robust.Shared.Audio;

View 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View 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; }
}