Predict EMPs (#39802)

* predicted emps

* fixes

* fix

* review
This commit is contained in:
slarticodefast
2025-10-04 13:24:42 +02:00
committed by GitHub
parent 690bb5a8f2
commit 5227489360
70 changed files with 669 additions and 516 deletions

View File

@@ -7,6 +7,18 @@ public sealed class EmpSystem : SharedEmpSystem
{
[Dependency] private readonly IRobustRandom _random = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<EmpDisabledComponent, ComponentStartup>(OnStartup);
}
private void OnStartup(Entity<EmpDisabledComponent> ent, ref ComponentStartup args)
{
// EmpPulseEvent.Affected will spawn the first visual effect directly when the emp is used
ent.Comp.TargetTime = Timing.CurTime + _random.NextFloat(0.8f, 1.2f) * ent.Comp.EffectCooldown;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
@@ -16,7 +28,7 @@ public sealed class EmpSystem : SharedEmpSystem
{
if (Timing.CurTime > comp.TargetTime)
{
comp.TargetTime = Timing.CurTime + _random.NextFloat(0.8f, 1.2f) * TimeSpan.FromSeconds(comp.EffectCooldown);
comp.TargetTime = Timing.CurTime + _random.NextFloat(0.8f, 1.2f) * comp.EffectCooldown;
Spawn(EmpDisabledEffectPrototype, transform.Coordinates);
}
}

View File

@@ -0,0 +1,5 @@
using Content.Shared.Power.EntitySystems;
namespace Content.Client.Power.EntitySystems;
public sealed class BatterySystem : SharedBatterySystem;

View File

@@ -0,0 +1,5 @@
using Content.Shared.Power.EntitySystems;
namespace Content.Client.Power.EntitySystems;
public sealed class ChargerSystem : SharedChargerSystem;

View File

@@ -0,0 +1,5 @@
using Content.Shared.SurveillanceCamera;
namespace Content.Client.SurveillanceCamera;
public sealed class SurveillanceCameraSystem : SharedSurveillanceCameraSystem;

View File

@@ -33,6 +33,7 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
component.EjectEnd = state.EjectEnd;
component.DenyEnd = state.DenyEnd;
component.DispenseOnHitEnd = state.DispenseOnHitEnd;
component.Broken = state.Broken;
// If all we did was update amounts then we can leave BUI buttons in place.
var fullUiUpdate = !component.Inventory.Keys.SequenceEqual(state.Inventory.Keys) ||

View File

@@ -1,12 +1,11 @@
#nullable enable
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Power.Nodes;
using Content.Shared.Coordinates;
using Content.Shared.NodeContainer;
using Content.Shared.Power.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;

View File

@@ -5,6 +5,7 @@ using Content.Server.Maps;
using Content.Server.Power.Components;
using Content.Server.Power.NodeGroups;
using Content.Server.Power.Pow3r;
using Content.Shared.Power.Components;
using Content.Shared.NodeContainer;
using Robust.Shared.EntitySerialization;

View File

@@ -24,6 +24,7 @@ using Content.Shared.Doors.Components;
using Content.Shared.Hands.Components;
using Content.Shared.Inventory;
using Content.Shared.PDA;
using Content.Shared.Power.Components;
using Content.Shared.Stacks;
using Content.Shared.Station.Components;
using Content.Shared.Verbs;

View File

@@ -1,14 +1,10 @@
using System.Linq;
using Content.Server.Emp;
using Content.Shared.Clothing.Components;
using Content.Shared.Clothing.EntitySystems;
using Content.Shared.Emp;
using Content.Shared.IdentityManagement;
using Content.Shared.IdentityManagement.Components;
using Content.Shared.Inventory;
using Content.Shared.Prototypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Clothing.Systems;
@@ -16,15 +12,12 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IdentitySystem _identity = default!;
[Dependency] private readonly IRobustRandom _random = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ChameleonClothingComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ChameleonClothingComponent, ChameleonPrototypeSelectedMessage>(OnSelected);
SubscribeLocalEvent<ChameleonClothingComponent, EmpPulseEvent>(OnEmpPulse);
}
private void OnMapInit(EntityUid uid, ChameleonClothingComponent component, MapInitEvent args)
@@ -37,21 +30,6 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
SetSelectedPrototype(uid, args.SelectedId, component: component);
}
private void OnEmpPulse(EntityUid uid, ChameleonClothingComponent component, ref EmpPulseEvent args)
{
if (!component.AffectedByEmp)
return;
if (component.EmpContinuous)
component.NextEmpChange = _timing.CurTime + TimeSpan.FromSeconds(1f / component.EmpChangeIntensity);
var pick = GetRandomValidPrototype(component.Slot, component.RequireTag);
SetSelectedPrototype(uid, pick, component: component);
args.Affected = true;
args.Disabled = true;
}
private void UpdateUi(EntityUid uid, ChameleonClothingComponent? component = null)
{
if (!Resolve(uid, ref component))
@@ -64,7 +42,7 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
/// <summary>
/// Change chameleon items name, description and sprite to mimic other entity prototype.
/// </summary>
public void SetSelectedPrototype(EntityUid uid, string? protoId, bool forceUpdate = false,
public override void SetSelectedPrototype(EntityUid uid, string? protoId, bool forceUpdate = false,
ChameleonClothingComponent? component = null)
{
if (!Resolve(uid, ref component, false))
@@ -88,14 +66,6 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
Dirty(uid, component);
}
/// <summary>
/// Get a random prototype for a given slot.
/// </summary>
public string GetRandomValidPrototype(SlotFlags slot, string? tag = null)
{
return _random.Pick(GetValidTargets(slot, tag).ToList());
}
public override void Update(float frameTime)
{
base.Update(frameTime);
@@ -106,7 +76,7 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
if (!chameleon.EmpContinuous)
continue;
if (_timing.CurTime < chameleon.NextEmpChange)
if (Timing.CurTime < chameleon.NextEmpChange)
continue;
// randomly pick cloth element from available and apply it

View File

@@ -1,7 +1,7 @@
using Content.Server.Mech.Systems;
using Content.Server.Power.Components;
using Content.Shared.Construction;
using Content.Shared.Mech.Components;
using Content.Shared.Power.Components;
using JetBrains.Annotations;
using Robust.Server.Containers;
using Robust.Shared.Containers;

View File

@@ -2,16 +2,11 @@ using Content.Server.Power.EntitySystems;
using Content.Server.Radio;
using Content.Server.SurveillanceCamera;
using Content.Shared.Emp;
using Robust.Shared.Map;
namespace Content.Server.Emp;
public sealed class EmpSystem : SharedEmpSystem
{
[Dependency] private readonly EntityLookupSystem _lookup = default!;
public const string EmpPulseEffectPrototype = "EffectEmpPulse";
public override void Initialize()
{
base.Initialize();
@@ -22,84 +17,6 @@ public sealed class EmpSystem : SharedEmpSystem
SubscribeLocalEvent<EmpDisabledComponent, SurveillanceCameraSetActiveAttemptEvent>(OnCameraSetActive);
}
public override void EmpPulse(MapCoordinates coordinates, float range, float energyConsumption, float duration)
{
foreach (var uid in _lookup.GetEntitiesInRange(coordinates, range))
{
TryEmpEffects(uid, energyConsumption, duration);
}
Spawn(EmpPulseEffectPrototype, coordinates);
}
/// <summary>
/// Triggers an EMP pulse at the given location, by first raising an <see cref="EmpAttemptEvent"/>, then a raising <see cref="EmpPulseEvent"/> on all entities in range.
/// </summary>
/// <param name="coordinates">The location to trigger the EMP pulse at.</param>
/// <param name="range">The range of the EMP pulse.</param>
/// <param name="energyConsumption">The amount of energy consumed by the EMP pulse.</param>
/// <param name="duration">The duration of the EMP effects.</param>
public void EmpPulse(EntityCoordinates coordinates, float range, float energyConsumption, float duration)
{
foreach (var uid in _lookup.GetEntitiesInRange(coordinates, range))
{
TryEmpEffects(uid, energyConsumption, duration);
}
Spawn(EmpPulseEffectPrototype, coordinates);
}
/// <summary>
/// Attempts to apply the effects of an EMP pulse onto an entity by first raising an <see cref="EmpAttemptEvent"/>, followed by raising a <see cref="EmpPulseEvent"/> on it.
/// </summary>
/// <param name="uid">The entity to apply the EMP effects on.</param>
/// <param name="energyConsumption">The amount of energy consumed by the EMP.</param>
/// <param name="duration">The duration of the EMP effects.</param>
public void TryEmpEffects(EntityUid uid, float energyConsumption, float duration)
{
var attemptEv = new EmpAttemptEvent();
RaiseLocalEvent(uid, attemptEv);
if (attemptEv.Cancelled)
return;
DoEmpEffects(uid, energyConsumption, duration);
}
/// <summary>
/// Applies the effects of an EMP pulse onto an entity by raising a <see cref="EmpPulseEvent"/> on it.
/// </summary>
/// <param name="uid">The entity to apply the EMP effects on.</param>
/// <param name="energyConsumption">The amount of energy consumed by the EMP.</param>
/// <param name="duration">The duration of the EMP effects.</param>
public void DoEmpEffects(EntityUid uid, float energyConsumption, float duration)
{
var ev = new EmpPulseEvent(energyConsumption, false, false, TimeSpan.FromSeconds(duration));
RaiseLocalEvent(uid, ref ev);
if (ev.Affected)
Spawn(EmpDisabledEffectPrototype, Transform(uid).Coordinates);
if (!ev.Disabled)
return;
var disabled = EnsureComp<EmpDisabledComponent>(uid);
disabled.DisabledUntil = Timing.CurTime + TimeSpan.FromSeconds(duration);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<EmpDisabledComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (comp.DisabledUntil < Timing.CurTime)
{
RemComp<EmpDisabledComponent>(uid);
var ev = new EmpDisabledRemoved();
RaiseLocalEvent(uid, ref ev);
}
}
}
private void OnRadioSendAttempt(EntityUid uid, EmpDisabledComponent component, ref RadioSendAttemptEvent args)
{
args.Cancelled = true;
@@ -120,14 +37,3 @@ public sealed class EmpSystem : SharedEmpSystem
args.Cancelled = true;
}
}
/// <summary>
/// Raised on an entity before <see cref="EmpPulseEvent"/>. Cancel this to prevent the emp event being raised.
/// </summary>
public sealed partial class EmpAttemptEvent : CancellableEntityEventArgs;
[ByRefEvent]
public record struct EmpPulseEvent(float EnergyConsumption, bool Affected, bool Disabled, TimeSpan Duration);
[ByRefEvent]
public record struct EmpDisabledRemoved();

View File

@@ -1,8 +1,8 @@
using Content.Shared.Examine;
using Content.Shared.Coordinates.Helpers;
using Content.Server.Power.Components;
using Content.Server.PowerCell;
using Content.Shared.Interaction;
using Content.Shared.Power.Components;
using Content.Shared.Storage;
namespace Content.Server.Holosign;
@@ -12,7 +12,6 @@ public sealed class HolosignSystem : EntitySystem
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public override void Initialize()
{
base.Initialize();

View File

@@ -8,6 +8,7 @@ using Content.Shared.Examine;
using Content.Shared.Light;
using Content.Shared.Light.Components;
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Content.Shared.Station.Components;
using Robust.Server.GameObjects;
using Color = Robust.Shared.Maths.Color;

View File

@@ -1,4 +1,3 @@
using Content.Server.Emp;
using Content.Server.Ghost;
using Content.Shared.Light.Components;
using Content.Shared.Light.EntitySystems;
@@ -16,8 +15,6 @@ public sealed class PoweredLightSystem : SharedPoweredLightSystem
SubscribeLocalEvent<PoweredLightComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<PoweredLightComponent, GhostBooEvent>(OnGhostBoo);
SubscribeLocalEvent<PoweredLightComponent, EmpPulseEvent>(OnEmpPulse);
}
private void OnGhostBoo(EntityUid uid, PoweredLightComponent light, GhostBooEvent args)
@@ -55,10 +52,4 @@ public sealed class PoweredLightSystem : SharedPoweredLightSystem
// need this to update visualizers
UpdateLight(uid, light);
}
private void OnEmpPulse(EntityUid uid, PoweredLightComponent component, ref EmpPulseEvent args)
{
if (TryDestroyBulb(uid, component))
args.Affected = true;
}
}

