diff --git a/Content.Server/Atmos/Monitor/Systems/FireAlarmSystem.cs b/Content.Server/Atmos/Monitor/Systems/FireAlarmSystem.cs index f6076821b9..eca7cc86fa 100644 --- a/Content.Server/Atmos/Monitor/Systems/FireAlarmSystem.cs +++ b/Content.Server/Atmos/Monitor/Systems/FireAlarmSystem.cs @@ -74,7 +74,7 @@ public sealed class FireAlarmSystem : EntitySystem } } - private void OnEmagged(EntityUid uid, FireAlarmComponent component, GotEmaggedEvent args) + private void OnEmagged(EntityUid uid, FireAlarmComponent component, ref GotEmaggedEvent args) { if (TryComp(uid, out var alarmable)) { diff --git a/Content.Server/Bed/BedSystem.cs b/Content.Server/Bed/BedSystem.cs index dcf94dfded..d39f24931c 100644 --- a/Content.Server/Bed/BedSystem.cs +++ b/Content.Server/Bed/BedSystem.cs @@ -112,7 +112,7 @@ namespace Content.Server.Bed UpdateMetabolisms(uid, component, args.Powered); } - private void OnEmagged(EntityUid uid, StasisBedComponent component, GotEmaggedEvent args) + private void OnEmagged(EntityUid uid, StasisBedComponent component, ref GotEmaggedEvent args) { // Repeatable // Reset any metabolisms first so they receive the multiplier correctly diff --git a/Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs b/Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs index 12979cccb3..62f4fc0e7d 100644 --- a/Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs +++ b/Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs @@ -89,7 +89,7 @@ namespace Content.Server.Chemistry.EntitySystems return inventory; } - private void OnEmagged(EntityUid uid, ReagentDispenserComponent reagentDispenser, GotEmaggedEvent args) + private void OnEmagged(EntityUid uid, ReagentDispenserComponent reagentDispenser, ref GotEmaggedEvent args) { if (!reagentDispenser.IsEmagged) { diff --git a/Content.Server/Doors/Systems/DoorSystem.cs b/Content.Server/Doors/Systems/DoorSystem.cs index 1651ca4ebd..cac9876d27 100644 --- a/Content.Server/Doors/Systems/DoorSystem.cs +++ b/Content.Server/Doors/Systems/DoorSystem.cs @@ -267,7 +267,7 @@ public sealed class DoorSystem : SharedDoorSystem if (Tags.HasTag(otherUid, "DoorBumpOpener")) TryOpen(uid, door, otherUid); } - private void OnEmagged(EntityUid uid, DoorComponent door, GotEmaggedEvent args) + private void OnEmagged(EntityUid uid, DoorComponent door, ref GotEmaggedEvent args) { if(TryComp(uid, out var airlockComponent)) { diff --git a/Content.Server/Emag/EmagSystem.cs b/Content.Server/Emag/EmagSystem.cs deleted file mode 100644 index bf174419b5..0000000000 --- a/Content.Server/Emag/EmagSystem.cs +++ /dev/null @@ -1,85 +0,0 @@ -using Content.Shared.Administration.Logs; -using Content.Shared.Database; -using Content.Shared.Emag.Components; -using Content.Shared.Emag.Systems; -using Content.Shared.IdentityManagement; -using Content.Shared.Interaction; -using Content.Shared.Popups; -using Content.Shared.Tag; -using Robust.Shared.Player; - -namespace Content.Server.Emag -{ - public sealed class EmagSystem : EntitySystem - { - [Dependency] private readonly SharedPopupSystem _popupSystem = default!; - [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; - - [Dependency] private readonly TagSystem _tagSystem = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnAfterInteract); - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - foreach (var emag in EntityManager.EntityQuery()) - { - if (emag.Charges == emag.MaxCharges) - { - emag.Accumulator = 0; - continue; - } - - emag.Accumulator += frameTime; - - if (emag.Accumulator < emag.RechargeTime) - { - continue; - } - - emag.Accumulator -= emag.RechargeTime; - emag.Charges++; - } - } - - private void OnAfterInteract(EntityUid uid, EmagComponent component, AfterInteractEvent args) - { - if (!args.CanReach || args.Target == null) - return; - - if (_tagSystem.HasTag(args.Target.Value, "EmagImmune")) - return; - - if (component.Charges <= 0) - { - _popupSystem.PopupEntity(Loc.GetString("emag-no-charges"), args.User, args.User); - return; - } - - var handled = DoEmag(args.Target, args.User); - if (handled) - { - _popupSystem.PopupEntity(Loc.GetString("emag-success", ("target", Identity.Entity(args.Target.Value, EntityManager))), args.User, - args.User, PopupType.Medium); - _adminLogger.Add(LogType.Emag, LogImpact.High, $"{ToPrettyString(args.User):player} emagged {ToPrettyString(args.Target.Value):target}"); - component.Charges--; - } - } - - public bool DoEmag(EntityUid? target, EntityUid? user) - { - if (target == null || user == null) - return false; - - var emaggedEvent = new GotEmaggedEvent(user.Value); - RaiseLocalEvent(target.Value, emaggedEvent, false); - return emaggedEvent.Handled; - } - } -} diff --git a/Content.Server/Fax/FaxSystem.cs b/Content.Server/Fax/FaxSystem.cs index 8ae7137731..b7126a192c 100644 --- a/Content.Server/Fax/FaxSystem.cs +++ b/Content.Server/Fax/FaxSystem.cs @@ -226,7 +226,7 @@ public sealed class FaxSystem : EntitySystem args.Handled = true; } - private void OnEmagged(EntityUid uid, FaxMachineComponent component, GotEmaggedEvent args) + private void OnEmagged(EntityUid uid, FaxMachineComponent component, ref GotEmaggedEvent args) { if (component.Emagged) return; diff --git a/Content.Server/Lock/LockSystem.cs b/Content.Server/Lock/LockSystem.cs index 8821cd86f0..c31e0ce8f3 100644 --- a/Content.Server/Lock/LockSystem.cs +++ b/Content.Server/Lock/LockSystem.cs @@ -188,7 +188,7 @@ namespace Content.Server.Lock args.Verbs.Add(verb); } - private void OnEmagged(EntityUid uid, LockComponent component, GotEmaggedEvent args) + private void OnEmagged(EntityUid uid, LockComponent component, ref GotEmaggedEvent args) { if (component.Locked) { diff --git a/Content.Server/Power/EntitySystems/ApcSystem.cs b/Content.Server/Power/EntitySystems/ApcSystem.cs index bb247ef50e..9e7ab87100 100644 --- a/Content.Server/Power/EntitySystems/ApcSystem.cs +++ b/Content.Server/Power/EntitySystems/ApcSystem.cs @@ -85,7 +85,7 @@ namespace Content.Server.Power.EntitySystems SoundSystem.Play(apc.OnReceiveMessageSound.GetSound(), Filter.Pvs(uid), uid, AudioParams.Default.WithVolume(-2f)); } - private void OnEmagged(EntityUid uid, ApcComponent comp, GotEmaggedEvent args) + private void OnEmagged(EntityUid uid, ApcComponent comp, ref GotEmaggedEvent args) { if(!comp.Emagged) { diff --git a/Content.Server/Recycling/RecyclerSystem.cs b/Content.Server/Recycling/RecyclerSystem.cs index 92fdabe111..43d274bcf4 100644 --- a/Content.Server/Recycling/RecyclerSystem.cs +++ b/Content.Server/Recycling/RecyclerSystem.cs @@ -189,7 +189,7 @@ namespace Content.Server.Recycling QueueDel(component.Owner); } - private void OnEmagged(EntityUid uid, RecyclerComponent component, GotEmaggedEvent args) + private void OnEmagged(EntityUid uid, RecyclerComponent component, ref GotEmaggedEvent args) { if (!component.Safe) return; component.Safe = false; diff --git a/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs b/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs index c48e744fca..66aa1a0151 100644 --- a/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs +++ b/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs @@ -5,7 +5,6 @@ using Content.Shared.Revenant; using Robust.Shared.Random; using Robust.Shared.Map; using Content.Shared.Tag; -using Content.Shared.Maps; using Content.Server.Storage.Components; using Content.Server.Light.Components; using Content.Server.Ghost; @@ -18,12 +17,10 @@ using Content.Server.Disease.Components; using Content.Shared.Item; using Content.Shared.Bed.Sleep; using System.Linq; -using Content.Server.Beam; -using Content.Server.Emag; -using Content.Server.Humanoid; using Content.Server.Maps; using Content.Server.Revenant.Components; using Content.Server.Store.Components; +using Content.Shared.Emag.Systems; using Content.Shared.FixedPoint; using Content.Shared.Humanoid; using Content.Shared.Mobs; @@ -339,7 +336,7 @@ public sealed partial class RevenantSystem foreach (var ent in _lookup.GetEntitiesInRange(uid, component.MalfunctionRadius)) { - _emag.DoEmag(ent, ent); //it emags itself. spooky. + _emag.DoEmagEffect(ent, ent); //it emags itself. spooky. } } } diff --git a/Content.Server/VendingMachines/VendingMachineSystem.cs b/Content.Server/VendingMachines/VendingMachineSystem.cs index a46f955385..7949b56ec4 100644 --- a/Content.Server/VendingMachines/VendingMachineSystem.cs +++ b/Content.Server/VendingMachines/VendingMachineSystem.cs @@ -131,7 +131,7 @@ namespace Content.Server.VendingMachines TryUpdateVisualState(uid, vendComponent); } - private void OnEmagged(EntityUid uid, VendingMachineComponent component, GotEmaggedEvent args) + private void OnEmagged(EntityUid uid, VendingMachineComponent component, ref GotEmaggedEvent args) { if (component.Emagged || component.EmaggedInventory.Count == 0 ) return; diff --git a/Content.Shared/Access/Systems/AccessReaderSystem.cs b/Content.Shared/Access/Systems/AccessReaderSystem.cs index 53aef04030..e19db766ab 100644 --- a/Content.Shared/Access/Systems/AccessReaderSystem.cs +++ b/Content.Shared/Access/Systems/AccessReaderSystem.cs @@ -44,7 +44,7 @@ namespace Content.Shared.Access.Systems } } - private void OnEmagged(EntityUid uid, AccessReaderComponent reader, GotEmaggedEvent args) + private void OnEmagged(EntityUid uid, AccessReaderComponent reader, ref GotEmaggedEvent args) { if (reader.Enabled) { diff --git a/Content.Shared/Emag/Components/EmagComponent.cs b/Content.Shared/Emag/Components/EmagComponent.cs index 4258a2b84e..e696671aff 100644 --- a/Content.Shared/Emag/Components/EmagComponent.cs +++ b/Content.Shared/Emag/Components/EmagComponent.cs @@ -1,18 +1,70 @@ -namespace Content.Shared.Emag.Components +using Content.Shared.Emag.Systems; +using Content.Shared.Tag; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Serialization; + +namespace Content.Shared.Emag.Components; + +[Access(typeof(EmagSystem))] +[RegisterComponent, NetworkedComponent] +public sealed class EmagComponent : Component { - [RegisterComponent] - public sealed class EmagComponent : Component + /// + /// The maximum number of charges the emag can have + /// + [DataField("maxCharges"), ViewVariables(VVAccess.ReadWrite)] + public int MaxCharges = 3; + + /// + /// The current number of charges on the emag + /// + [DataField("charges"), ViewVariables(VVAccess.ReadWrite)] + public int Charges = 3; + + /// + /// Whether or not the emag automatically recharges over time. + /// + [DataField("autoRecharge"), ViewVariables(VVAccess.ReadWrite)] + public bool AutoRecharge = true; + + /// + /// The time it takes to regain a single charge + /// + [DataField("rechargeDuration"), ViewVariables(VVAccess.ReadWrite)] + public TimeSpan RechargeDuration = TimeSpan.FromSeconds(90); + + /// + /// The time when the next charge will be added + /// + [DataField("nextChargeTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] + public TimeSpan NextChargeTime = TimeSpan.MaxValue; + + /// + /// The tag that marks an entity as immune to emags + /// + [DataField("emagImmuneTag", customTypeSerializer: typeof(PrototypeIdSerializer)), ViewVariables(VVAccess.ReadWrite)] + public string EmagImmuneTag = "EmagImmune"; +} + +[Serializable, NetSerializable] +public sealed class EmagComponentState : ComponentState +{ + public int MaxCharges; + public int Charges; + public bool AutoRecharge; + public TimeSpan RechargeTime; + public TimeSpan NextChargeTime; + public string EmagImmuneTag; + + public EmagComponentState(int maxCharges, int charges, TimeSpan rechargeTime, TimeSpan nextChargeTime, string emagImmuneTag, bool autoRecharge) { - [DataField("maxCharges"), ViewVariables(VVAccess.ReadWrite)] - public int MaxCharges = 3; - - [DataField("charges"), ViewVariables(VVAccess.ReadWrite)] - public int Charges = 3; - - [DataField("rechargeTime"), ViewVariables(VVAccess.ReadWrite)] - public float RechargeTime = 90f; - - [DataField("accumulator")] - public float Accumulator = 0f; + MaxCharges = maxCharges; + Charges = charges; + RechargeTime = rechargeTime; + NextChargeTime = nextChargeTime; + EmagImmuneTag = emagImmuneTag; + AutoRecharge = autoRecharge; } } diff --git a/Content.Shared/Emag/Systems/EmagSystem.cs b/Content.Shared/Emag/Systems/EmagSystem.cs new file mode 100644 index 0000000000..8d68131e42 --- /dev/null +++ b/Content.Shared/Emag/Systems/EmagSystem.cs @@ -0,0 +1,167 @@ +using Content.Shared.Administration.Logs; +using Content.Shared.Database; +using Content.Shared.Emag.Components; +using Content.Shared.Examine; +using Content.Shared.IdentityManagement; +using Content.Shared.Interaction; +using Content.Shared.Popups; +using Content.Shared.Tag; +using Robust.Shared.GameStates; +using Robust.Shared.Network; +using Robust.Shared.Timing; + +namespace Content.Shared.Emag.Systems +{ + /// How to add an emag interaction: + /// 1. Go to the system for the component you want the interaction with + /// 2. Subscribe to the GotEmaggedEvent + /// 3. Have some check for if this actually needs to be emagged or is already emagged (to stop charge waste) + /// 4. Past the check, add all the effects you desire and HANDLE THE EVENT ARGUMENT so a charge is spent + public sealed class EmagSystem : EntitySystem + { + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly TagSystem _tagSystem = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnExamine); + SubscribeLocalEvent(OnAfterInteract); + SubscribeLocalEvent(OnGetState); + SubscribeLocalEvent(OnHandleState); + SubscribeLocalEvent(OnUnpaused); + } + + private void OnGetState(EntityUid uid, EmagComponent component, ref ComponentGetState args) + { + args.State = new EmagComponentState(component.MaxCharges, component.Charges, component.RechargeDuration, + component.NextChargeTime, component.EmagImmuneTag, component.AutoRecharge); + } + + private void OnHandleState(EntityUid uid, EmagComponent component, ref ComponentHandleState args) + { + if (args.Current is not EmagComponentState state) + return; + + component.MaxCharges = state.MaxCharges; + component.Charges = state.Charges; + component.RechargeDuration = state.RechargeTime; + component.NextChargeTime = state.NextChargeTime; + component.EmagImmuneTag = state.EmagImmuneTag; + component.AutoRecharge = state.AutoRecharge; + } + + private void OnUnpaused(EntityUid uid, EmagComponent component, ref EntityUnpausedEvent args) + { + component.NextChargeTime += args.PausedTime; + } + + private void OnExamine(EntityUid uid, EmagComponent component, ExaminedEvent args) + { + args.PushMarkup(Loc.GetString("emag-charges-remaining", ("charges", component.Charges))); + if (component.Charges == component.MaxCharges) + { + args.PushMarkup(Loc.GetString("emag-max-charges")); + return; + } + var timeRemaining = Math.Round((component.NextChargeTime - _timing.CurTime).TotalSeconds); + args.PushMarkup(Loc.GetString("emag-recharging", ("seconds", timeRemaining))); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + foreach (var emag in EntityQuery()) + { + if (!emag.AutoRecharge) + continue; + + if (emag.Charges == emag.MaxCharges) + continue; + + if (_timing.CurTime < emag.NextChargeTime) + continue; + + ChangeEmagCharge(emag.Owner, 1, true, emag); + } + } + + private void OnAfterInteract(EntityUid uid, EmagComponent component, AfterInteractEvent args) + { + if (!args.CanReach || args.Target is not { } target) + return; + + args.Handled = TryUseEmag(uid, args.User, target, component); + } + + /// + /// Changes the charge on an emag. + /// + public bool ChangeEmagCharge(EntityUid uid, int change, bool resetTimer, EmagComponent? component = null) + { + if (!Resolve(uid, ref component)) + return false; + + if (component.Charges + change < 0 || component.Charges + change > component.MaxCharges) + return false; + + if (resetTimer || component.Charges == component.MaxCharges) + component.NextChargeTime = _timing.CurTime + component.RechargeDuration; + + component.Charges += change; + Dirty(component); + return true; + } + + /// + /// Tries to use the emag on a target entity + /// + public bool TryUseEmag(EntityUid emag, EntityUid user, EntityUid target, EmagComponent? component = null) + { + if (!Resolve(emag, ref component, false)) + return false; + + if (_tagSystem.HasTag(target, component.EmagImmuneTag)) + return false; + + if (component.Charges <= 0) + { + _popupSystem.PopupEntity(Loc.GetString("emag-no-charges"), user, user); + return false; + } + + var handled = DoEmagEffect(user, target); + if (!handled) + return false; + + // only do popup on client + if (_net.IsClient && _timing.IsFirstTimePredicted) + { + _popupSystem.PopupEntity(Loc.GetString("emag-success", ("target", Identity.Entity(target, EntityManager))), user, + user, PopupType.Medium); + } + + _adminLogger.Add(LogType.Emag, LogImpact.High, $"{ToPrettyString(user):player} emagged {ToPrettyString(target):target}"); + + ChangeEmagCharge(emag, -1, false, component); + return true; + } + + /// + /// Does the emag effect on a specified entity + /// + public bool DoEmagEffect(EntityUid user, EntityUid target) + { + var emaggedEvent = new GotEmaggedEvent(user); + RaiseLocalEvent(target, ref emaggedEvent); + return emaggedEvent.Handled; + } + } + + [ByRefEvent] + public record struct GotEmaggedEvent(EntityUid UserUid, bool Handled = false); +} diff --git a/Content.Shared/Emag/Systems/SharedEmagSystem.cs b/Content.Shared/Emag/Systems/SharedEmagSystem.cs deleted file mode 100644 index 3f08ec60fa..0000000000 --- a/Content.Shared/Emag/Systems/SharedEmagSystem.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Content.Shared.Emag.Components; -using Content.Shared.Examine; - -namespace Content.Shared.Emag.Systems -{ - /// How to add an emag interaction: - /// 1. Go to the system for the component you want the interaction with - /// 2. Subscribe to the GotEmaggedEvent - /// 3. Have some check for if this actually needs to be emagged or is already emagged (to stop charge waste) - /// 4. Past the check, add all the effects you desire and HANDLE THE EVENT ARGUMENT so a charge is spent - public sealed class SharedEmagSystem : EntitySystem - { - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnExamine); - } - - private void OnExamine(EntityUid uid, EmagComponent component, ExaminedEvent args) - { - float timeRemaining = component.RechargeTime - component.Accumulator; - args.PushMarkup(Loc.GetString("emag-charges-remaining", ("charges", component.Charges))); - if (component.Charges == component.MaxCharges) - { - args.PushMarkup(Loc.GetString("emag-max-charges")); - return; - } - args.PushMarkup(Loc.GetString("emag-recharging", ("seconds", Math.Round(timeRemaining)))); - } - } - - public sealed class GotEmaggedEvent : HandledEntityEventArgs - { - public readonly EntityUid UserUid; - - public GotEmaggedEvent(EntityUid userUid) - { - UserUid = userUid; - } - } -} diff --git a/Content.Shared/Medical/Cryogenics/SharedCryoPodSystem.cs b/Content.Shared/Medical/Cryogenics/SharedCryoPodSystem.cs index d29f6c277e..3c41cec3cd 100644 --- a/Content.Shared/Medical/Cryogenics/SharedCryoPodSystem.cs +++ b/Content.Shared/Medical/Cryogenics/SharedCryoPodSystem.cs @@ -123,7 +123,7 @@ public abstract partial class SharedCryoPodSystem: EntitySystem } } - protected void OnEmagged(EntityUid uid, SharedCryoPodComponent? cryoPodComponent, GotEmaggedEvent args) + protected void OnEmagged(EntityUid uid, SharedCryoPodComponent? cryoPodComponent, ref GotEmaggedEvent args) { if (!Resolve(uid, ref cryoPodComponent)) { diff --git a/Resources/Locale/en-US/emag/emag.ftl b/Resources/Locale/en-US/emag/emag.ftl index 0f4648be5e..7c9514cc1d 100644 --- a/Resources/Locale/en-US/emag/emag.ftl +++ b/Resources/Locale/en-US/emag/emag.ftl @@ -1,5 +1,12 @@ emag-success = The card zaps something in {THE($target)}. emag-no-charges = No charges left! -emag-charges-remaining = It has [color=fuchsia]{$charges}[/color] charges remaining. +emag-charges-remaining = {$charges -> + [one] It has [color=fuchsia]{$charges}[/color] charge remaining. + *[other] It has [color=fuchsia]{$charges}[/color] charges remaining. +} + emag-max-charges = It's at [color=green]maximum[/color] charges. -emag-recharging = There are [color=yellow]{$seconds}[/color] seconds left until the next charge. +emag-recharging = {$seconds -> + [one] There is [color=yellow]{$seconds}[/color] second left until the next charge. + *[other] There are [color=yellow]{$seconds}[/color] seconds left until the next charge. +} \ No newline at end of file