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.Components;
using Content.Shared.Light.EntitySystems;
using Robust.Client.GameObjects; 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) if (args.Sprite == null)
return; return;
// update sprite state // 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) switch (state)
{ {
case LightBulbState.Normal: case LightBulbState.Normal:
SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.NormalSpriteState); _sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.NormalSpriteState);
break; break;
case LightBulbState.Broken: case LightBulbState.Broken:
SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BrokenSpriteState); _sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BrokenSpriteState);
break; break;
case LightBulbState.Burned: case LightBulbState.Burned:
SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BurnedSpriteState); _sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BurnedSpriteState);
break; break;
} }
} }
// also update sprites color // 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] [RegisterComponent]
public sealed partial class ApcPowerReceiverComponent : SharedApcPowerReceiverComponent 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.Administration.Logs;
using Content.Server.Light.Components;
using Content.Server.NodeContainer; using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.NodeGroups; using Content.Server.NodeContainer.NodeGroups;
@@ -16,6 +15,7 @@ using Content.Shared.IdentityManagement;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Inventory; using Content.Shared.Inventory;
using Content.Shared.Jittering; using Content.Shared.Jittering;
using Content.Shared.Light.Components;
using Content.Shared.Maps; using Content.Shared.Maps;
using Content.Shared.NodeContainer; using Content.Shared.NodeContainer;
using Content.Shared.NodeContainer.NodeGroups; using Content.Shared.NodeContainer.NodeGroups;

View File

@@ -1,87 +1,5 @@
using Content.Server.Light.Components; using Content.Shared.Light.EntitySystems;
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;
namespace Content.Server.Light.EntitySystems namespace Content.Server.Light.EntitySystems;
{
public sealed class LightBulbSystem : EntitySystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
public override void Initialize() public sealed class LightBulbSystem : SharedLightBulbSystem;
{
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);
}
}
}

View File

@@ -1,440 +1,64 @@
using Content.Server.DeviceLinking.Systems;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Emp; using Content.Server.Emp;
using Content.Server.Ghost; 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 Content.Shared.Light.Components;
using Robust.Server.GameObjects; using Content.Shared.Light.EntitySystems;
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;
namespace Content.Server.Light.EntitySystems namespace Content.Server.Light.EntitySystems;
/// <summary>
/// System for the PoweredLightComponents
/// </summary>
public sealed class PoweredLightSystem : SharedPoweredLightSystem
{ {
/// <summary> public override void Initialize()
/// System for the PoweredLightComponents
/// </summary>
public sealed class PoweredLightSystem : EntitySystem
{ {
[Dependency] private readonly IGameTiming _gameTiming = default!; base.Initialize();
[Dependency] private readonly SharedAmbientSoundSystem _ambientSystem = default!; SubscribeLocalEvent<PoweredLightComponent, MapInitEvent>(OnMapInit);
[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); SubscribeLocalEvent<PoweredLightComponent, GhostBooEvent>(OnGhostBoo);
public const string LightBulbContainer = "light_bulb";
public override void Initialize() SubscribeLocalEvent<PoweredLightComponent, EmpPulseEvent>(OnEmpPulse);
}
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)
{ {
base.Initialize(); if (time <= light.LastGhostBlink + light.GhostBlinkingCooldown)
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; return;
args.Handled = InsertBulb(uid, args.Used, component);
} }
private void OnInteractHand(EntityUid uid, PoweredLightComponent light, InteractHandEvent args) light.LastGhostBlink = time;
ToggleBlinkingLight(uid, light, true);
uid.SpawnTimer(light.GhostBlinkingTime, () =>
{ {
if (args.Handled) ToggleBlinkingLight(uid, light, false);
return; });
// check if light has bulb to eject args.Handled = true;
var bulbUid = GetBulb(uid, light); }
if (bulbUid == null)
return;
var userUid = args.User; private void OnMapInit(EntityUid uid, PoweredLightComponent light, MapInitEvent args)
//removing a broken/burned bulb, so allow instant removal {
if(TryComp<LightBulbComponent>(bulbUid.Value, out var bulb) && bulb.State != LightBulbState.Normal) // TODO: Use ContainerFill dog
{ if (light.HasLampOnSpawn != null)
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)) var entity = EntityManager.SpawnEntity(light.HasLampOnSpawn, EntityManager.GetComponent<TransformComponent>(uid).Coordinates);
return false; ContainerSystem.Insert(entity, light.LightBulbContainer);
// 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;
} }
// need this to update visualizers
UpdateLight(uid, light);
}
/// <summary> private void OnEmpPulse(EntityUid uid, PoweredLightComponent component, ref EmpPulseEvent args)
/// Ejects the bulb to a mob's hand if possible. {
/// </summary> if (TryDestroyBulb(uid, component))
/// <returns>Bulb uid if it was successfully ejected, null otherwise</returns> args.Affected = true;
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;
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;
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;
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> /// <summary>
/// Amount of charge this needs from an APC per second to function. /// Amount of charge this needs from an APC per second to function.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("powerLoad")] [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; public ApcPowerProviderComponent? Provider = null;