View File

@@ -2,7 +2,6 @@ using System.Linq;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Systems;
using Content.Server.Mech.Components;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.ActionBlocker;
using Content.Shared.Damage;
@@ -14,6 +13,7 @@ using Content.Shared.Mech.Components;
using Content.Shared.Mech.EntitySystems;
using Content.Shared.Movement.Events;
using Content.Shared.Popups;
using Content.Shared.Power.Components;
using Content.Shared.Tools;
using Content.Shared.Tools.Components;
using Content.Shared.Tools.Systems;

View File

@@ -1,8 +1,6 @@
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Emp;
using Content.Server.Medical.CrewMonitoring;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.Medical.SuitSensor;
using Content.Shared.Medical.SuitSensors;
using Robust.Shared.Timing;
@@ -14,14 +12,6 @@ public sealed class SuitSensorSystem : SharedSuitSensorSystem
[Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
[Dependency] private readonly SingletonDeviceNetServerSystem _singletonServerSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SuitSensorComponent, EmpPulseEvent>(OnEmpPulse);
SubscribeLocalEvent<SuitSensorComponent, EmpDisabledRemoved>(OnEmpFinished);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
@@ -70,22 +60,4 @@ public sealed class SuitSensorSystem : SharedSuitSensorSystem
_deviceNetworkSystem.QueuePacket(uid, sensor.ConnectedServer, payload, device: device);
}
}
private void OnEmpPulse(Entity<SuitSensorComponent> ent, ref EmpPulseEvent args)
{
args.Affected = true;
args.Disabled = true;
ent.Comp.PreviousMode = ent.Comp.Mode;
SetSensor(ent.AsNullable(), SuitSensorMode.SensorOff, null);
ent.Comp.PreviousControlsLocked = ent.Comp.ControlsLocked;
ent.Comp.ControlsLocked = true;
}
private void OnEmpFinished(Entity<SuitSensorComponent> ent, ref EmpDisabledRemoved args)
{
SetSensor(ent.AsNullable(), ent.Comp.PreviousMode, null);
ent.Comp.ControlsLocked = ent.Comp.PreviousControlsLocked;
}
}

View File

@@ -6,7 +6,7 @@ using Content.Shared.Interaction;
using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
using Content.Shared.Popups;
using Robust.Shared.Audio;
using Content.Shared.Power.Components;
using Robust.Shared.Audio.Systems;
namespace Content.Server.Ninja.Systems;

View File

@@ -1,11 +1,11 @@
using Content.Server.Emp;
using Content.Server.Ninja.Events;
using Content.Server.Power.Components;
using Content.Server.PowerCell;
using Content.Shared.Emp;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
using Content.Shared.Popups;
using Content.Shared.Power.Components;
using Content.Shared.PowerCell.Components;
using Robust.Shared.Containers;
@@ -16,7 +16,7 @@ namespace Content.Server.Ninja.Systems;
/// </summary>
public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
{
[Dependency] private readonly EmpSystem _emp = default!;
[Dependency] private readonly SharedEmpSystem _emp = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SpaceNinjaSystem _ninja = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
@@ -30,7 +30,6 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
base.Initialize();
SubscribeLocalEvent<NinjaSuitComponent, ContainerIsInsertingAttemptEvent>(OnSuitInsertAttempt);
SubscribeLocalEvent<NinjaSuitComponent, EmpAttemptEvent>(OnEmpAttempt);
SubscribeLocalEvent<NinjaSuitComponent, RecallKatanaEvent>(OnRecallKatana);
SubscribeLocalEvent<NinjaSuitComponent, NinjaEmpEvent>(OnEmp);
}
@@ -96,17 +95,10 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
// if a cell is able to automatically recharge, boost the score drastically depending on the recharge rate,
// this is to ensure a ninja can still upgrade to a micro reactor cell even if they already have a medium or high.
if (TryComp<BatterySelfRechargerComponent>(uid, out var selfcomp) && selfcomp.AutoRecharge)
return battcomp.MaxCharge + (selfcomp.AutoRechargeRate*AutoRechargeValue);
return battcomp.MaxCharge + selfcomp.AutoRechargeRate * AutoRechargeValue;
return battcomp.MaxCharge;
}
private void OnEmpAttempt(EntityUid uid, NinjaSuitComponent comp, EmpAttemptEvent args)
{
// ninja suit (battery) is immune to emp
// powercell relays the event to suit
args.Cancel();
}
protected override void UserUnequippedSuit(Entity<NinjaSuitComponent> ent, Entity<SpaceNinjaComponent> user)
{
base.UserUnequippedSuit(ent, user);
@@ -144,6 +136,7 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
Popup.PopupEntity(Loc.GetString(message), user, user);
}
// TODO: Move this to shared when power cells are predicted.
private void OnEmp(Entity<NinjaSuitComponent> ent, ref NinjaEmpEvent args)
{
var (uid, comp) = ent;
@@ -159,7 +152,6 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
if (CheckDisabled(ent, user))
return;
var coords = _transform.GetMapCoordinates(user);
_emp.EmpPulse(coords, comp.EmpRange, comp.EmpConsumption, comp.EmpDuration);
_emp.EmpPulse(Transform(user).Coordinates, comp.EmpRange, comp.EmpConsumption, comp.EmpDuration, user);
}
}

View File

@@ -1,26 +1,20 @@
using Content.Server.Communications;
using Content.Server.Chat.Managers;
using Content.Server.CriminalRecords.Systems;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Objectives.Components;
using Content.Server.Objectives.Systems;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.PowerCell;
using Content.Server.Research.Systems;
using Content.Server.Roles;
using Content.Shared.Alert;
using Content.Shared.Doors.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Mind;
using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
using Content.Shared.Power.Components;
using Content.Shared.Popups;
using Content.Shared.Rounding;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Audio.Systems;
namespace Content.Server.Ninja.Systems;

View File

