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