View File

@@ -20,6 +20,7 @@ using Content.Shared.DoAfter;
using Content.Shared.Emag.Systems; using Content.Shared.Emag.Systems;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using Content.Shared.Humanoid; using Content.Shared.Humanoid;
using Content.Shared.Light.Components;
using Content.Shared.Maps; using Content.Shared.Maps;
using Content.Shared.Mobs; using Content.Shared.Mobs;
using Content.Shared.Mobs.Components; 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.Components;
using Content.Shared.Doors.Systems; using Content.Shared.Doors.Systems;
using Content.Shared.GameTicking.Components; using Content.Shared.GameTicking.Components;
using Content.Shared.Light.Components;
namespace Content.Server.StationEvents.Events; namespace Content.Server.StationEvents.Events;

View File

@@ -1,6 +1,6 @@
using Content.Server.Ghost; using Content.Server.Ghost;
using Content.Server.Light.Components;
using Content.Server.Xenoarchaeology.Artifact.XAE.Components; using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
using Content.Shared.Light.Components;
using Content.Shared.Xenoarchaeology.Artifact; using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.XAE; using Content.Shared.Xenoarchaeology.Artifact.XAE;
using Robust.Shared.Random; 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. /// 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. /// TODO: Breaking and burning should probably be moved to another component eventually.
/// </summary> /// </summary>
[RegisterComponent, NetworkedComponent] [RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class LightBulbComponent : Component public sealed partial class LightBulbComponent : Component
{ {
/// <summary> /// <summary>
/// The color of the lightbulb and the light it produces. /// The color of the lightbulb and the light it produces.
/// </summary> /// </summary>
[DataField("color")] [DataField, AutoNetworkedField]
[ViewVariables(VVAccess.ReadWrite)]
public Color Color = Color.White; public Color Color = Color.White;
/// <summary> /// <summary>
/// The type of lightbulb. Tube/bulb/etc... /// The type of lightbulb. Tube/bulb/etc...
/// </summary> /// </summary>
[DataField("bulb")] [DataField("bulb")]
[ViewVariables(VVAccess.ReadWrite)]
public LightBulbType Type = LightBulbType.Tube; public LightBulbType Type = LightBulbType.Tube;
/// <summary> /// <summary>
/// The initial state of the lightbulb. /// The initial state of the lightbulb.
/// </summary> /// </summary>
[DataField("startingState")] [DataField("startingState"), AutoNetworkedField]
public LightBulbState State = LightBulbState.Normal; public LightBulbState State = LightBulbState.Normal;
/// <summary> /// <summary>
/// The temperature the air around the lightbulb is exposed to when the lightbulb burns out. /// The temperature the air around the lightbulb is exposed to when the lightbulb burns out.
/// </summary> /// </summary>
[DataField("BurningTemperature")] [DataField("BurningTemperature")]
[ViewVariables(VVAccess.ReadWrite)]
public int BurningTemperature = 1400; public int BurningTemperature = 1400;
/// <summary> /// <summary>
/// Relates to how bright the light produced by the lightbulb is. /// Relates to how bright the light produced by the lightbulb is.
/// </summary> /// </summary>
[DataField("lightEnergy")] [DataField]
[ViewVariables(VVAccess.ReadWrite)]
public float LightEnergy = 0.8f; public float LightEnergy = 0.8f;
/// <summary> /// <summary>
/// The maximum radius of the point light source this light produces. /// The maximum radius of the point light source this light produces.
/// </summary> /// </summary>
[DataField("lightRadius")] [DataField]
[ViewVariables(VVAccess.ReadWrite)]
public float LightRadius = 10; public float LightRadius = 10;
/// <summary> /// <summary>
/// Relates to the falloff constant of the light produced by the lightbulb. /// Relates to the falloff constant of the light produced by the lightbulb.
/// </summary> /// </summary>
[DataField("lightSoftness")] [DataField]
[ViewVariables(VVAccess.ReadWrite)]
public float LightSoftness = 1; public float LightSoftness = 1;
/// <summary> /// <summary>
/// The amount of power used by the lightbulb when it's active. /// The amount of power used by the lightbulb when it's active.
/// </summary> /// </summary>
[DataField("PowerUse")] [DataField("PowerUse")]
[ViewVariables(VVAccess.ReadWrite)]
public int PowerUse = 60; public int PowerUse = 60;
/// <summary> /// <summary>
/// The sound produced when the lightbulb breaks. /// The sound produced when the lightbulb breaks.
/// </summary> /// </summary>
[DataField("breakSound")] [DataField]
[ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier BreakSound = new SoundCollectionSpecifier("GlassBreak", AudioParams.Default.WithVolume(-6f)); public SoundSpecifier BreakSound = new SoundCollectionSpecifier("GlassBreak", AudioParams.Default.WithVolume(-6f));
#region Appearance #region Appearance
@@ -78,22 +70,19 @@ public sealed partial class LightBulbComponent : Component
/// <summary> /// <summary>
/// The sprite state used when the lightbulb is intact. /// The sprite state used when the lightbulb is intact.
/// </summary> /// </summary>
[DataField("normalSpriteState")] [DataField]
[ViewVariables(VVAccess.ReadWrite)]
public string NormalSpriteState = "normal"; public string NormalSpriteState = "normal";
/// <summary> /// <summary>
/// The sprite state used when the lightbulb is broken. /// The sprite state used when the lightbulb is broken.
/// </summary> /// </summary>
[DataField("brokenSpriteState")] [DataField]
[ViewVariables(VVAccess.ReadWrite)]
public string BrokenSpriteState = "broken"; public string BrokenSpriteState = "broken";
/// <summary> /// <summary>
/// The sprite state used when the lightbulb is burned. /// The sprite state used when the lightbulb is burned.
/// </summary> /// </summary>
[DataField("burnedSpriteState")] [DataField]
[ViewVariables(VVAccess.ReadWrite)]
public string BurnedSpriteState = "burned"; public string BurnedSpriteState = "burned";
#endregion Appearance #endregion Appearance

View File

@@ -1,3 +1,4 @@
using Content.Shared.Light.Components;
using Content.Shared.Light.EntitySystems; using Content.Shared.Light.EntitySystems;
using Content.Shared.Storage; using Content.Shared.Storage;
using Robust.Shared.Audio; 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.DeviceLinking;
using Content.Shared.Light.Components; using Content.Shared.Light.EntitySystems;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Containers; using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Light.Components namespace Content.Shared.Light.Components
{ {
/// <summary> /// <summary>
/// Component that represents a wall light. It has a light bulb that can be replaced when broken. /// Component that represents a wall light. It has a light bulb that can be replaced when broken.
/// </summary> /// </summary>
[RegisterComponent, Access(typeof(PoweredLightSystem))] [RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause, Access(typeof(SharedPoweredLightSystem))]
public sealed partial class PoweredLightComponent : Component 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"); public SoundSpecifier BurnHandSound = new SoundPathSpecifier("/Audio/Effects/lightburn.ogg");
[DataField("turnOnSound")] [DataField]
public SoundSpecifier TurnOnSound = new SoundPathSpecifier("/Audio/Machines/light_tube_on.ogg"); public SoundSpecifier TurnOnSound = new SoundPathSpecifier("/Audio/Machines/light_tube_on.ogg");
[DataField("hasLampOnSpawn", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))] // Should be using containerfill?
public string? HasLampOnSpawn = null; [DataField]
public EntProtoId? HasLampOnSpawn = null;
[DataField("bulb")] [DataField("bulb")]
public LightBulbType BulbType; public LightBulbType BulbType;
[DataField("on")] [DataField, AutoNetworkedField]
public bool On = true; public bool On = true;
[DataField("ignoreGhostsBoo")] [DataField]
public bool IgnoreGhostsBoo; public bool IgnoreGhostsBoo;
[DataField("ghostBlinkingTime")] [DataField]
public TimeSpan GhostBlinkingTime = TimeSpan.FromSeconds(10); public TimeSpan GhostBlinkingTime = TimeSpan.FromSeconds(10);
[DataField("ghostBlinkingCooldown")] [DataField]
public TimeSpan GhostBlinkingCooldown = TimeSpan.FromSeconds(60); public TimeSpan GhostBlinkingCooldown = TimeSpan.FromSeconds(60);
[ViewVariables] [ViewVariables]
public ContainerSlot LightBulbContainer = default!; public ContainerSlot LightBulbContainer = default!;
[ViewVariables]
[AutoNetworkedField]
public bool CurrentLit; public bool CurrentLit;
[ViewVariables]
[DataField, AutoNetworkedField]
public bool IsBlinking; public bool IsBlinking;
[ViewVariables]
[DataField, AutoNetworkedField, AutoPausedField]
public TimeSpan LastThunk; public TimeSpan LastThunk;
[ViewVariables]
[DataField, AutoPausedField]
public TimeSpan? LastGhostBlink; public TimeSpan? LastGhostBlink;
[DataField("onPort", customTypeSerializer: typeof(PrototypeIdSerializer<SinkPortPrototype>))] [DataField]
public string OnPort = "On"; public ProtoId<SinkPortPrototype> OnPort = "On";
[DataField("offPort", customTypeSerializer: typeof(PrototypeIdSerializer<SinkPortPrototype>))] [DataField]
public string OffPort = "Off"; public ProtoId<SinkPortPrototype> OffPort = "Off";
[DataField("togglePort", customTypeSerializer: typeof(PrototypeIdSerializer<SinkPortPrototype>))] [DataField]
public string TogglePort = "Toggle"; public ProtoId<SinkPortPrototype> TogglePort = "Toggle";
/// <summary> /// <summary>
/// How long it takes to eject a bulb from this /// How long it takes to eject a bulb from this
/// </summary> /// </summary>
[DataField("ejectBulbDelay")] [DataField]
public float EjectBulbDelay = 2; public float EjectBulbDelay = 2;
/// <summary> /// <summary>
/// Shock damage done to a mob that hits the light with an unarmed attack /// Shock damage done to a mob that hits the light with an unarmed attack
/// </summary> /// </summary>
[DataField("unarmedHitShock")] [DataField]
public int UnarmedHitShock = 20; public int UnarmedHitShock = 20;
/// <summary> /// <summary>
/// Stun duration applied to a mob that hits the light with an unarmed attack /// Stun duration applied to a mob that hits the light with an unarmed attack
/// </summary> /// </summary>
[DataField("unarmedHitStun")] [DataField]
public TimeSpan UnarmedHitStun = TimeSpan.FromSeconds(5); 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> /// </summary>
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public virtual bool PowerDisabled { get; set; } 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; }
} }