@@ -1,4 +1,5 @@
using Content.Server.Power.NodeGroups;
using Content.Shared.Power.Components;
namespace Content.Server.Power.Components
{

View File

@@ -1,67 +0,0 @@
using Content.Server.Power.EntitySystems;
using Content.Shared.Guidebook;
namespace Content.Server.Power.Components
{
/// <summary>
/// Battery node on the pow3r network. Needs other components to connect to actual networks.
/// </summary>
[RegisterComponent]
[Virtual]
[Access(typeof(BatterySystem))]
public partial class BatteryComponent : Component
{
public string SolutionName = "battery";
/// <summary>
/// Maximum charge of the battery in joules (ie. watt seconds)
/// </summary>
[DataField]
[GuidebookData]
public float MaxCharge;
/// <summary>
/// Current charge of the battery in joules (ie. watt seconds)
/// </summary>
[DataField("startingCharge")]
public float CurrentCharge;
/// <summary>
/// The price per one joule. Default is 1 credit for 10kJ.
/// </summary>
[DataField]
public float PricePerJoule = 0.0001f;
}
/// <summary>
/// Raised when a battery's charge or capacity changes (capacity affects relative charge percentage).
/// </summary>
[ByRefEvent]
public readonly record struct ChargeChangedEvent(float Charge, float MaxCharge);
/// <summary>
/// Raised when it is necessary to get information about battery charges.
/// </summary>
[ByRefEvent]
public sealed class GetChargeEvent : EntityEventArgs
{
public float CurrentCharge;
public float MaxCharge;
}
/// <summary>
/// Raised when it is necessary to change the current battery charge to a some value.
/// </summary>
[ByRefEvent]
public sealed class ChangeChargeEvent : EntityEventArgs
{
public float OriginalValue;
public float ResidualValue;
public ChangeChargeEvent(float value)
{
OriginalValue = value;
ResidualValue = value;
}
}
}

View File

@@ -1,37 +0,0 @@
using Content.Shared.Power;
using Content.Shared.Whitelist;
namespace Content.Server.Power.Components
{
[RegisterComponent]
public sealed partial class ChargerComponent : Component
{
[ViewVariables]
public CellChargerStatus Status;
/// <summary>
/// The charge rate of the charger, in watts
/// </summary>
[DataField("chargeRate")]
public float ChargeRate = 20.0f;
/// <summary>
/// The container ID that is holds the entities being charged.
/// </summary>
[DataField("slotId", required: true)]
public string SlotId = string.Empty;
/// <summary>
/// A whitelist for what entities can be charged by this Charger.
/// </summary>
[DataField("whitelist")]
public EntityWhitelist? Whitelist;
/// <summary>
/// Indicates whether the charger is portable and thus subject to EMP effects
/// and bypasses checks for transform, anchored, and ApcPowerReceiverComponent.
/// </summary>
[DataField]
public bool Portable = false;
}
}

View File

@@ -1,11 +1,12 @@
using Content.Server.Emp;
using Content.Server.Popups;
using Content.Server.Power.Components;
using Content.Server.Power.Pow3r;
using Content.Shared.Access.Systems;
using Content.Shared.APC;
using Content.Shared.Emag.Systems;
using Content.Shared.Emp;
using Content.Shared.Popups;
using Content.Shared.Power;
using Content.Shared.Rounding;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
@@ -203,6 +204,9 @@ public sealed class ApcSystem : EntitySystem
return ApcExternalPowerState.Good;
}
// TODO: This subscription should be in shared.
// But I am not moving ApcComponent to shared, this PR already got soaped enough and that component uses several layers of OOP.
// At least the EMP visuals won't mispredict, since all APCs also have the BatteryComponent, which also has a EMP effect and is in shared.
private void OnEmpPulse(EntityUid uid, ApcComponent component, ref EmpPulseEvent args)
{
if (component.MainBreakerEnabled)

View File

@@ -2,6 +2,7 @@
using Content.Server.Power.Components;
using Content.Shared.Database;
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Robust.Server.GameObjects;
namespace Content.Server.Power.EntitySystems;

View File

@@ -1,7 +1,9 @@
using Content.Server.Emp;
using Content.Server.Power.Components;
using Content.Shared.Cargo;
using Content.Shared.Examine;
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Rejuvenate;
using JetBrains.Annotations;
using Robust.Shared.Utility;
@@ -10,7 +12,7 @@ using Robust.Shared.Timing;
namespace Content.Server.Power.EntitySystems
{
[UsedImplicitly]
public sealed class BatterySystem : EntitySystem
public sealed class BatterySystem : SharedBatterySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
@@ -22,7 +24,6 @@ namespace Content.Server.Power.EntitySystems
SubscribeLocalEvent<PowerNetworkBatteryComponent, RejuvenateEvent>(OnNetBatteryRejuvenate);
SubscribeLocalEvent<BatteryComponent, RejuvenateEvent>(OnBatteryRejuvenate);
SubscribeLocalEvent<BatteryComponent, PriceCalculationEvent>(CalculateBatteryPrice);
SubscribeLocalEvent<BatteryComponent, EmpPulseEvent>(OnEmpPulse);
SubscribeLocalEvent<BatteryComponent, ChangeChargeEvent>(OnChangeCharge);
SubscribeLocalEvent<BatteryComponent, GetChargeEvent>(OnGetCharge);
@@ -108,15 +109,6 @@ namespace Content.Server.Power.EntitySystems
{
args.Price += component.CurrentCharge * component.PricePerJoule;
}
private void OnEmpPulse(EntityUid uid, BatteryComponent component, ref EmpPulseEvent args)
{
args.Affected = true;
UseCharge(uid, args.EnergyConsumption, component);
// Apply a cooldown to the entity's self recharge if needed to avoid it immediately self recharging after an EMP.
TrySetChargeCooldown(uid);
}
private void OnChangeCharge(Entity<BatteryComponent> entity, ref ChangeChargeEvent args)
{
if (args.ResidualValue == 0)
@@ -131,7 +123,7 @@ namespace Content.Server.Power.EntitySystems
args.MaxCharge += entity.Comp.MaxCharge;
}
public float UseCharge(EntityUid uid, float value, BatteryComponent? battery = null)
public override float UseCharge(EntityUid uid, float value, BatteryComponent? battery = null)
{
if (value <= 0 || !Resolve(uid, ref battery) || battery.CurrentCharge == 0)
return 0;
@@ -139,7 +131,7 @@ namespace Content.Server.Power.EntitySystems
return ChangeCharge(uid, -value, battery);
}
public void SetMaxCharge(EntityUid uid, float value, BatteryComponent? battery = null)
public override void SetMaxCharge(EntityUid uid, float value, BatteryComponent? battery = null)
{
if (!Resolve(uid, ref battery))
return;
@@ -174,7 +166,7 @@ namespace Content.Server.Power.EntitySystems
/// <summary>
/// Changes the current battery charge by some value
/// </summary>
public float ChangeCharge(EntityUid uid, float value, BatteryComponent? battery = null)
public override float ChangeCharge(EntityUid uid, float value, BatteryComponent? battery = null)
{
if (!Resolve(uid, ref battery))
return 0;
@@ -190,10 +182,7 @@ namespace Content.Server.Power.EntitySystems
return delta;
}
/// <summary>
/// Checks if the entity has a self recharge and puts it on cooldown if applicable.
/// </summary>
public void TrySetChargeCooldown(EntityUid uid, float value = -1)
public override void TrySetChargeCooldown(EntityUid uid, float value = -1)
{
if (!TryComp<BatterySelfRechargerComponent>(uid, out var batteryself))
return;
@@ -228,7 +217,7 @@ namespace Content.Server.Power.EntitySystems
/// <summary>
/// If sufficient charge is available on the battery, use it. Otherwise, don't.
/// </summary>
public bool TryUseCharge(EntityUid uid, float value, BatteryComponent? battery = null)
public override bool TryUseCharge(EntityUid uid, float value, BatteryComponent? battery = null)
{
if (!Resolve(uid, ref battery, false) || value > battery.CurrentCharge)
return false;

View File

@@ -1,8 +1,9 @@
using Content.Server.Power.Components;
using Content.Server.Emp;
using Content.Server.PowerCell;
using Content.Shared.Examine;
using Content.Server.PowerCell;
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.PowerCell.Components;
using Content.Shared.Emp;
using JetBrains.Annotations;
@@ -15,7 +16,7 @@ using Content.Shared.Whitelist;
namespace Content.Server.Power.EntitySystems;
[UsedImplicitly]
internal sealed class ChargerSystem : EntitySystem
public sealed class ChargerSystem : SharedChargerSystem
{
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
@@ -25,6 +26,8 @@ internal sealed class ChargerSystem : EntitySystem
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ChargerComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<ChargerComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<ChargerComponent, EntInsertedIntoContainerMessage>(OnInserted);
@@ -32,8 +35,6 @@ internal sealed class ChargerSystem : EntitySystem
SubscribeLocalEvent<ChargerComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
SubscribeLocalEvent<ChargerComponent, InsertIntoEntityStorageAttemptEvent>(OnEntityStorageInsertAttempt);
SubscribeLocalEvent<ChargerComponent, ExaminedEvent>(OnChargerExamine);
SubscribeLocalEvent<ChargerComponent, EmpPulseEvent>(OnEmpPulse);
}
private void OnStartup(EntityUid uid, ChargerComponent component, ComponentStartup args)
@@ -194,12 +195,6 @@ internal sealed class ChargerSystem : EntitySystem
}
}
private void OnEmpPulse(EntityUid uid, ChargerComponent component, ref EmpPulseEvent args)
{
args.Affected = true;
args.Disabled = true;
}
private CellChargerStatus GetStatus(EntityUid uid, ChargerComponent component)
{
if (!component.Portable)

View File

@@ -1,6 +1,4 @@
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Content.Server.Power.Nodes;
using Content.Server.Power.NodeGroups;
@@ -9,6 +7,7 @@ using Content.Shared.GameTicking.Components;
using Content.Shared.Pinpointer;
using Content.Shared.Station.Components;
using Content.Shared.Power;
using Content.Shared.Power.Components;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Map.Components;

View File

@@ -4,6 +4,7 @@ using Content.Server.Kitchen.Components;
using Content.Server.Power.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Database;
using Content.Shared.Power.Components;
using Content.Shared.Rejuvenate;
namespace Content.Server.Power.EntitySystems;

View File

@@ -1,6 +1,7 @@
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Content.Shared.Rounding;
using Content.Shared.SMES;
using JetBrains.Annotations;

View File

@@ -1,7 +1,7 @@
using Content.Server.Administration;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Administration;
using Content.Shared.Power.Components;
using Robust.Shared.Console;
namespace Content.Server.Power

View File

@@ -1,4 +1,4 @@
using Content.Server.Power.Components;
using Content.Shared.Power;
using Content.Shared.PowerCell;
using Content.Shared.PowerCell.Components;

View File

@@ -1,17 +1,17 @@
using Content.Server.Emp;
using System.Diagnostics.CodeAnalysis;
using Content.Server.Kitchen.Components;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Examine;
using Content.Shared.Popups;
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Content.Shared.PowerCell;
using Content.Shared.PowerCell.Components;
using Content.Shared.Rounding;
using Content.Shared.UserInterface;
using Robust.Shared.Containers;
using System.Diagnostics.CodeAnalysis;
using Content.Server.Kitchen.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.UserInterface;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Popups;
using ActivatableUISystem = Content.Shared.UserInterface.ActivatableUISystem;
namespace Content.Server.PowerCell;
@@ -34,7 +34,6 @@ public sealed partial class PowerCellSystem : SharedPowerCellSystem
SubscribeLocalEvent<PowerCellComponent, ChargeChangedEvent>(OnChargeChanged);
SubscribeLocalEvent<PowerCellComponent, ExaminedEvent>(OnCellExamined);
SubscribeLocalEvent<PowerCellComponent, EmpAttemptEvent>(OnCellEmpAttempt);
SubscribeLocalEvent<PowerCellDrawComponent, ChargeChangedEvent>(OnDrawChargeChanged);
SubscribeLocalEvent<PowerCellDrawComponent, PowerCellChangedEvent>(OnDrawCellChanged);
@@ -221,14 +220,6 @@ public sealed partial class PowerCellSystem : SharedPowerCellSystem
OnBatteryExamined(uid, battery, args);
}
private void OnCellEmpAttempt(EntityUid uid, PowerCellComponent component, EmpAttemptEvent args)
{
var parent = Transform(uid).ParentUid;
// relay the attempt event to the slot so it can cancel it
if (HasComp<PowerCellSlotComponent>(parent))
RaiseLocalEvent(parent, args);
}
private void OnCellSlotExamined(EntityUid uid, PowerCellSlotComponent component, ExaminedEvent args)
{
TryGetBatteryFromSlot(uid, out var battery);

View File

@@ -1,13 +1,13 @@
using Content.Server.Explosion.EntitySystems;
using Content.Server.Chat.Systems;
using Content.Server.Explosion.EntitySystems;
using Content.Server.Power.Components;
using Content.Shared.Examine;
using Robust.Shared.Utility;
using Content.Server.Chat.Systems;
using Content.Server.Station.Systems;
using Robust.Shared.Timing;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Content.Server.Power.EntitySystems;
using Content.Server.Station.Systems;
using Content.Shared.Examine;
using Content.Shared.Power.Components;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.PowerSink
{

View File

@@ -1,5 +1,4 @@
using Content.Server.Chat.Systems;
using Content.Server.Emp;
using Content.Shared.Inventory.Events;
using Content.Shared.Radio;
using Content.Shared.Radio.Components;
@@ -21,8 +20,6 @@ public sealed class HeadsetSystem : SharedHeadsetSystem
SubscribeLocalEvent<HeadsetComponent, EncryptionChannelsChangedEvent>(OnKeysChanged);
SubscribeLocalEvent<WearingHeadsetComponent, EntitySpokeEvent>(OnSpeak);
SubscribeLocalEvent<HeadsetComponent, EmpPulseEvent>(OnEmpPulse);
}
private void OnKeysChanged(EntityUid uid, HeadsetComponent component, EncryptionChannelsChangedEvent args)
@@ -69,7 +66,6 @@ public sealed class HeadsetSystem : SharedHeadsetSystem
protected override void OnGotUnequipped(EntityUid uid, HeadsetComponent component, GotUnequippedEvent args)
{
base.OnGotUnequipped(uid, component, args);
component.IsEquipped = false;
RemComp<ActiveRadioComponent>(uid);
RemComp<WearingHeadsetComponent>(args.Equipee);
}
@@ -82,6 +78,9 @@ public sealed class HeadsetSystem : SharedHeadsetSystem
if (component.Enabled == value)
return;
component.Enabled = value;
Dirty(uid, component);
if (!value)
{
RemCompDeferred<ActiveRadioComponent>(uid);
@@ -113,13 +112,4 @@ public sealed class HeadsetSystem : SharedHeadsetSystem
if (TryComp(parent, out ActorComponent? actor))
_netMan.ServerSendMessage(args.ChatMsg, actor.PlayerSession.Channel);
}
private void OnEmpPulse(EntityUid uid, HeadsetComponent component, ref EmpPulseEvent args)
{
if (component.Enabled)
{
args.Affected = true;
args.Disabled = true;
}
}
}

View File

@@ -1,4 +1,5 @@
using Content.Server.Power.Components;
using Content.Shared.Power.Components;
namespace Content.Server.SensorMonitoring;

View File

@@ -1,8 +1,8 @@
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Power.Components;
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Events;
using Content.Shared.Power.Components;
namespace Content.Server.SensorMonitoring;

View File

@@ -19,6 +19,7 @@ using Content.Shared.DoAfter;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Content.Shared.Rejuvenate;
using Content.Shared.Roles;

View File

@@ -7,6 +7,8 @@ using Content.Shared.Examine;
using Content.Shared.Item.ItemToggle;
using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.Popups;
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Content.Shared.Stunnable;
namespace Content.Server.Stunnable.Systems

View File

@@ -1,6 +1,7 @@
using Content.Server.Chat.Systems;
using Content.Shared.Speech;
using Content.Shared.Speech.Components;
using Content.Shared.SurveillanceCamera.Components;
using Content.Shared.Whitelist;
using Robust.Shared.Player;
using static Content.Server.Chat.Systems.ChatSystem;

View File

@@ -1,13 +1,12 @@
using Content.Server.Administration.Logs;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Emp;
using Content.Shared.ActionBlocker;
using Content.Shared.Database;
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Events;
using Content.Shared.Power;
using Content.Shared.SurveillanceCamera;
using Content.Shared.Verbs;
using Content.Shared.SurveillanceCamera.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
@@ -15,7 +14,7 @@ using Content.Shared.DeviceNetwork.Components;
namespace Content.Server.SurveillanceCamera;
public sealed class SurveillanceCameraSystem : EntitySystem
public sealed class SurveillanceCameraSystem : SharedSurveillanceCameraSystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
@@ -57,15 +56,13 @@ public sealed class SurveillanceCameraSystem : EntitySystem
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SurveillanceCameraComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<SurveillanceCameraComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<SurveillanceCameraComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
SubscribeLocalEvent<SurveillanceCameraComponent, SurveillanceCameraSetupSetName>(OnSetName);
SubscribeLocalEvent<SurveillanceCameraComponent, SurveillanceCameraSetupSetNetwork>(OnSetNetwork);
SubscribeLocalEvent<SurveillanceCameraComponent, GetVerbsEvent<AlternativeVerb>>(AddVerbs);
SubscribeLocalEvent<SurveillanceCameraComponent, EmpPulseEvent>(OnEmpPulse);
SubscribeLocalEvent<SurveillanceCameraComponent, EmpDisabledRemoved>(OnEmpDisabledRemoved);
}
private void OnPacketReceived(EntityUid uid, SurveillanceCameraComponent component, DeviceNetworkPacketEvent args)
@@ -131,26 +128,6 @@ public sealed class SurveillanceCameraSystem : EntitySystem
}
}
private void AddVerbs(EntityUid uid, SurveillanceCameraComponent component, GetVerbsEvent<AlternativeVerb> verbs)
{
if (!_actionBlocker.CanInteract(verbs.User, uid) || !_actionBlocker.CanComplexInteract(verbs.User))
{
return;
}
if (component.NameSet && component.NetworkSet)
{
return;
}
AlternativeVerb verb = new();
verb.Text = Loc.GetString("surveillance-camera-setup");
verb.Act = () => OpenSetupInterface(uid, verbs.User, component);
verbs.Verbs.Add(verb);
}
private void OnPowerChanged(EntityUid camera, SurveillanceCameraComponent component, ref PowerChangedEvent args)
{
SetActive(camera, args.Powered, component);
@@ -173,6 +150,7 @@ public sealed class SurveillanceCameraSystem : EntitySystem
component.CameraId = args.Name;
component.NameSet = true;
Dirty(uid, component);
UpdateSetupInterface(uid, component);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"{ToPrettyString(args.Actor)} set the name of {ToPrettyString(uid)} to \"{args.Name}.\"");
}
@@ -198,10 +176,11 @@ public sealed class SurveillanceCameraSystem : EntitySystem
_deviceNetworkSystem.SetReceiveFrequency(uid, frequency.Frequency);
component.NetworkSet = true;
Dirty(uid, component);
UpdateSetupInterface(uid, component);
}
private void OpenSetupInterface(EntityUid uid, EntityUid player, SurveillanceCameraComponent? camera = null)
protected override void OpenSetupInterface(EntityUid uid, EntityUid player, SurveillanceCameraComponent? camera = null)
{
if (!Resolve(uid, ref camera))
return;
@@ -271,7 +250,7 @@ public sealed class SurveillanceCameraSystem : EntitySystem
UpdateVisuals(camera, component);
}
public void SetActive(EntityUid camera, bool setting, SurveillanceCameraComponent? component = null)
public override void SetActive(EntityUid camera, bool setting, SurveillanceCameraComponent? component = null)
{
if (!Resolve(camera, ref component))
{
@@ -418,21 +397,6 @@ public sealed class SurveillanceCameraSystem : EntitySystem
_appearance.SetData(uid, SurveillanceCameraVisualsKey.Key, key, appearance);
}
private void OnEmpPulse(EntityUid uid, SurveillanceCameraComponent component, ref EmpPulseEvent args)
{
if (component.Active)
{
args.Affected = true;
args.Disabled = true;
SetActive(uid, false);
}
}
private void OnEmpDisabledRemoved(EntityUid uid, SurveillanceCameraComponent component, ref EmpDisabledRemoved args)
{
SetActive(uid, true);
}
}
public sealed class OnSurveillanceCameraViewerAddEvent : EntityEventArgs

