Files
tbd-station-14/Content.Server/Light/EntitySystems/PoweredLightSystem.cs
Pieter-Jan Briers 68ce53ae17 Random spontaneous cleanup PR (#25131)
* Use new Subs.CVar helper

Removes manual config OnValueChanged calls, removes need to remember to manually unsubscribe.

This both reduces boilerplate and fixes many issues where subscriptions weren't removed on entity system shutdown.

* Fix a bunch of warnings

* More warning fixes

* Use new DateTime serializer to get rid of ISerializationHooks in changelog code.

* Get rid of some more ISerializationHooks for enums

* And a little more

* Apply suggestions from code review

Co-authored-by: 0x6273 <0x40@keemail.me>

---------

Co-authored-by: 0x6273 <0x40@keemail.me>
2024-02-13 16:48:39 -05:00

475 lines
19 KiB
C#

using Content.Server.Administration.Logs;
using Content.Server.Clothing.Components;
using Content.Server.DeviceLinking.Events;
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.Database;
using Content.Shared.DoAfter;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Light;
using Content.Shared.Light.Components;
using Content.Shared.Popups;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Audio.Systems;
namespace Content.Server.Light.EntitySystems
{
/// <summary>
/// System for the PoweredLightComponents
/// </summary>
public sealed class PoweredLightSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly SharedAmbientSoundSystem _ambientSystem = default!;
[Dependency] private readonly LightBulbSystem _bulbSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger= 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 InventorySystem _inventory = 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 = EntityManager.SpawnEntity(light.HasLampOnSpawn, EntityManager.GetComponent<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;
// check if it's possible to apply burn damage to user
var userUid = args.User;
if (EntityManager.TryGetComponent(bulbUid.Value, out LightBulbComponent? lightBulb))
{
// get users heat resistance
var res = int.MinValue;
if (_inventory.TryGetSlotEntity(userUid, "gloves", out var slotEntity) &&
TryComp<GloveHeatResistanceComponent>(slotEntity, out var gloves))
{
res = gloves.HeatResistance;
}
// check heat resistance against user
var burnedHand = light.CurrentLit && res < lightBulb.BurningTemperature;
if (burnedHand)
{
var damage = _damageableSystem.TryChangeDamage(userUid, light.Damage, origin: userUid);
// If damage is null then the entity could not take heat damage so they did not get burned.
if (damage != null)
{
var burnMsg = Loc.GetString("powered-light-component-burn-hand");
_popupSystem.PopupEntity(burnMsg, uid, userUid);
_adminLogger.Add(LogType.Damaged, $"{ToPrettyString(args.User):user} burned their hand on {ToPrettyString(args.Target):target} and received {damage.GetTotal():damage} damage");
_audio.PlayEntity(light.BurnHandSound, Filter.Pvs(uid), uid, true);
args.Handled = true;
return;
}
}
}
//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)
{
BreakOnUserMove = 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 (!EntityManager.TryGetComponent(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 || !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;
}
#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 || !EntityManager.TryGetComponent(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.PlayEntity(light.TurnOnSound, Filter.Pvs(uid), uid, true, AudioParams.Default.WithVolume(-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 (!EntityManager.TryGetComponent(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 (EntityManager.TryGetComponent(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);
}
}
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;
}
}
}