From 6ddd8761a95d3caec561d41aee35cb4ca1f45940 Mon Sep 17 00:00:00 2001
From: deltanedas <39013340+deltanedas@users.noreply.github.com>
Date: Wed, 19 Apr 2023 05:46:00 +0000
Subject: [PATCH] emag refactor (#15181)
* limitedcharges stuff from emag
* changes except broken
* fix
* the
* move recharging to server, emag namespace -> charges
* the
* use resolve
* pro
webedit gaming
* the
* the
---------
Co-authored-by: deltanedas <@deltanedas:kde.org>
---
.../Charges/Systems/ChargesSystem.cs | 5 +
.../Components/AutoRechargeComponent.cs | 25 ++
.../Charges/Systems/ChargesSystem.cs | 63 +++++
.../Components/LimitedChargesComponent.cs | 24 ++
.../Charges/Systems/SharedChargesSystem.cs | 62 +++++
.../Emag/Components/EmagComponent.cs | 56 +---
Content.Shared/Emag/Systems/EmagSystem.cs | 244 ++++++------------
Resources/Locale/en-US/emag/emag.ftl | 10 -
.../en-US/limited-charges/limited-charges.ftl | 10 +
.../Entities/Objects/Tools/emag.yml | 15 ++
10 files changed, 292 insertions(+), 222 deletions(-)
create mode 100644 Content.Client/Charges/Systems/ChargesSystem.cs
create mode 100644 Content.Server/Charges/Components/AutoRechargeComponent.cs
create mode 100644 Content.Server/Charges/Systems/ChargesSystem.cs
create mode 100644 Content.Shared/Charges/Components/LimitedChargesComponent.cs
create mode 100644 Content.Shared/Charges/Systems/SharedChargesSystem.cs
create mode 100644 Resources/Locale/en-US/limited-charges/limited-charges.ftl
diff --git a/Content.Client/Charges/Systems/ChargesSystem.cs b/Content.Client/Charges/Systems/ChargesSystem.cs
new file mode 100644
index 0000000000..9170ac5e94
--- /dev/null
+++ b/Content.Client/Charges/Systems/ChargesSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared.Charges.Systems;
+
+namespace Content.Client.Charges.Systems;
+
+public sealed class ChargesSystem : SharedChargesSystem { }
diff --git a/Content.Server/Charges/Components/AutoRechargeComponent.cs b/Content.Server/Charges/Components/AutoRechargeComponent.cs
new file mode 100644
index 0000000000..6a64c159a8
--- /dev/null
+++ b/Content.Server/Charges/Components/AutoRechargeComponent.cs
@@ -0,0 +1,25 @@
+using Content.Server.Charges.Systems;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Server.Charges.Components;
+
+///
+/// Something with limited charges that can be recharged automatically.
+/// Requires LimitedChargesComponent to function.
+///
+[RegisterComponent]
+[Access(typeof(ChargesSystem))]
+public sealed class AutoRechargeComponent : Component
+{
+ ///
+ /// 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))]
+ public TimeSpan NextChargeTime = TimeSpan.MaxValue;
+}
diff --git a/Content.Server/Charges/Systems/ChargesSystem.cs b/Content.Server/Charges/Systems/ChargesSystem.cs
new file mode 100644
index 0000000000..82758f4653
--- /dev/null
+++ b/Content.Server/Charges/Systems/ChargesSystem.cs
@@ -0,0 +1,63 @@
+using Content.Server.Charges.Components;
+using Content.Shared.Charges.Components;
+using Content.Shared.Charges.Systems;
+using Content.Shared.Examine;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Charges.Systems;
+
+public sealed class ChargesSystem : SharedChargesSystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnUnpaused);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var charges, out var recharge))
+ {
+ if (charges.Charges == charges.MaxCharges || _timing.CurTime < recharge.NextChargeTime)
+ continue;
+
+ AddCharges(uid, 1, charges);
+ recharge.NextChargeTime = _timing.CurTime + recharge.RechargeDuration;
+ }
+ }
+
+ private void OnUnpaused(EntityUid uid, AutoRechargeComponent comp, ref EntityUnpausedEvent args)
+ {
+ comp.NextChargeTime += args.PausedTime;
+ }
+
+ protected override void OnExamine(EntityUid uid, LimitedChargesComponent comp, ExaminedEvent args)
+ {
+ base.OnExamine(uid, comp, args);
+
+ // only show the recharging info if it's not full
+ if (!args.IsInDetailsRange || comp.Charges == comp.MaxCharges || !TryComp(uid, out var recharge))
+ return;
+
+ var timeRemaining = Math.Round((recharge.NextChargeTime - _timing.CurTime).TotalSeconds);
+ args.PushMarkup(Loc.GetString("limited-charges-recharging", ("seconds", timeRemaining)));
+ }
+
+ public override void UseCharge(EntityUid uid, LimitedChargesComponent? comp = null)
+ {
+ if (!Resolve(uid, ref comp, false))
+ return;
+
+ var startRecharge = comp.Charges == comp.MaxCharges;
+ base.UseCharge(uid, comp);
+ // start the recharge time after first use at full charge
+ if (startRecharge && TryComp(uid, out var recharge))
+ recharge.NextChargeTime = _timing.CurTime + recharge.RechargeDuration;
+ }
+}
diff --git a/Content.Shared/Charges/Components/LimitedChargesComponent.cs b/Content.Shared/Charges/Components/LimitedChargesComponent.cs
new file mode 100644
index 0000000000..6973ffbe72
--- /dev/null
+++ b/Content.Shared/Charges/Components/LimitedChargesComponent.cs
@@ -0,0 +1,24 @@
+using Content.Shared.Charges.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Charges.Components;
+
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(SharedChargesSystem))]
+[AutoGenerateComponentState]
+public sealed partial class LimitedChargesComponent : Component
+{
+ ///
+ /// The maximum number of charges
+ ///
+ [DataField("maxCharges"), ViewVariables(VVAccess.ReadWrite)]
+ [AutoNetworkedField]
+ public int MaxCharges = 3;
+
+ ///
+ /// The current number of charges
+ ///
+ [DataField("charges"), ViewVariables(VVAccess.ReadWrite)]
+ [AutoNetworkedField]
+ public int Charges = 3;
+}
diff --git a/Content.Shared/Charges/Systems/SharedChargesSystem.cs b/Content.Shared/Charges/Systems/SharedChargesSystem.cs
new file mode 100644
index 0000000000..be1526d3d3
--- /dev/null
+++ b/Content.Shared/Charges/Systems/SharedChargesSystem.cs
@@ -0,0 +1,62 @@
+using Content.Shared.Charges.Components;
+using Content.Shared.Examine;
+
+namespace Content.Shared.Charges.Systems;
+
+public abstract class SharedChargesSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnExamine);
+ }
+
+ protected virtual void OnExamine(EntityUid uid, LimitedChargesComponent comp, ExaminedEvent args)
+ {
+ if (!args.IsInDetailsRange)
+ return;
+
+ args.PushMarkup(Loc.GetString("limited-charges-charges-remaining", ("charges", comp.Charges)));
+ if (comp.Charges == comp.MaxCharges)
+ {
+ args.PushMarkup(Loc.GetString("limited-charges-max-charges"));
+ return;
+ }
+ }
+
+ ///
+ /// Tries to add a number of charges. If it over or underflows it will be clamped, wasting the extra charges.
+ ///
+ public void AddCharges(EntityUid uid, int change, LimitedChargesComponent? comp = null)
+ {
+ if (!Resolve(uid, ref comp, false))
+ return;
+
+ var old = comp.Charges;
+ comp.Charges = Math.Clamp(comp.Charges + change, 0, comp.MaxCharges);
+ if (comp.Charges != old)
+ Dirty(comp);
+ }
+
+ ///
+ /// Gets the limited charges component and returns true if there are no charges. Will return false if there is no limited charges component.
+ ///
+ public bool IsEmpty(EntityUid uid, LimitedChargesComponent? comp = null)
+ {
+ // can't be empty if there are no limited charges
+ if (!Resolve(uid, ref comp, false))
+ return false;
+
+ return comp.Charges <= 0;
+ }
+
+ ///
+ /// Uses a single charge. Must check IsEmpty beforehand to prevent using with 0 charge.
+ ///
+ public virtual void UseCharge(EntityUid uid, LimitedChargesComponent? comp = null)
+ {
+ if (Resolve(uid, ref comp, false))
+ AddCharges(uid, -1, comp);
+ }
+}
diff --git a/Content.Shared/Emag/Components/EmagComponent.cs b/Content.Shared/Emag/Components/EmagComponent.cs
index e696671aff..235cf0c744 100644
--- a/Content.Shared/Emag/Components/EmagComponent.cs
+++ b/Content.Shared/Emag/Components/EmagComponent.cs
@@ -1,7 +1,6 @@
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;
@@ -9,62 +8,13 @@ namespace Content.Shared.Emag.Components;
[Access(typeof(EmagSystem))]
[RegisterComponent, NetworkedComponent]
-public sealed class EmagComponent : Component
+[AutoGenerateComponentState]
+public sealed partial 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)]
+ [AutoNetworkedField]
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)
- {
- 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
index dc1225577e..7d30438155 100644
--- a/Content.Shared/Emag/Systems/EmagSystem.cs
+++ b/Content.Shared/Emag/Systems/EmagSystem.cs
@@ -1,175 +1,101 @@
using Content.Shared.Administration.Logs;
+using Content.Shared.Charges.Components;
+using Content.Shared.Charges.Systems;
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
+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
+/// 5. Optionally, set Repeatable on the event to true if you don't want the emagged component to be added
+public sealed class EmagSystem : EntitySystem
{
- /// 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 ISharedAdminLogManager _adminLogger = default!;
+ [Dependency] private readonly SharedChargesSystem _charges = default!;
+ [Dependency] private readonly INetManager _net = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly TagSystem _tag = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+
+ public override void Initialize()
{
- [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!;
+ base.Initialize();
- 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)
- {
- if (_net.IsServer)
- _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)
- {
- // prevent emagging twice
- if (HasComp(target))
- return false;
-
- var emaggedEvent = new GotEmaggedEvent(user);
- RaiseLocalEvent(target, ref emaggedEvent);
-
- if (emaggedEvent.Handled && !emaggedEvent.Repeatable)
- EnsureComp(target);
- return emaggedEvent.Handled;
- }
+ SubscribeLocalEvent(OnAfterInteract);
}
- [ByRefEvent]
- public record struct GotEmaggedEvent(EntityUid UserUid, bool Handled = false, bool Repeatable = false);
+ private void OnAfterInteract(EntityUid uid, EmagComponent comp, AfterInteractEvent args)
+ {
+ if (!args.CanReach || args.Target is not { } target)
+ return;
+
+ args.Handled = TryUseEmag(uid, args.User, target, comp);
+ }
+
+ ///
+ /// Tries to use the emag on a target entity
+ ///
+ public bool TryUseEmag(EntityUid uid, EntityUid user, EntityUid target, EmagComponent? comp = null)
+ {
+ if (!Resolve(uid, ref comp, false))
+ return false;
+
+ if (_tag.HasTag(target, comp.EmagImmuneTag))
+ return false;
+
+ TryComp(uid, out var charges);
+ if (_charges.IsEmpty(uid, charges))
+ {
+ if (_net.IsClient && _timing.IsFirstTimePredicted)
+ _popup.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)
+ {
+ _popup.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}");
+
+ if (charges != null)
+ _charges.UseCharge(uid, charges);
+ return true;
+ }
+
+ ///
+ /// Does the emag effect on a specified entity
+ ///
+ public bool DoEmagEffect(EntityUid user, EntityUid target)
+ {
+ // prevent emagging twice
+ if (HasComp(target))
+ return false;
+
+ var emaggedEvent = new GotEmaggedEvent(user);
+ RaiseLocalEvent(target, ref emaggedEvent);
+
+ if (emaggedEvent.Handled && !emaggedEvent.Repeatable)
+ EnsureComp(target);
+ return emaggedEvent.Handled;
+ }
}
+
+[ByRefEvent]
+public record struct GotEmaggedEvent(EntityUid UserUid, bool Handled = false, bool Repeatable = false);
diff --git a/Resources/Locale/en-US/emag/emag.ftl b/Resources/Locale/en-US/emag/emag.ftl
index 7c9514cc1d..b4679870b5 100644
--- a/Resources/Locale/en-US/emag/emag.ftl
+++ b/Resources/Locale/en-US/emag/emag.ftl
@@ -1,12 +1,2 @@
emag-success = The card zaps something in {THE($target)}.
emag-no-charges = No charges left!
-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 = {$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
diff --git a/Resources/Locale/en-US/limited-charges/limited-charges.ftl b/Resources/Locale/en-US/limited-charges/limited-charges.ftl
new file mode 100644
index 0000000000..d6b28a01ff
--- /dev/null
+++ b/Resources/Locale/en-US/limited-charges/limited-charges.ftl
@@ -0,0 +1,10 @@
+limited-charges-charges-remaining = {$charges ->
+ [one] It has [color=fuchsia]{$charges}[/color] charge remaining.
+ *[other] It has [color=fuchsia]{$charges}[/color] charges remaining.
+}
+
+limited-charges-max-charges = It's at [color=green]maximum[/color] charges.
+limited-charges-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.
+}
diff --git a/Resources/Prototypes/Entities/Objects/Tools/emag.yml b/Resources/Prototypes/Entities/Objects/Tools/emag.yml
index bc120c4d0b..dcc0d0a288 100644
--- a/Resources/Prototypes/Entities/Objects/Tools/emag.yml
+++ b/Resources/Prototypes/Entities/Objects/Tools/emag.yml
@@ -5,6 +5,21 @@
description: The all-in-one hacking solution. The thinking man's lockpick. The iconic EMAG.
components:
- type: Emag
+ - type: LimitedCharges
+ - type: AutoRecharge
+ - type: Sprite
+ netsync: false
+ sprite: Objects/Tools/emag.rsi
+ state: icon
+
+- type: entity
+ parent: BaseItem
+ id: EmagUnlimited
+ suffix: Unlimited
+ name: cryptographic sequencer
+ description: The all-in-one hacking solution. The thinking man's lockpick. The iconic EMAG.
+ components:
+ - type: Emag
- type: Sprite
netsync: false
sprite: Objects/Tools/emag.rsi