View File

@@ -1,7 +1,7 @@
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Tesla.Components;
using Content.Server.Lightning;
using Content.Shared.Power.Components;
namespace Content.Server.Tesla.EntitySystems;

View File

@@ -1,9 +1,7 @@
using System.Linq;
using System.Numerics;
using Content.Server.Cargo.Systems;
using Content.Server.Emp;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Vocalization.Systems;
using Content.Shared.Cargo;
using Content.Shared.Damage;
@@ -35,7 +33,6 @@ namespace Content.Server.VendingMachines
SubscribeLocalEvent<VendingMachineComponent, BreakageEventArgs>(OnBreak);
SubscribeLocalEvent<VendingMachineComponent, DamageChangedEvent>(OnDamageChanged);
SubscribeLocalEvent<VendingMachineComponent, PriceCalculationEvent>(OnVendingPrice);
SubscribeLocalEvent<VendingMachineComponent, EmpPulseEvent>(OnEmpPulse);
SubscribeLocalEvent<VendingMachineComponent, TryVocalizeEvent>(OnTryVocalize);
SubscribeLocalEvent<VendingMachineComponent, ActivatableUIOpenAttemptEvent>(OnActivatableUIOpenAttempt);
@@ -86,6 +83,7 @@ namespace Content.Server.VendingMachines
private void OnBreak(EntityUid uid, VendingMachineComponent vendComponent, BreakageEventArgs eventArgs)
{
vendComponent.Broken = true;
Dirty(uid, vendComponent);
TryUpdateVisualState((uid, vendComponent));
}
@@ -94,6 +92,7 @@ namespace Content.Server.VendingMachines
if (!args.DamageIncreased && component.Broken)
{
component.Broken = false;
Dirty(uid, component);
TryUpdateVisualState((uid, component));
return;
}
@@ -257,16 +256,6 @@ namespace Content.Server.VendingMachines
args.Price += priceSets.Max();
}
private void OnEmpPulse(EntityUid uid, VendingMachineComponent component, ref EmpPulseEvent args)
{
if (!component.Broken && this.IsPowered(uid, EntityManager))
{
args.Affected = true;
args.Disabled = true;
component.NextEmpEject = Timing.CurTime;
}
}
private void OnTryVocalize(Entity<VendingMachineComponent> ent, ref TryVocalizeEvent args)
{
args.Cancelled |= ent.Comp.Broken;

View File

@@ -1,7 +1,6 @@
using Content.Server.Power.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.Events;
using Content.Shared.FixedPoint;
using Content.Shared.Power;
using Content.Shared.PowerCell.Components;
using Content.Shared.Projectiles;
using Content.Shared.Weapons.Ranged;

View File

@@ -22,5 +22,5 @@ public sealed partial class XAEEmpInAreaComponent : Component
/// Duration (in seconds) for which devices going to be disabled.
/// </summary>
[DataField]
public float DisableDuration = 60f;
public TimeSpan DisableDuration = TimeSpan.FromSeconds(60);
}

