Action charges refactor (#33993)

* Action charges refactor

- Fixes the slight godmoding of baseactioncomponent.
- Gets back 1ms of server time.

* chorg

* Remove FrameUpdate

* Fixes

* More fixes

* Combine

* Fixes

* Updates

* weh

* Last fixes

* weh

* Fix naughty

* YAML fixes

* This one too

* Merge conflicts

* This thing

* Review

* Fix this as well

* Icon fix

* weh

* Review

* Review

* seamless

* Review
This commit is contained in:
metalgearsloth
2025-04-18 13:45:48 +10:00
committed by GitHub
parent 424f153142
commit 7d2ef2bd47
30 changed files with 366 additions and 399 deletions

View File

@@ -86,24 +86,6 @@ public abstract partial class BaseActionComponent : Component
/// </summary>
[DataField("useDelay")] public TimeSpan? UseDelay;
/// <summary>
/// Convenience tool for actions with limited number of charges. Automatically decremented on use, and the
/// action is disabled when it reaches zero. Does NOT automatically remove the action from the action bar.
/// However, charges will regenerate if <see cref="RenewCharges"/> is enabled and the action will not disable
/// when charges reach zero.
/// </summary>
[DataField("charges")] public int? Charges;
/// <summary>
/// The max charges this action has. If null, this is set automatically from <see cref="Charges"/> on mapinit.
/// </summary>
[DataField] public int? MaxCharges;
/// <summary>
/// If enabled, charges will regenerate after a <see cref="Cooldown"/> is complete
/// </summary>
[DataField("renewCharges")]public bool RenewCharges;
/// <summary>
/// The entity that contains this action. If the action is innate, this may be the user themselves.
/// This should almost always be non-null.
@@ -209,9 +191,6 @@ public abstract class BaseActionComponentState : ComponentState
public bool Toggled;
public (TimeSpan Start, TimeSpan End)? Cooldown;
public TimeSpan? UseDelay;
public int? Charges;
public int? MaxCharges;
public bool RenewCharges;
public NetEntity? Container;
public NetEntity? EntityIcon;
public bool CheckCanInteract;
@@ -243,9 +222,6 @@ public abstract class BaseActionComponentState : ComponentState
Toggled = component.Toggled;
Cooldown = component.Cooldown;
UseDelay = component.UseDelay;
Charges = component.Charges;
MaxCharges = component.MaxCharges;
RenewCharges = component.RenewCharges;
CheckCanInteract = component.CheckCanInteract;
CheckConsciousness = component.CheckConsciousness;
ClientExclusive = component.ClientExclusive;

View File

@@ -21,14 +21,14 @@ namespace Content.Shared.Actions;
public abstract class SharedActionsSystem : EntitySystem
{
[Dependency] protected readonly IGameTiming GameTiming = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
[Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
public override void Initialize()
{
@@ -69,47 +69,9 @@ public abstract class SharedActionsSystem : EntitySystem
SubscribeAllEvent<RequestPerformActionEvent>(OnActionRequest);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var worldActionQuery = EntityQueryEnumerator<WorldTargetActionComponent>();
while (worldActionQuery.MoveNext(out var uid, out var action))
{
if (IsCooldownActive(action) || !ShouldResetCharges(action))
continue;
ResetCharges(uid, dirty: true);
}
var instantActionQuery = EntityQueryEnumerator<InstantActionComponent>();
while (instantActionQuery.MoveNext(out var uid, out var action))
{
if (IsCooldownActive(action) || !ShouldResetCharges(action))
continue;
ResetCharges(uid, dirty: true);
}
var entityActionQuery = EntityQueryEnumerator<EntityTargetActionComponent>();
while (entityActionQuery.MoveNext(out var uid, out var action))
{
if (IsCooldownActive(action) || !ShouldResetCharges(action))
continue;
ResetCharges(uid, dirty: true);
}
}
private void OnActionMapInit(EntityUid uid, BaseActionComponent component, MapInitEvent args)
{
component.OriginalIconColor = component.IconColor;
if (component.Charges == null)
return;
component.MaxCharges ??= component.Charges.Value;
Dirty(uid, component);
}
private void OnActionShutdown(EntityUid uid, BaseActionComponent component, ComponentShutdown args)
@@ -312,68 +274,6 @@ public abstract class SharedActionsSystem : EntitySystem
Dirty(actionId.Value, action);
}
public void SetCharges(EntityUid? actionId, int? charges)
{
if (!TryGetActionData(actionId, out var action) ||
action.Charges == charges)
{
return;
}
action.Charges = charges;
UpdateAction(actionId, action);
Dirty(actionId.Value, action);
}
public int? GetCharges(EntityUid? actionId)
{
if (!TryGetActionData(actionId, out var action))
return null;
return action.Charges;
}
public void AddCharges(EntityUid? actionId, int addCharges)
{
if (!TryGetActionData(actionId, out var action) || action.Charges == null || addCharges < 1)
return;
action.Charges += addCharges;
UpdateAction(actionId, action);
Dirty(actionId.Value, action);
}
public void RemoveCharges(EntityUid? actionId, int? removeCharges)
{
if (!TryGetActionData(actionId, out var action) || action.Charges == null)
return;
if (removeCharges == null)
action.Charges = removeCharges;
else
action.Charges -= removeCharges;
if (action.Charges is < 0)
action.Charges = null;
UpdateAction(actionId, action);
Dirty(actionId.Value, action);
}
public void ResetCharges(EntityUid? actionId, bool update = false, bool dirty = false)
{
if (!TryGetActionData(actionId, out var action))
return;
action.Charges = action.MaxCharges;
if (update)
UpdateAction(actionId, action);
if (dirty)
Dirty(actionId.Value, action);
}
private void OnActionsGetState(EntityUid uid, ActionsComponent component, ref ComponentGetState args)
{
args.State = new ActionsComponentState(GetNetEntitySet(component.Actions));
@@ -416,6 +316,10 @@ public abstract class SharedActionsSystem : EntitySystem
if (!action.Enabled)
return;
var curTime = GameTiming.CurTime;
if (IsCooldownActive(action, curTime))
return;
// check for action use prevention
// TODO: make code below use this event with a dedicated component
var attemptEv = new ActionAttemptEvent(user);
@@ -423,14 +327,6 @@ public abstract class SharedActionsSystem : EntitySystem
if (attemptEv.Cancelled)
return;
var curTime = GameTiming.CurTime;
if (IsCooldownActive(action, curTime))
return;
// TODO: Replace with individual charge recovery when we have the visuals to aid it
if (action is { Charges: < 1, RenewCharges: true })
ResetCharges(actionEnt, true, true);
BaseActionEvent? performEvent = null;
if (action.CheckConsciousness && !_actionBlockerSystem.CanConsciouslyPerformAction(user))
@@ -705,16 +601,8 @@ public abstract class SharedActionsSystem : EntitySystem
var dirty = toggledBefore != action.Toggled;
if (action.Charges != null)
{
dirty = true;
action.Charges--;
if (action is { Charges: 0, RenewCharges: false })
action.Enabled = false;
}
action.Cooldown = null;
if (action is { UseDelay: not null, Charges: null or < 1 })
if (action is { UseDelay: not null})
{
dirty = true;
action.Cooldown = (curTime, curTime + action.UseDelay.Value);
@@ -1014,8 +902,6 @@ public abstract class SharedActionsSystem : EntitySystem
if (!action.Enabled)
return false;
if (action.Charges.HasValue && action.Charges <= 0)
return false;
var curTime = GameTiming.CurTime;
if (action.Cooldown.HasValue && action.Cooldown.Value.End > curTime)
@@ -1125,15 +1011,9 @@ public abstract class SharedActionsSystem : EntitySystem
/// <summary>
/// Checks if the action has a cooldown and if it's still active
/// </summary>
protected bool IsCooldownActive(BaseActionComponent action, TimeSpan? curTime = null)
public bool IsCooldownActive(BaseActionComponent action, TimeSpan? curTime = null)
{
curTime ??= GameTiming.CurTime;
// TODO: Check for charge recovery timer
return action.Cooldown.HasValue && action.Cooldown.Value.End > curTime;
}
protected bool ShouldResetCharges(BaseActionComponent action)
{
return action is { Charges: < 1, RenewCharges: true };
}
}

View File

@@ -0,0 +1,19 @@
using Content.Shared.Charges.Systems;
using Robust.Shared.GameStates;
namespace Content.Shared.Charges.Components;
/// <summary>
/// Something with limited charges that can be recharged automatically.
/// Requires LimitedChargesComponent to function.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedChargesSystem))]
public sealed partial class AutoRechargeComponent : Component
{
/// <summary>
/// The time it takes to regain a single charge
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan RechargeDuration = TimeSpan.FromSeconds(90);
}

View File

@@ -1,24 +1,27 @@
using Content.Shared.Charges.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Charges.Components;
[RegisterComponent, NetworkedComponent]
[Access(typeof(SharedChargesSystem))]
[AutoGenerateComponentState]
/// <summary>
/// Specifies the attached action has discrete charges, separate to a cooldown.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedChargesSystem))]
public sealed partial class LimitedChargesComponent : Component
{
/// <summary>
/// The maximum number of charges
/// </summary>
[DataField("maxCharges"), ViewVariables(VVAccess.ReadWrite)]
[AutoNetworkedField]
public int MaxCharges = 3;
[DataField, AutoNetworkedField]
public int LastCharges;
/// <summary>
/// The current number of charges
/// The max charges this action has.
/// </summary>
[DataField("charges"), ViewVariables(VVAccess.ReadWrite)]
[AutoNetworkedField]
public int Charges = 3;
[DataField, AutoNetworkedField, Access(Other = AccessPermissions.Read)]
public int MaxCharges = 1;
/// <summary>
/// Last time charges was changed. Used to derive current charges.
/// </summary>
[DataField(customTypeSerializer:typeof(TimeOffsetSerializer)), AutoNetworkedField]
public TimeSpan LastUpdate;
}

View File

@@ -1,103 +1,232 @@
using Content.Shared.Actions.Events;
using Content.Shared.Charges.Components;
using Content.Shared.Examine;
using JetBrains.Annotations;
using Robust.Shared.Timing;
namespace Content.Shared.Charges.Systems;
public abstract class SharedChargesSystem : EntitySystem
{
protected EntityQuery<LimitedChargesComponent> Query;
[Dependency] protected readonly IGameTiming _timing = default!;
/*
* Despite what a bunch of systems do you don't need to continuously tick linear number updates and can just derive it easily.
*/
public override void Initialize()
{
base.Initialize();
Query = GetEntityQuery<LimitedChargesComponent>();
SubscribeLocalEvent<LimitedChargesComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<LimitedChargesComponent, ActionAttemptEvent>(OnChargesAttempt);
SubscribeLocalEvent<LimitedChargesComponent, MapInitEvent>(OnChargesMapInit);
SubscribeLocalEvent<LimitedChargesComponent, ActionPerformedEvent>(OnChargesPerformed);
}
protected virtual void OnExamine(EntityUid uid, LimitedChargesComponent comp, ExaminedEvent args)
private void OnExamine(EntityUid uid, LimitedChargesComponent comp, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
using (args.PushGroup(nameof(LimitedChargesComponent)))
var rechargeEnt = new Entity<LimitedChargesComponent?, AutoRechargeComponent?>(uid, comp, null);
var charges = GetCurrentCharges(rechargeEnt);
using var _ = args.PushGroup(nameof(LimitedChargesComponent));
args.PushMarkup(Loc.GetString("limited-charges-charges-remaining", ("charges", charges)));
if (charges == comp.MaxCharges)
{
args.PushMarkup(Loc.GetString("limited-charges-charges-remaining", ("charges", comp.Charges)));
if (comp.Charges == comp.MaxCharges)
{
args.PushMarkup(Loc.GetString("limited-charges-max-charges"));
}
args.PushMarkup(Loc.GetString("limited-charges-max-charges"));
}
// only show the recharging info if it's not full
if (charges == comp.MaxCharges || !TryComp<AutoRechargeComponent>(uid, out var recharge))
return;
rechargeEnt.Comp2 = recharge;
var timeRemaining = GetNextRechargeTime(rechargeEnt);
args.PushMarkup(Loc.GetString("limited-charges-recharging", ("seconds", timeRemaining.TotalSeconds.ToString("F1"))));
}
private void OnChargesAttempt(Entity<LimitedChargesComponent> ent, ref ActionAttemptEvent args)
{
if (args.Cancelled)
return;
var charges = GetCurrentCharges((ent.Owner, ent.Comp, null));
if (charges <= 0)
{
args.Cancelled = true;
}
}
/// <summary>
/// Tries to add a number of charges. If it over or underflows it will be clamped, wasting the extra charges.
/// </summary>
public virtual void AddCharges(EntityUid uid, int change, LimitedChargesComponent? comp = null)
private void OnChargesPerformed(Entity<LimitedChargesComponent> ent, ref ActionPerformedEvent args)
{
if (!Query.Resolve(uid, ref comp, false))
AddCharges((ent.Owner, ent.Comp), -1);
}
private void OnChargesMapInit(Entity<LimitedChargesComponent> ent, ref MapInitEvent args)
{
// If nothing specified use max.
if (ent.Comp.LastCharges == 0)
{
ent.Comp.LastCharges = ent.Comp.MaxCharges;
}
// If -1 used then we don't want any.
else if (ent.Comp.LastCharges < 0)
{
ent.Comp.LastCharges = 0;
}
ent.Comp.LastUpdate = _timing.CurTime;
Dirty(ent);
}
[Pure]
public bool HasCharges(Entity<LimitedChargesComponent?> action, int charges)
{
var current = GetCurrentCharges(action);
return current >= charges;
}
/// <summary>
/// Adds the specified charges. Does not reset the accumulator.
/// </summary>
public void AddCharges(Entity<LimitedChargesComponent?> action, int addCharges)
{
if (addCharges == 0)
return;
var old = comp.Charges;
comp.Charges = Math.Clamp(comp.Charges + change, 0, comp.MaxCharges);
if (comp.Charges != old)
Dirty(uid, comp);
action.Comp ??= EnsureComp<LimitedChargesComponent>(action.Owner);
// 1. If we're going FROM max then set lastupdate to now (so it doesn't instantly recharge).
// 2. If we're going TO max then also set lastupdate to now.
// 3. Otherwise don't modify it.
// No idea if we go to 0 but future problem.
var lastCharges = GetCurrentCharges(action);
var charges = lastCharges + addCharges;
if (lastCharges == charges)
return;
if (charges == action.Comp.MaxCharges || lastCharges == action.Comp.MaxCharges)
{
action.Comp.LastUpdate = _timing.CurTime;
}
action.Comp.LastCharges = Math.Clamp(action.Comp.LastCharges + addCharges, 0, action.Comp.MaxCharges);
Dirty(action);
}
/// <summary>
/// Gets the limited charges component and returns true if there are no charges. Will return false if there is no limited charges component.
/// </summary>
public bool IsEmpty(EntityUid uid, LimitedChargesComponent? comp = null)
public bool TryUseCharge(Entity<LimitedChargesComponent?> entity)
{
// can't be empty if there are no limited charges
if (!Query.Resolve(uid, ref comp, false))
return TryUseCharges(entity, 1);
}
public bool TryUseCharges(Entity<LimitedChargesComponent?> entity, int amount)
{
var current = GetCurrentCharges(entity);
if (current < amount)
{
return false;
}
return comp.Charges <= 0;
}
/// <summary>
/// Uses a single charge. Must check IsEmpty beforehand to prevent using with 0 charge.
/// </summary>
public void UseCharge(EntityUid uid, LimitedChargesComponent? comp = null)
{
AddCharges(uid, -1, comp);
}
/// <summary>
/// Checks IsEmpty and uses a charge if it isn't empty.
/// </summary>
public bool TryUseCharge(Entity<LimitedChargesComponent?> ent)
{
if (!Query.Resolve(ent, ref ent.Comp, false))
return true;
if (IsEmpty(ent, ent.Comp))
return false;
UseCharge(ent, ent.Comp);
AddCharges(entity, -amount);
return true;
}
/// <summary>
/// Gets the limited charges component and returns true if the number of charges remaining is less than the specified value.
/// Will return false if there is no limited charges component.
/// </summary>
public bool HasInsufficientCharges(EntityUid uid, int requiredCharges, LimitedChargesComponent? comp = null)
[Pure]
public bool IsEmpty(Entity<LimitedChargesComponent?> entity)
{
// can't be empty if there are no limited charges
if (!Resolve(uid, ref comp, false))
return false;
return comp.Charges < requiredCharges;
return GetCurrentCharges(entity) == 0;
}
/// <summary>
/// Uses up a specified number of charges. Must check HasInsufficentCharges beforehand to prevent using with insufficient remaining charges.
/// Resets action charges to MaxCharges.
/// </summary>
public virtual void UseCharges(EntityUid uid, int chargesUsed, LimitedChargesComponent? comp = null)
public void ResetCharges(Entity<LimitedChargesComponent?> action)
{
AddCharges(uid, -chargesUsed, comp);
if (!Resolve(action.Owner, ref action.Comp, false))
return;
var charges = GetCurrentCharges((action.Owner, action.Comp, null));
if (charges == action.Comp.MaxCharges)
return;
action.Comp.LastCharges = action.Comp.MaxCharges;
action.Comp.LastUpdate = _timing.CurTime;
Dirty(action);
}
public void SetCharges(Entity<LimitedChargesComponent?> action, int value)
{
action.Comp ??= EnsureComp<LimitedChargesComponent>(action.Owner);
var adjusted = Math.Clamp(value, 0, action.Comp.MaxCharges);
if (action.Comp.LastCharges == adjusted)
{
return;
}
action.Comp.LastCharges = adjusted;
action.Comp.LastUpdate = _timing.CurTime;
Dirty(action);
}
/// <summary>
/// The next time a charge will be considered to be filled.
/// </summary>
/// <returns>0 timespan if invalid or no charges to generate.</returns>
[Pure]
public TimeSpan GetNextRechargeTime(Entity<LimitedChargesComponent?, AutoRechargeComponent?> entity)
{
if (!Resolve(entity.Owner, ref entity.Comp1, ref entity.Comp2, false))
{
return TimeSpan.Zero;
}
// Okay so essentially we need to get recharge time to full, then modulus that by the recharge timer which should be the next tick.
var fullTime = ((entity.Comp1.MaxCharges - entity.Comp1.LastCharges) * entity.Comp2.RechargeDuration) + entity.Comp1.LastUpdate;
var timeRemaining = fullTime - _timing.CurTime;
if (timeRemaining < TimeSpan.Zero)
{
return TimeSpan.Zero;
}
var nextChargeTime = timeRemaining.TotalSeconds % entity.Comp2.RechargeDuration.TotalSeconds;
return TimeSpan.FromSeconds(nextChargeTime);
}
/// <summary>
/// Derives the current charges of an entity.
/// </summary>
[Pure]
public int GetCurrentCharges(Entity<LimitedChargesComponent?, AutoRechargeComponent?> entity)
{
if (!Resolve(entity.Owner, ref entity.Comp1, false))
{
// I'm all in favor of nullable ints however null-checking return args against comp nullability is dodgy
// so we get this.
return -1;
}
var calculated = 0;
if (Resolve(entity.Owner, ref entity.Comp2, false) && entity.Comp2.RechargeDuration.TotalSeconds != 0.0)
{
calculated = (int)((_timing.CurTime - entity.Comp1.LastUpdate).TotalSeconds / entity.Comp2.RechargeDuration.TotalSeconds);
}
return Math.Clamp(entity.Comp1.LastCharges + calculated,
0,
entity.Comp1.MaxCharges);
}
}

View File

@@ -21,7 +21,7 @@ namespace Content.Shared.Emag.Systems;
public sealed class EmagSystem : EntitySystem
{
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedChargesSystem _charges = default!;
[Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
@@ -61,8 +61,8 @@ public sealed class EmagSystem : EntitySystem
if (_tag.HasTag(target, ent.Comp.EmagImmuneTag))
return false;
TryComp<LimitedChargesComponent>(ent, out var charges);
if (_charges.IsEmpty(ent, charges))
Entity<LimitedChargesComponent?> chargesEnt = ent.Owner;
if (_sharedCharges.IsEmpty(chargesEnt))
{
_popup.PopupClient(Loc.GetString("emag-no-charges"), user, user);
return false;
@@ -80,8 +80,8 @@ public sealed class EmagSystem : EntitySystem
_adminLogger.Add(LogType.Emag, LogImpact.High, $"{ToPrettyString(user):player} emagged {ToPrettyString(target):target} with flag(s): {ent.Comp.EmagType}");
if (charges != null && emaggedEvent.Handled)
_charges.UseCharge(ent, charges);
if (emaggedEvent.Handled)
_sharedCharges.TryUseCharge(chargesEnt);
if (!emaggedEvent.Repeatable)
{

View File

@@ -1,4 +1,5 @@
using Content.Shared.Actions;
using Content.Shared.Charges.Systems;
using Content.Shared.DoAfter;
using Content.Shared.Interaction.Events;
using Content.Shared.Magic.Components;
@@ -9,6 +10,7 @@ namespace Content.Shared.Magic;
public sealed class SpellbookSystem : EntitySystem
{
[Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
@@ -30,11 +32,7 @@ public sealed class SpellbookSystem : EntitySystem
if (spell == null)
continue;
int? charge = charges;
if (_actions.GetCharges(spell) != null)
charge = _actions.GetCharges(spell);
_actions.SetCharges(spell, charge < 0 ? null : charge);
_sharedCharges.SetCharges(spell.Value, charges);
ent.Comp.Spells.Add(spell.Value);
}
}
@@ -75,7 +73,7 @@ public sealed class SpellbookSystem : EntitySystem
{
EntityUid? actionId = null;
if (_actions.AddAction(args.Args.User, ref actionId, id))
_actions.SetCharges(actionId, charges < 0 ? null : charges);
_sharedCharges.SetCharges(actionId.Value, charges);
}
}

View File

@@ -22,7 +22,7 @@ public sealed class DashAbilitySystem : EntitySystem
{
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedChargesSystem _charges = default!;
[Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly ExamineSystemShared _examine = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
@@ -79,7 +79,7 @@ public sealed class DashAbilitySystem : EntitySystem
return;
}
if (!_charges.TryUseCharge(uid))
if (!_sharedCharges.TryUseCharge(uid))
{
_popup.PopupClient(Loc.GetString("dash-ability-no-charges", ("item", uid)), user, user);
return;

View File

@@ -28,6 +28,7 @@ public sealed class EnergyKatanaSystem : EntitySystem
private void OnCheckDash(Entity<EnergyKatanaComponent> ent, ref CheckDashEvent args)
{
// Just use a whitelist fam
if (!_ninja.IsNinja(args.User))
args.Cancelled = true;
}

View File

@@ -11,6 +11,6 @@ public sealed partial class RCDAmmoComponent : Component
/// How many charges are contained in this ammo cartridge.
/// Can be partially transferred into an RCD, until it is empty then it gets deleted.
/// </summary>
[DataField("charges"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
[DataField, AutoNetworkedField]
public int Charges = 30;
}

View File

@@ -10,7 +10,7 @@ namespace Content.Shared.RCD.Systems;
public sealed class RCDAmmoSystem : EntitySystem
{
[Dependency] private readonly SharedChargesSystem _charges = default!;
[Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly IGameTiming _timing = default!;
@@ -41,9 +41,10 @@ public sealed class RCDAmmoSystem : EntitySystem
!TryComp<LimitedChargesComponent>(target, out var charges))
return;
var current = _sharedCharges.GetCurrentCharges((target, charges));
var user = args.User;
args.Handled = true;
var count = Math.Min(charges.MaxCharges - charges.Charges, comp.Charges);
var count = Math.Min(charges.MaxCharges - current, comp.Charges);
if (count <= 0)
{
_popup.PopupClient(Loc.GetString("rcd-ammo-component-after-interact-full"), target, user);
@@ -51,7 +52,7 @@ public sealed class RCDAmmoSystem : EntitySystem
}
_popup.PopupClient(Loc.GetString("rcd-ammo-component-after-interact-refilled"), target, user);
_charges.AddCharges(target, count, charges);
_sharedCharges.AddCharges(target, count);
comp.Charges -= count;
Dirty(uid, comp);

View File

@@ -33,7 +33,7 @@ public sealed class RCDSystem : EntitySystem
[Dependency] private readonly ITileDefinitionManager _tileDefMan = default!;
[Dependency] private readonly FloorTileSystem _floors = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedChargesSystem _charges = default!;
[Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
@@ -43,7 +43,6 @@ public sealed class RCDSystem : EntitySystem
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly TagSystem _tags = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
private readonly int _instantConstructionDelay = 0;
private readonly EntProtoId _instantConstructionFx = "EffectRCDConstruct0";
@@ -133,7 +132,7 @@ public sealed class RCDSystem : EntitySystem
if (!location.IsValid(EntityManager))
return;
var gridUid = _transformSystem.GetGrid(location);
var gridUid = _transform.GetGrid(location);
if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
{
@@ -239,7 +238,7 @@ public sealed class RCDSystem : EntitySystem
// Ensure the RCD operation is still valid
var location = GetCoordinates(args.Event.Location);
var gridUid = _transformSystem.GetGrid(location);
var gridUid = _transform.GetGrid(location);
if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
{
@@ -272,7 +271,7 @@ public sealed class RCDSystem : EntitySystem
var location = GetCoordinates(args.Location);
var gridUid = _transformSystem.GetGrid(location);
var gridUid = _transform.GetGrid(location);
if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
return;
@@ -289,7 +288,7 @@ public sealed class RCDSystem : EntitySystem
// Play audio and consume charges
_audio.PlayPredicted(component.SuccessSound, uid, args.User);
_charges.UseCharges(uid, args.Cost);
_sharedCharges.AddCharges(uid, -args.Cost);
}
private void OnRCDconstructionGhostRotationEvent(RCDConstructionGhostRotationEvent ev, EntitySessionEventArgs session)
@@ -321,10 +320,10 @@ public sealed class RCDSystem : EntitySystem
var prototype = _protoManager.Index(component.ProtoId);
// Check that the RCD has enough ammo to get the job done
TryComp<LimitedChargesComponent>(uid, out var charges);
var charges = _sharedCharges.GetCurrentCharges(uid);
// Both of these were messages were suppose to be predicted, but HasInsufficientCharges wasn't being checked on the client for some reason?
if (_charges.IsEmpty(uid, charges))
if (charges == 0)
{
if (popMsgs)
_popup.PopupClient(Loc.GetString("rcd-component-no-ammo-message"), uid, user);
@@ -332,7 +331,7 @@ public sealed class RCDSystem : EntitySystem
return false;
}
if (_charges.HasInsufficientCharges(uid, prototype.Cost, charges))
if (prototype.Cost > charges)
{
if (popMsgs)
_popup.PopupClient(Loc.GetString("rcd-component-insufficient-ammo-message"), uid, user);