View File

@@ -1,6 +1,6 @@
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
using Content.Shared.Power.Components;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.XAE;

View File

@@ -61,5 +61,5 @@ public sealed partial class ElectricityAnomalyComponent : Component
/// Duration of devices being disabled by the emp pulse upon going supercritical.
/// <summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float EmpDisabledDuration = 60f;
public TimeSpan EmpDisabledDuration = TimeSpan.FromSeconds(60);
}

View File

@@ -2,13 +2,16 @@ using System.Linq;
using Content.Shared.Access.Components;
using Content.Shared.Clothing.Components;
using Content.Shared.Contraband;
using Content.Shared.Emp;
using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;
using Content.Shared.Item;
using Content.Shared.Lock;
using Content.Shared.Tag;
using Content.Shared.Verbs;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -23,8 +26,11 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
[Dependency] private readonly SharedItemSystem _itemSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] protected readonly IGameTiming _timing = default!;
[Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] private readonly LockSystem _lock = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] protected readonly SharedUserInterfaceSystem UI = default!;
[Dependency] private readonly INetManager _net = default!;
private static readonly SlotFlags[] IgnoredSlots =
{
@@ -32,12 +38,12 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
SlotFlags.PREVENTEQUIP,
SlotFlags.NONE
};
private static readonly SlotFlags[] Slots = Enum.GetValues<SlotFlags>().Except(IgnoredSlots).ToArray();
private readonly Dictionary<SlotFlags, List<EntProtoId>> _data = new();
public readonly Dictionary<SlotFlags, List<string>> ValidVariants = new();
[Dependency] protected readonly SharedUserInterfaceSystem UI = default!;
private static readonly ProtoId<TagPrototype> WhitelistChameleonTag = "WhitelistChameleon";
@@ -47,6 +53,7 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
SubscribeLocalEvent<ChameleonClothingComponent, GotEquippedEvent>(OnGotEquipped);
SubscribeLocalEvent<ChameleonClothingComponent, GotUnequippedEvent>(OnGotUnequipped);
SubscribeLocalEvent<ChameleonClothingComponent, GetVerbsEvent<InteractionVerb>>(OnVerb);
SubscribeLocalEvent<ChameleonClothingComponent, EmpPulseEvent>(OnEmpPulse);
SubscribeLocalEvent<ChameleonClothingComponent, PrototypesReloadedEventArgs>(OnPrototypeReload);
PrepareAllVariants();
@@ -97,21 +104,21 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
// clothing sprite logic
if (TryComp(uid, out ClothingComponent? clothing) &&
proto.TryGetComponent("Clothing", out ClothingComponent? otherClothing))
proto.TryGetComponent(out ClothingComponent? otherClothing, Factory))
{
_clothingSystem.CopyVisuals(uid, otherClothing, clothing);
}
// appearance data logic
if (TryComp(uid, out AppearanceComponent? appearance) &&
proto.TryGetComponent("Appearance", out AppearanceComponent? appearanceOther))
proto.TryGetComponent(out AppearanceComponent? appearanceOther, Factory))
{
_appearance.AppendData(appearanceOther, uid);
Dirty(uid, appearance);
}
// properly mark contraband
if (proto.TryGetComponent("Contraband", out ContrabandComponent? contra))
if (proto.TryGetComponent(out ContrabandComponent? contra, Factory))
{
EnsureComp<ContrabandComponent>(uid, out var current);
_contraband.CopyDetails(uid, contra, current);
@@ -138,6 +145,24 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
});
}
private void OnEmpPulse(EntityUid uid, ChameleonClothingComponent component, ref EmpPulseEvent args)
{
if (!component.AffectedByEmp)
return;
if (component.EmpContinuous)
component.NextEmpChange = Timing.CurTime + TimeSpan.FromSeconds(1f / component.EmpChangeIntensity);
if (_net.IsServer) // needs RandomPredicted
{
var pick = GetRandomValidPrototype(component.Slot, component.RequireTag);
SetSelectedPrototype(uid, pick, component: component);
}
args.Affected = true;
args.Disabled = true;
}
protected virtual void UpdateSprite(EntityUid uid, EntityPrototype proto) { }
/// <summary>
@@ -157,7 +182,7 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
return false;
// check if it's valid clothing
if (!proto.TryGetComponent("Clothing", out ClothingComponent? clothing))
if (!proto.TryGetComponent(out ClothingComponent? clothing, Factory))
return false;
if (!clothing.Slots.HasFlag(chameleonSlot))
return false;
@@ -187,6 +212,14 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
return validTargets;
}
/// <summary>
/// Get a random prototype for a given slot.
/// </summary>
public string GetRandomValidPrototype(SlotFlags slot, string? tag = null)
{
return _random.Pick(GetValidTargets(slot, tag).ToList());
}
protected void PrepareAllVariants()
{
_data.Clear();
@@ -215,4 +248,9 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
}
}
}
// TODO: Predict and use component states for the UI
public virtual void SetSelectedPrototype(EntityUid uid, string? protoId, bool forceUpdate = false,
ChameleonClothingComponent? component = null)
{ }
}

View File

@@ -5,24 +5,30 @@ namespace Content.Shared.Emp;
/// <summary>
/// While entity has this component it is "disabled" by EMP.
/// Add desired behaviour in other systems
/// Add desired behaviour in other systems.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentPause]
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState, AutoGenerateComponentPause]
[Access(typeof(SharedEmpSystem))]
public sealed partial class EmpDisabledComponent : Component
{
/// <summary>
/// Moment of time when component is removed and entity stops being "disabled"
/// Moment of time when the component is removed and entity stops being "disabled".
/// </summary>
[DataField("timeLeft", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
[AutoPausedField]
public TimeSpan DisabledUntil;
[DataField("effectCoolDown"), ViewVariables(VVAccess.ReadWrite)]
public float EffectCooldown = 3f;
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoNetworkedField, AutoPausedField]
public TimeSpan DisabledUntil = TimeSpan.Zero;
/// <summary>
/// When next effect will be spawned
/// Default time between visual effect spawns.
/// This gets a random multiplier.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan EffectCooldown = TimeSpan.FromSeconds(3);
/// <summary>
/// When next effect will be spawned.
/// TODO: Particle system.
/// </summary>
[AutoPausedField]
public TimeSpan TargetTime = TimeSpan.Zero;

View File

@@ -1,21 +1,58 @@
using Content.Shared.Examine;
using Content.Shared.Rejuvenate;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
using Robust.Shared.Timing;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
namespace Content.Shared.Emp;
public abstract class SharedEmpSystem : EntitySystem
{
[Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
private HashSet<EntityUid> _entSet = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<EmpDisabledComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<EmpDisabledComponent, ComponentRemove>(OnRemove);
SubscribeLocalEvent<EmpDisabledComponent, RejuvenateEvent>(OnRejuvenate);
}
protected const string EmpDisabledEffectPrototype = "EffectEmpDisabled";
public static readonly EntProtoId EmpPulseEffectPrototype = "EffectEmpPulse";
public static readonly EntProtoId EmpDisabledEffectPrototype = "EffectEmpDisabled";
public static readonly SoundSpecifier EmpSound = new SoundPathSpecifier("/Audio/Effects/Lightning/lightningbolt.ogg");
/// <summary>
/// Triggers an EMP pulse at the given location, by first raising an <see cref="EmpAttemptEvent"/>, then by raising <see cref="EmpPulseEvent"/> on all entities in range.
/// </summary>
/// <param name="coordinates">The location to trigger the EMP pulse at.</param>
/// <param name="range">The range of the EMP pulse.</param>
/// <param name="energyConsumption">The amount of energy consumed by the EMP pulse. In Joule.</param>
/// <param name="duration">The duration of the EMP effects.</param>
/// <param name="user">The player that caused the effect. Used for predicted audio.</param>
public void EmpPulse(MapCoordinates mapCoordinates, float range, float energyConsumption, TimeSpan duration, EntityUid? user = null)
{
foreach (var uid in _lookup.GetEntitiesInRange(mapCoordinates, range))
{
TryEmpEffects(uid, energyConsumption, duration, user);
}
// TODO: replace with PredictedSpawn once it works with animated sprites
if (_net.IsServer)
Spawn(EmpPulseEffectPrototype, mapCoordinates);
var coordinates = _transform.ToCoordinates(mapCoordinates);
_audio.PlayPredicted(EmpSound, coordinates, user);
}
/// <summary>
/// Triggers an EMP pulse at the given location, by first raising an <see cref="EmpAttemptEvent"/>, then a raising <see cref="EmpPulseEvent"/> on all entities in range.
@@ -24,12 +61,119 @@ public abstract class SharedEmpSystem : EntitySystem
/// <param name="range">The range of the EMP pulse.</param>
/// <param name="energyConsumption">The amount of energy consumed by the EMP pulse.</param>
/// <param name="duration">The duration of the EMP effects.</param>
public virtual void EmpPulse(MapCoordinates coordinates, float range, float energyConsumption, float duration)
/// <param name="user">The player that caused the effect. Used for predicted audio.</param>
public void EmpPulse(EntityCoordinates coordinates, float range, float energyConsumption, TimeSpan duration, EntityUid? user = null)
{
_entSet.Clear();
_lookup.GetEntitiesInRange(coordinates, range, _entSet);
foreach (var uid in _entSet)
{
TryEmpEffects(uid, energyConsumption, duration, user);
}
// TODO: replace with PredictedSpawn once it works with animated sprites
if (_net.IsServer)
Spawn(EmpPulseEffectPrototype, coordinates);
_audio.PlayPredicted(EmpSound, coordinates, user);
}
/// <summary>
/// Attempts to apply the effects of an EMP pulse onto an entity by first raising an <see cref="EmpAttemptEvent"/>, followed by raising a <see cref="EmpPulseEvent"/> on it.
/// </summary>
/// <param name="uid">The entity to apply the EMP effects on.</param>
/// <param name="energyConsumption">The amount of energy consumed by the EMP.</param>
/// <param name="duration">The duration of the EMP effects.</param>
/// <param name="user">The player that caused the EMP. For prediction purposes.</param>
/// <returns>If the entity was affected by the EMP.</returns>
public bool TryEmpEffects(EntityUid uid, float energyConsumption, TimeSpan duration, EntityUid? user = null)
{
var attemptEv = new EmpAttemptEvent();
RaiseLocalEvent(uid, ref attemptEv);
if (attemptEv.Cancelled)
return false;
return DoEmpEffects(uid, energyConsumption, duration, user);
}
/// <summary>
/// Applies the effects of an EMP pulse onto an entity by raising a <see cref="EmpPulseEvent"/> on it.
/// </summary>
/// <param name="uid">The entity to apply the EMP effects on.</param>
/// <param name="energyConsumption">The amount of energy consumed by the EMP.</param>
/// <param name="duration">The duration of the EMP effects.</param>
/// <param name="user">The player that caused the EMP. For prediction purposes.</param>
/// <returns>If the entity was affected by the EMP.</returns>
public bool DoEmpEffects(EntityUid uid, float energyConsumption, TimeSpan duration, EntityUid? user = null)
{
var ev = new EmpPulseEvent(energyConsumption, false, false, duration, user);
RaiseLocalEvent(uid, ref ev);
// TODO: replace with PredictedSpawn once it works with animated sprites
if (ev.Affected && _net.IsServer)
Spawn(EmpDisabledEffectPrototype, Transform(uid).Coordinates);
if (!ev.Disabled)
return ev.Affected;
var disabled = EnsureComp<EmpDisabledComponent>(uid);
disabled.DisabledUntil = Timing.CurTime + duration;
Dirty(uid, disabled);
return ev.Affected;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var curTime = Timing.CurTime;
var query = EntityQueryEnumerator<EmpDisabledComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (curTime < comp.DisabledUntil)
continue;
RemComp<EmpDisabledComponent>(uid);
}
}
private void OnExamine(Entity<EmpDisabledComponent> ent, ref ExaminedEvent args)
{
args.PushMarkup(Loc.GetString("emp-disabled-comp-on-examine"));
}
private void OnRemove(Entity<EmpDisabledComponent> ent, ref ComponentRemove args)
{
var ev = new EmpDisabledRemovedEvent();
RaiseLocalEvent(ent, ref ev);
}
private void OnRejuvenate(Entity<EmpDisabledComponent> ent, ref RejuvenateEvent args)
{
RemCompDeferred<EmpDisabledComponent>(ent);
}
}
/// <summary>
/// Raised on an entity before <see cref="EmpPulseEvent"/>. Cancel this to prevent the emp event being raised.
/// </summary>
[ByRefEvent]
public record struct EmpAttemptEvent(bool Cancelled);
/// <summary>
/// Raised on an entity when it gets hit by an EMP Pulse.
/// </summary>
/// <param name="EnergyConsumption">The amount of energy to remove from batteries. In Joule.</param>
/// <param name="Affected">Set this is true in the subscription to spawn a visual effect at the entity's location.</param>
/// <param name="Disabled">Set this to ture in the subscription to add <see cref="EmpDisabledComponent"/> to the entity.</param>
/// <param name="Duration">The duration the entity will be disabled.</param>
/// <param name="User">The player that caused the EMP. For prediction purposes.</param>
[ByRefEvent]
public record struct EmpPulseEvent(float EnergyConsumption, bool Affected, bool Disabled, TimeSpan Duration, EntityUid? User);
/// <summary>
/// Raised on an entity after <see cref="EmpDisabledComponent"/> is removed.
/// </summary>
[ByRefEvent]
public record struct EmpDisabledRemovedEvent();

View File

@@ -27,7 +27,7 @@ public sealed partial class EmpReactionEffect : EventEntityEffect<EmpReactionEff
/// Amount of time entities will be disabled
/// </summary>
[DataField("duration")]
public float DisableDuration = 15;
public TimeSpan DisableDuration = TimeSpan.FromSeconds(15);
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
=> Loc.GetString("reagent-effect-guidebook-emp-reaction-effect", ("chance", Probability));

View File

@@ -7,6 +7,7 @@ using Content.Shared.DeviceLinking.Events;
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Events;
using Content.Shared.DoAfter;
using Content.Shared.Emp;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Light.Components;
@@ -52,6 +53,7 @@ public abstract class SharedPoweredLightSystem : EntitySystem
SubscribeLocalEvent<PoweredLightComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<PoweredLightComponent, PoweredLightDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<PoweredLightComponent, DamageChangedEvent>(HandleLightDamaged);
SubscribeLocalEvent<PoweredLightComponent, EmpPulseEvent>(OnEmpPulse);
}
private void OnInit(EntityUid uid, PoweredLightComponent light, ComponentInit args)
@@ -230,29 +232,21 @@ public abstract class SharedPoweredLightSystem : EntitySystem
/// <summary>
/// Try to break bulb inside light fixture
/// </summary>
public bool TryDestroyBulb(EntityUid uid, PoweredLightComponent? light = null)
public bool TryDestroyBulb(EntityUid uid, PoweredLightComponent? light = null, EntityUid? user = null)
{
if (!Resolve(uid, ref light, false))
return false;
// if we aren't mapinited,
// just null the spawned bulb
if (LifeStage(uid) < EntityLifeStage.MapInitialized)
{
light.HasLampOnSpawn = null;
return true;
}
// check bulb state
var bulbUid = GetBulb(uid, light);
if (bulbUid == null || !EntityManager.TryGetComponent(bulbUid.Value, out LightBulbComponent? lightBulb))
if (bulbUid == null || !TryComp<LightBulbComponent>(bulbUid.Value, out var lightBulb))
return false;
if (lightBulb.State == LightBulbState.Broken)
return false;
// break it
_bulbSystem.SetState(bulbUid.Value, LightBulbState.Broken, lightBulb);
_bulbSystem.PlayBreakSound(bulbUid.Value, lightBulb);
_bulbSystem.PlayBreakSound(bulbUid.Value, lightBulb, user);
UpdateLight(uid, light);
return true;
}
@@ -327,13 +321,18 @@ public abstract class SharedPoweredLightSystem : EntitySystem
/// <summary>
/// Destroy the light bulb if the light took any damage.
/// </summary>
/// <remarks>
/// TODO: This should be an IThresholdBehaviour once DestructibleSystem is predicted.
/// </remarks>
public void HandleLightDamaged(EntityUid uid, PoweredLightComponent component, DamageChangedEvent args)
{
if (GameTiming.ApplyingState) // The destruction is already networked on its own.
return;
// Was it being repaired, or did it take damage?
if (args.DamageIncreased)
{
// Eventually, this logic should all be done by this (or some other) system, not a component.
TryDestroyBulb(uid, component);
TryDestroyBulb(uid, component, args.Origin);
}
}
@@ -348,6 +347,12 @@ public abstract class SharedPoweredLightSystem : EntitySystem
UpdateLight(uid, component);
}
private void OnEmpPulse(EntityUid uid, PoweredLightComponent component, ref EmpPulseEvent args)
{
if (TryDestroyBulb(uid, component))
args.Affected = true;
}
public void ToggleBlinkingLight(EntityUid uid, PoweredLightComponent light, bool isNowBlinking)
{
if (light.IsBlinking == isNowBlinking)

View File

@@ -5,6 +5,7 @@ using Content.Shared.Clothing;
using Content.Shared.Damage;
using Content.Shared.DeviceNetwork;
using Content.Shared.DoAfter;
using Content.Shared.Emp;
using Content.Shared.Examine;
using Content.Shared.GameTicking;
using Content.Shared.Interaction;
@@ -49,6 +50,8 @@ public abstract class SharedSuitSensorSystem : EntitySystem
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawn);
SubscribeLocalEvent<SuitSensorComponent, ClothingGotEquippedEvent>(OnEquipped);
SubscribeLocalEvent<SuitSensorComponent, ClothingGotUnequippedEvent>(OnUnequipped);
SubscribeLocalEvent<SuitSensorComponent, EmpPulseEvent>(OnEmpPulse);
SubscribeLocalEvent<SuitSensorComponent, EmpDisabledRemovedEvent>(OnEmpFinished);
SubscribeLocalEvent<SuitSensorComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<SuitSensorComponent, GetVerbsEvent<Verb>>(OnVerb);
SubscribeLocalEvent<SuitSensorComponent, EntGotInsertedIntoContainerMessage>(OnInsert);
@@ -133,6 +136,25 @@ public abstract class SharedSuitSensorSystem : EntitySystem
Dirty(ent);
}
private void OnEmpPulse(Entity<SuitSensorComponent> ent, ref EmpPulseEvent args)
{
args.Affected = true;
args.Disabled = true;
ent.Comp.PreviousMode = ent.Comp.Mode;
SetSensor(ent.AsNullable(), SuitSensorMode.SensorOff, null);
ent.Comp.PreviousControlsLocked = ent.Comp.ControlsLocked;
ent.Comp.ControlsLocked = true;
// SetSensor already calls Dirty
}
private void OnEmpFinished(Entity<SuitSensorComponent> ent, ref EmpDisabledRemovedEvent args)
{
SetSensor(ent.AsNullable(), ent.Comp.PreviousMode, null);
ent.Comp.ControlsLocked = ent.Comp.PreviousControlsLocked;
}
private void OnExamine(Entity<SuitSensorComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)

View File

@@ -85,13 +85,13 @@ public sealed partial class SuitSensorComponent : Component
/// <summary>
/// The previous mode of the suit. This is used to restore the state when an EMP effect ends.
/// </summary>
[DataField, ViewVariables]
[DataField, AutoNetworkedField, ViewVariables]
public SuitSensorMode PreviousMode = SuitSensorMode.SensorOff;
/// <summary>
/// The previous locked status of the controls. This is used to restore the state when an EMP effect ends.
/// This keeps prisoner jumpsuits/internal implants from becoming unlocked after an EMP.
/// </summary>
[DataField, ViewVariables]
[DataField, AutoNetworkedField, ViewVariables]
public bool PreviousControlsLocked = false;
}

View File

@@ -72,10 +72,10 @@ public sealed partial class NinjaSuitComponent : Component
public float EmpConsumption = 100000f;
/// <summary>
/// How long the EMP effects last for, in seconds
/// How long the EMP effects last for
/// </summary>
[DataField]
public float EmpDuration = 60f;
public TimeSpan EmpDuration = TimeSpan.FromSeconds(60);
}
public sealed partial class RecallKatanaEvent : InstantActionEvent;

View File

@@ -1,7 +1,7 @@
using Content.Shared.Actions;
using Content.Shared.Clothing;
using Content.Shared.Clothing.Components;
using Content.Shared.Clothing.EntitySystems;
using Content.Shared.Emp;
using Content.Shared.Inventory.Events;
using Content.Shared.Item.ItemToggle;
using Content.Shared.Item.ItemToggle.Components;
@@ -18,8 +18,8 @@ namespace Content.Shared.Ninja.Systems;
public abstract class SharedNinjaSuitSystem : EntitySystem
{
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly ItemToggleSystem _toggle = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] protected readonly SharedPopupSystem Popup = default!;
[Dependency] private readonly SharedSpaceNinjaSystem _ninja = default!;
[Dependency] private readonly UseDelaySystem _useDelay = default!;
@@ -36,6 +36,7 @@ public abstract class SharedNinjaSuitSystem : EntitySystem
SubscribeLocalEvent<NinjaSuitComponent, CreateItemAttemptEvent>(OnCreateStarAttempt);
SubscribeLocalEvent<NinjaSuitComponent, ItemToggleActivateAttemptEvent>(OnActivateAttempt);
SubscribeLocalEvent<NinjaSuitComponent, GotUnequippedEvent>(OnUnequipped);
SubscribeLocalEvent<NinjaSuitComponent, EmpAttemptEvent>(OnEmpAttempt);
}
private void OnEquipped(Entity<NinjaSuitComponent> ent, ref ClothingGotEquippedEvent args)
@@ -171,4 +172,11 @@ public abstract class SharedNinjaSuitSystem : EntitySystem
if (user.Comp.Gloves is { } uid)
_toggle.TryDeactivate(uid, user: user);
}
private void OnEmpAttempt(Entity<NinjaSuitComponent> ent, ref EmpAttemptEvent args)
{
// ninja suit (battery) is immune to emp
// powercell relays the event to suit
args.Cancelled = true;
}
}

View File

@@ -0,0 +1,33 @@
namespace Content.Shared.Power;
/// <summary>
/// Raised when a battery's charge or capacity changes (capacity affects relative charge percentage).
/// </summary>
[ByRefEvent]
public readonly record struct ChargeChangedEvent(float Charge, float MaxCharge);
/// <summary>
/// Raised when it is necessary to get information about battery charges.
/// </summary>
[ByRefEvent]
public sealed class GetChargeEvent : EntityEventArgs
{
public float CurrentCharge;
public float MaxCharge;
}
/// <summary>
/// Raised when it is necessary to change the current battery charge to a some value.
/// </summary>
[ByRefEvent]
public sealed class ChangeChargeEvent : EntityEventArgs
{
public float OriginalValue;
public float ResidualValue;
public ChangeChargeEvent(float value)
{
OriginalValue = value;
ResidualValue = value;
}
}

View File

@@ -6,7 +6,7 @@ namespace Content.Shared.Power.Components;
/// <summary>
/// Attached to APC powered entities that possess a rechargeable internal battery.
/// If external power is interrupted, the entity will draw power from this battery instead.
/// Requires <see cref="Content.Server.Power.Components.ApcPowerReceiverComponent"/> and <see cref="Content.Server.Power.Components.BatteryComponent"/> to function.
/// Requires <see cref="Content.Server.Power.Components.ApcPowerReceiverComponent"/> and <see cref="BatteryComponent"/> to function.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedPowerNetSystem), typeof(SharedPowerReceiverSystem))]

View File

@@ -0,0 +1,34 @@
using Content.Shared.Power.EntitySystems;
using Content.Shared.Guidebook;
namespace Content.Shared.Power.Components;
/// <summary>
/// Battery node on the pow3r network. Needs other components to connect to actual networks.
/// </summary>
[RegisterComponent]
[Virtual]
[Access(typeof(SharedBatterySystem))]
public partial class BatteryComponent : Component
{
public string SolutionName = "battery";
/// <summary>
/// Maximum charge of the battery in joules (ie. watt seconds)
/// </summary>
[DataField]
[GuidebookData]
public float MaxCharge;
/// <summary>
/// Current charge of the battery in joules (ie. watt seconds)
/// </summary>
[DataField("startingCharge")]
public float CurrentCharge;
/// <summary>
/// The price per one joule. Default is 1 credit for 10kJ.
/// </summary>
[DataField]
public float PricePerJoule = 0.0001f;
}

View File

@@ -0,0 +1,36 @@
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
namespace Content.Shared.Power.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class ChargerComponent : Component
{
[ViewVariables]
public CellChargerStatus Status;
/// <summary>
/// The charge rate of the charger, in watts
/// </summary>
[DataField]
public float ChargeRate = 20.0f;
/// <summary>
/// The container ID that is holds the entities being charged.
/// </summary>
[DataField(required: true)]
public string SlotId = string.Empty;
/// <summary>
/// A whitelist for what entities can be charged by this Charger.
/// </summary>
[DataField]
public EntityWhitelist? Whitelist;
/// <summary>
/// Indicates whether the charger is portable and thus subject to EMP effects
/// and bypasses checks for transform, anchored, and ApcPowerReceiverComponent.
/// </summary>
[DataField]
public bool Portable = false;
}

View File

@@ -0,0 +1,44 @@
using Content.Shared.Emp;
using Content.Shared.Power.Components;
namespace Content.Shared.Power.EntitySystems;
public abstract class SharedBatterySystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BatteryComponent, EmpPulseEvent>(OnEmpPulse);
}
private void OnEmpPulse(EntityUid uid, BatteryComponent component, ref EmpPulseEvent args)
{
args.Affected = true;
UseCharge(uid, args.EnergyConsumption, component);
// Apply a cooldown to the entity's self recharge if needed to avoid it immediately self recharging after an EMP.
TrySetChargeCooldown(uid);
}
public virtual float UseCharge(EntityUid uid, float value, BatteryComponent? battery = null)
{
return 0f;
}
public virtual void SetMaxCharge(EntityUid uid, float value, BatteryComponent? battery = null) { }
public virtual float ChangeCharge(EntityUid uid, float value, BatteryComponent? battery = null)
{
return 0f;
}
/// <summary>
/// Checks if the entity has a self recharge and puts it on cooldown if applicable.
/// </summary>
public virtual void TrySetChargeCooldown(EntityUid uid, float value = -1) { }
public virtual bool TryUseCharge(EntityUid uid, float value, BatteryComponent? battery = null)
{
return false;
}
}

View File

@@ -0,0 +1,20 @@
using Content.Shared.Emp;
using Content.Shared.Power.Components;
namespace Content.Shared.Power.EntitySystems;
public abstract class SharedChargerSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ChargerComponent, EmpPulseEvent>(OnEmpPulse);
}
private void OnEmpPulse(EntityUid uid, ChargerComponent component, ref EmpPulseEvent args)
{
args.Affected = true;
args.Disabled = true;
}
}

View File

@@ -1,4 +1,5 @@
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Emp;
using Content.Shared.PowerCell.Components;
using Content.Shared.Rejuvenate;
using Robust.Shared.Containers;
@@ -22,6 +23,8 @@ public abstract class SharedPowerCellSystem : EntitySystem
SubscribeLocalEvent<PowerCellSlotComponent, EntInsertedIntoContainerMessage>(OnCellInserted);
SubscribeLocalEvent<PowerCellSlotComponent, EntRemovedFromContainerMessage>(OnCellRemoved);
SubscribeLocalEvent<PowerCellSlotComponent, ContainerIsInsertingAttemptEvent>(OnCellInsertAttempt);
SubscribeLocalEvent<PowerCellComponent, EmpAttemptEvent>(OnCellEmpAttempt);
}
private void OnMapInit(Entity<PowerCellDrawComponent> ent, ref MapInitEvent args)
@@ -71,6 +74,14 @@ public abstract class SharedPowerCellSystem : EntitySystem
RaiseLocalEvent(uid, new PowerCellChangedEvent(true), false);
}
private void OnCellEmpAttempt(EntityUid uid, PowerCellComponent component, EmpAttemptEvent args)
{
var parent = Transform(uid).ParentUid;
// relay the attempt event to the slot so it can cancel it
if (HasComp<PowerCellSlotComponent>(parent))
RaiseLocalEvent(parent, ref args);
}
public void SetDrawEnabled(Entity<PowerCellDrawComponent?> ent, bool enabled)
{
if (!Resolve(ent, ref ent.Comp, false) || ent.Comp.Enabled == enabled)

View File

@@ -1,18 +1,20 @@
using Content.Shared.Inventory;
using Robust.Shared.GameStates;
namespace Content.Shared.Radio.Components;
/// <summary>
/// This component relays radio messages to the parent entity's chat when equipped.
/// </summary>
[RegisterComponent]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class HeadsetComponent : Component
{
[DataField("enabled")]
[DataField, AutoNetworkedField]
public bool Enabled = true;
[DataField, AutoNetworkedField]
public bool IsEquipped = false;
[DataField("requiredSlot")]
[DataField, AutoNetworkedField]
public SlotFlags RequiredSlot = SlotFlags.EARS;
}

View File

@@ -1,3 +1,4 @@
using Content.Shared.Emp;
using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;
using Content.Shared.Radio.Components;
@@ -9,9 +10,11 @@ public abstract class SharedHeadsetSystem : EntitySystem
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HeadsetComponent, InventoryRelayedEvent<GetDefaultRadioChannelEvent>>(OnGetDefault);
SubscribeLocalEvent<HeadsetComponent, GotEquippedEvent>(OnGotEquipped);
SubscribeLocalEvent<HeadsetComponent, GotUnequippedEvent>(OnGotUnequipped);
SubscribeLocalEvent<HeadsetComponent, EmpPulseEvent>(OnEmpPulse);
}
private void OnGetDefault(EntityUid uid, HeadsetComponent component, InventoryRelayedEvent<GetDefaultRadioChannelEvent> args)
@@ -29,10 +32,21 @@ public abstract class SharedHeadsetSystem : EntitySystem
protected virtual void OnGotEquipped(EntityUid uid, HeadsetComponent component, GotEquippedEvent args)
{
component.IsEquipped = args.SlotFlags.HasFlag(component.RequiredSlot);
Dirty(uid, component);
}
protected virtual void OnGotUnequipped(EntityUid uid, HeadsetComponent component, GotUnequippedEvent args)
{
component.IsEquipped = false;
Dirty(uid, component);
}
private void OnEmpPulse(Entity<HeadsetComponent> ent, ref EmpPulseEvent args)
{
if (ent.Comp.Enabled)
{
args.Affected = true;
args.Disabled = true;
}
}
}

View File

@@ -1,10 +1,11 @@
using Content.Shared.DeviceNetwork;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Server.SurveillanceCamera;
namespace Content.Shared.SurveillanceCamera.Components;
[RegisterComponent]
[Access(typeof(SurveillanceCameraSystem))]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedSurveillanceCameraSystem))]
public sealed partial class SurveillanceCameraComponent : Component
{
// List of active viewers. This is for bookkeeping purposes,
@@ -24,23 +25,20 @@ public sealed partial class SurveillanceCameraComponent : Component
// If this camera is active or not. Deactivating a camera
// will not allow it to obtain any new viewers.
[ViewVariables]
public bool Active { get; set; } = true;
[DataField]
public bool Active = true;
// This one isn't easy to deal with. Will require a UI
// to change/set this so mapping these in isn't
// the most terrible thing possible.
[ViewVariables(VVAccess.ReadWrite)]
[DataField("id")]
public string CameraId { get; set; } = "camera";
public string CameraId = "camera";
[ViewVariables(VVAccess.ReadWrite)]
[DataField("nameSet")]
public bool NameSet { get; set; }
[DataField, AutoNetworkedField]
public bool NameSet;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("networkSet")]
public bool NetworkSet { get; set; }
[DataField, AutoNetworkedField]
public bool NetworkSet;
// This has to be device network frequency prototypes.
[DataField("setupAvailableNetworks")]

View File

@@ -1,8 +1,56 @@
using Robust.Shared.GameStates;
using Content.Shared.Emp;
using Content.Shared.SurveillanceCamera.Components;
using Content.Shared.Verbs;
using Robust.Shared.Serialization;
namespace Content.Shared.SurveillanceCamera;
public abstract class SharedSurveillanceCameraSystem : EntitySystem
{
public override void Initialize()
{
SubscribeLocalEvent<SurveillanceCameraComponent, GetVerbsEvent<AlternativeVerb>>(AddVerbs);
SubscribeLocalEvent<SurveillanceCameraComponent, EmpPulseEvent>(OnEmpPulse);
SubscribeLocalEvent<SurveillanceCameraComponent, EmpDisabledRemovedEvent>(OnEmpDisabledRemoved);
}
private void AddVerbs(EntityUid uid, SurveillanceCameraComponent component, GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanInteract || !args.CanComplexInteract)
return;
if (component.NameSet && component.NetworkSet)
return;
AlternativeVerb verb = new()
{
Text = Loc.GetString("surveillance-camera-setup"),
Act = () => OpenSetupInterface(uid, args.User, component)
};
args.Verbs.Add(verb);
}
private void OnEmpPulse(EntityUid uid, SurveillanceCameraComponent component, ref EmpPulseEvent args)
{
if (component.Active)
{
args.Affected = true;
args.Disabled = true;
SetActive(uid, false);
}
}
private void OnEmpDisabledRemoved(EntityUid uid, SurveillanceCameraComponent component, ref EmpDisabledRemovedEvent args)
{
SetActive(uid, true);
}
// TODO: predict the rest of the server side system
public virtual void SetActive(EntityUid camera, bool setting, SurveillanceCameraComponent? component = null) { }
protected virtual void OpenSetupInterface(EntityUid uid, EntityUid player, SurveillanceCameraComponent? camera = null) { }
}
[Serializable, NetSerializable]
public enum SurveillanceCameraVisualsKey : byte
{

View File

@@ -6,7 +6,6 @@ namespace Content.Shared.Trigger.Systems;
public sealed class EmpOnTriggerSystem : EntitySystem
{
[Dependency] private readonly SharedEmpSystem _emp = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public override void Initialize()
{
@@ -25,7 +24,7 @@ public sealed class EmpOnTriggerSystem : EntitySystem
if (target == null)
return;
_emp.EmpPulse(_transform.GetMapCoordinates(target.Value), ent.Comp.Range, ent.Comp.EnergyConsumption, (float)ent.Comp.DisableDuration.TotalSeconds);
_emp.EmpPulse(Transform(target.Value).Coordinates, ent.Comp.Range, ent.Comp.EnergyConsumption, ent.Comp.DisableDuration, args.User);
args.Handled = true;
}
}

View File

@@ -1,19 +1,19 @@
using Content.Shared.Emag.Components;
using Robust.Shared.Prototypes;
using System.Linq;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Advertise.Components;
using Content.Shared.Advertise.Systems;
using Content.Shared.DoAfter;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.Emp;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Power.EntitySystems;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
@@ -41,6 +41,7 @@ public abstract partial class SharedVendingMachineSystem : EntitySystem
SubscribeLocalEvent<VendingMachineComponent, ComponentGetState>(OnVendingGetState);
SubscribeLocalEvent<VendingMachineComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<VendingMachineComponent, GotEmaggedEvent>(OnEmagged);
SubscribeLocalEvent<VendingMachineComponent, EmpPulseEvent>(OnEmpPulse);
SubscribeLocalEvent<VendingMachineComponent, RestockDoAfterEvent>(OnRestockDoAfter);
SubscribeLocalEvent<VendingMachineRestockComponent, AfterInteractEvent>(OnAfterInteract);
@@ -83,6 +84,7 @@ public abstract partial class SharedVendingMachineSystem : EntitySystem
EjectEnd = component.EjectEnd,
DenyEnd = component.DenyEnd,
DispenseOnHitEnd = component.DispenseOnHitEnd,
Broken = component.Broken,
};
}
@@ -145,6 +147,16 @@ public abstract partial class SharedVendingMachineSystem : EntitySystem
RestockInventoryFromPrototype(uid, component, component.InitialStockQuality);
}
private void OnEmpPulse(Entity<VendingMachineComponent> ent, ref EmpPulseEvent args)
{
if (!ent.Comp.Broken && _receiver.IsPowered(ent.Owner))
{
args.Affected = true;
args.Disabled = true;
ent.Comp.NextEmpEject = Timing.CurTime;
}
}
protected virtual void EjectItem(EntityUid uid, VendingMachineComponent? vendComponent = null, bool forceEject = false) { }
/// <summary>

View File

@@ -67,6 +67,7 @@ namespace Content.Shared.VendingMachines
public string? NextItemToEject;
[DataField]
public bool Broken;
/// <summary>
@@ -300,5 +301,7 @@ namespace Content.Shared.VendingMachines
public TimeSpan? DenyEnd;
public TimeSpan? DispenseOnHitEnd;
public bool Broken;
}
}

View File

@@ -16,9 +16,6 @@
- type: Tag
tags:
- HideContextMenu
- type: EmitSoundOnSpawn
sound:
path: /Audio/Effects/Lightning/lightningbolt.ogg
- type: AnimationPlayer
- type: entity