Predict powercells, chargers and PowerCellDraw (#41379)

* cleanup

* fix fixtures

* prediction

* fix test

* review

* fix svalinn visuals

* fix chargers

* fix portable recharger and its unlit visuals

* fix borgs

* oomba review

* fix examination prediction
This commit is contained in:
slarticodefast 2025-11-24 17:52:11 +01:00 committed by BarryNorfolk
parent 92de25f0f3
commit 50b2986ef7
116 changed files with 2368 additions and 1638 deletions

View File

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

View File

@ -1,67 +0,0 @@
using Content.Shared.PowerCell;
using Content.Shared.PowerCell.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
namespace Content.Client.PowerCell;
[UsedImplicitly]
public sealed class PowerCellSystem : SharedPowerCellSystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PowerCellVisualsComponent, AppearanceChangeEvent>(OnPowerCellVisualsChange);
}
/// <inheritdoc/>
public override bool HasActivatableCharge(EntityUid uid, PowerCellDrawComponent? battery = null, PowerCellSlotComponent? cell = null,
EntityUid? user = null)
{
if (!Resolve(uid, ref battery, ref cell, false))
return true;
return battery.CanUse;
}
/// <inheritdoc/>
public override bool HasDrawCharge(
EntityUid uid,
PowerCellDrawComponent? battery = null,
PowerCellSlotComponent? cell = null,
EntityUid? user = null)
{
if (!Resolve(uid, ref battery, ref cell, false))
return true;
return battery.CanDraw;
}
private void OnPowerCellVisualsChange(EntityUid uid, PowerCellVisualsComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
if (!_sprite.LayerExists((uid, args.Sprite), PowerCellVisualLayers.Unshaded))
return;
// If no appearance data is set, rely on whatever existing sprite state is set being correct.
if (!_appearance.TryGetData<byte>(uid, PowerCellVisuals.ChargeLevel, out var level, args.Component))
return;
var positiveCharge = level > 0;
_sprite.LayerSetVisible((uid, args.Sprite), PowerCellVisualLayers.Unshaded, positiveCharge);
if (positiveCharge)
_sprite.LayerSetRsiState((uid, args.Sprite), PowerCellVisualLayers.Unshaded, $"o{level}");
}
private enum PowerCellVisualLayers : byte
{
Base,
Unshaded,
}
}

View File

@ -0,0 +1,11 @@
namespace Content.Client.PowerCell;
/// <summary>
/// Sprite layers for power cells.
/// For use with the generic visualizer.
/// </summary>
public enum PowerCellVisualLayers : byte
{
Base,
Unshaded,
}

View File

@ -1,4 +0,0 @@
namespace Content.Client.PowerCell;
[RegisterComponent]
public sealed partial class PowerCellVisualsComponent : Component {}

View File

@ -1,4 +1,4 @@
using Content.Shared.Power; using Content.Shared.Power.Components;
namespace Content.Client.PowerCell; namespace Content.Client.PowerCell;
@ -9,15 +9,13 @@ public sealed partial class PowerChargerVisualsComponent : Component
/// <summary> /// <summary>
/// The base sprite state used if the power cell charger does not contain a power cell. /// The base sprite state used if the power cell charger does not contain a power cell.
/// </summary> /// </summary>
[DataField("emptyState")] [DataField]
[ViewVariables(VVAccess.ReadWrite)]
public string EmptyState = "empty"; public string EmptyState = "empty";
/// <summary> /// <summary>
/// The base sprite state used if the power cell charger contains a power cell. /// The base sprite state used if the power cell charger contains a power cell.
/// </summary> /// </summary>
[DataField("occupiedState")] [DataField]
[ViewVariables(VVAccess.ReadWrite)]
public string OccupiedState = "full"; public string OccupiedState = "full";
/// <summary> /// <summary>
@ -27,8 +25,7 @@ public sealed partial class PowerChargerVisualsComponent : Component
/// <see cref="CellChargerStatus.Charging"/> Maps to the state used when the charger is charging a power cell. /// <see cref="CellChargerStatus.Charging"/> Maps to the state used when the charger is charging a power cell.
/// <see cref="CellChargerStatus.Charged"/> Maps to the state used when the charger contains a fully charged power cell. /// <see cref="CellChargerStatus.Charged"/> Maps to the state used when the charger contains a fully charged power cell.
/// </summary> /// </summary>
[DataField("lightStates")] [DataField]
[ViewVariables(VVAccess.ReadWrite)]
public Dictionary<CellChargerStatus, string> LightStates = new() public Dictionary<CellChargerStatus, string> LightStates = new()
{ {
[CellChargerStatus.Off] = "light-off", [CellChargerStatus.Off] = "light-off",

View File

@ -1,4 +1,4 @@
using Content.Shared.Power; using Content.Shared.Power.Components;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
namespace Content.Client.PowerCell; namespace Content.Client.PowerCell;

View File

@ -7,23 +7,20 @@ public sealed partial class GunSystem
protected override void InitializeBattery() protected override void InitializeBattery()
{ {
base.InitializeBattery(); base.InitializeBattery();
// Hitscan
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
// Projectile SubscribeLocalEvent<BatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl); SubscribeLocalEvent<BatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
} }
private void OnAmmoCountUpdate(EntityUid uid, BatteryAmmoProviderComponent component, UpdateAmmoCounterEvent args) private void OnAmmoCountUpdate(Entity<BatteryAmmoProviderComponent> ent, ref UpdateAmmoCounterEvent args)
{ {
if (args.Control is not BoxesStatusControl boxes) return; if (args.Control is not BoxesStatusControl boxes)
return;
boxes.Update(component.Shots, component.Capacity); boxes.Update(ent.Comp.Shots, ent.Comp.Capacity);
} }
private void OnControl(EntityUid uid, BatteryAmmoProviderComponent component, AmmoCounterControlEvent args) private void OnControl(Entity<BatteryAmmoProviderComponent> ent, ref AmmoCounterControlEvent args)
{ {
args.Control = new BoxesStatusControl(); args.Control = new BoxesStatusControl();
} }

View File

@ -80,7 +80,6 @@ public sealed partial class GunSystem : SharedGunSystem
base.Initialize(); base.Initialize();
UpdatesOutsidePrediction = true; UpdatesOutsidePrediction = true;
SubscribeLocalEvent<AmmoCounterComponent, ItemStatusCollectMessage>(OnAmmoCounterCollect); SubscribeLocalEvent<AmmoCounterComponent, ItemStatusCollectMessage>(OnAmmoCounterCollect);
SubscribeLocalEvent<AmmoCounterComponent, UpdateClientAmmoEvent>(OnUpdateClientAmmo);
SubscribeAllEvent<MuzzleFlashEvent>(OnMuzzleFlash); SubscribeAllEvent<MuzzleFlashEvent>(OnMuzzleFlash);
// Plays animated effects on the client. // Plays animated effects on the client.
@ -90,10 +89,6 @@ public sealed partial class GunSystem : SharedGunSystem
InitializeSpentAmmo(); InitializeSpentAmmo();
} }
private void OnUpdateClientAmmo(EntityUid uid, AmmoCounterComponent ammoComp, ref UpdateClientAmmoEvent args)
{
UpdateAmmoCount(uid, ammoComp);
}
private void OnMuzzleFlash(MuzzleFlashEvent args) private void OnMuzzleFlash(MuzzleFlashEvent args)
{ {
@ -158,6 +153,8 @@ public sealed partial class GunSystem : SharedGunSystem
public override void Update(float frameTime) public override void Update(float frameTime)
{ {
base.Update(frameTime);
if (!Timing.IsFirstTimePredicted) if (!Timing.IsFirstTimePredicted)
return; return;

View File

@ -8,6 +8,7 @@ using Content.Shared.Access.Components;
using Content.Shared.Access.Systems; using Content.Shared.Access.Systems;
using Content.Shared.Chat; using Content.Shared.Chat;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Kitchen;
using Content.Shared.Popups; using Content.Shared.Popups;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;

View File

@ -28,6 +28,7 @@ using Content.Shared.Inventory;
using Content.Shared.Item; // Imp using Content.Shared.Item; // Imp
using Content.Shared.PDA; using Content.Shared.PDA;
using Content.Shared.Power.Components; using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Stacks; using Content.Shared.Stacks;
using Content.Shared.Station.Components; using Content.Shared.Station.Components;
using Content.Shared.Verbs; using Content.Shared.Verbs;
@ -55,6 +56,7 @@ public sealed partial class AdminVerbSystem
[Dependency] private readonly StationJobsSystem _stationJobsSystem = default!; [Dependency] private readonly StationJobsSystem _stationJobsSystem = default!;
[Dependency] private readonly JointSystem _jointSystem = default!; [Dependency] private readonly JointSystem _jointSystem = default!;
[Dependency] private readonly BatterySystem _batterySystem = default!; [Dependency] private readonly BatterySystem _batterySystem = default!;
[Dependency] private readonly PredictedBatterySystem _predictedBatterySystem = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!; [Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly GunSystem _gun = default!; [Dependency] private readonly GunSystem _gun = default!;
[Dependency] private readonly RevenantAnimatedSystem _revenantAnimate = default!; // Imp [Dependency] private readonly RevenantAnimatedSystem _revenantAnimate = default!; // Imp
@ -164,6 +166,57 @@ public sealed partial class AdminVerbSystem
args.Verbs.Add(makeVulnerable); args.Verbs.Add(makeVulnerable);
} }
if (TryComp<PredictedBatteryComponent>(args.Target, out var pBattery))
{
Verb refillBattery = new()
{
Text = Loc.GetString("admin-verbs-refill-battery"),
Category = VerbCategory.Tricks,
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/fill_battery.png")),
Act = () =>
{
_predictedBatterySystem.SetCharge((args.Target, pBattery), pBattery.MaxCharge);
},
Impact = LogImpact.Medium,
Message = Loc.GetString("admin-trick-refill-battery-description"),
Priority = (int)TricksVerbPriorities.RefillBattery,
};
args.Verbs.Add(refillBattery);
Verb drainBattery = new()
{
Text = Loc.GetString("admin-verbs-drain-battery"),
Category = VerbCategory.Tricks,
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/drain_battery.png")),
Act = () =>
{
_predictedBatterySystem.SetCharge((args.Target, pBattery), 0);
},
Impact = LogImpact.Medium,
Priority = (int)TricksVerbPriorities.DrainBattery,
};
args.Verbs.Add(drainBattery);
Verb infiniteBattery = new()
{
Text = Loc.GetString("admin-verbs-infinite-battery"),
Category = VerbCategory.Tricks,
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/infinite_battery.png")),
Act = () =>
{
var recharger = EnsureComp<PredictedBatterySelfRechargerComponent>(args.Target);
recharger.AutoRechargeRate = pBattery.MaxCharge; // Instant refill.
recharger.AutoRechargePauseTime = TimeSpan.Zero; // No delay.
Dirty(args.Target, recharger);
_predictedBatterySystem.RefreshChargeRate((args.Target, pBattery));
},
Impact = LogImpact.Medium,
Message = Loc.GetString("admin-trick-infinite-battery-object-description"),
Priority = (int)TricksVerbPriorities.InfiniteBattery,
};
args.Verbs.Add(infiniteBattery);
}
if (TryComp<BatteryComponent>(args.Target, out var battery)) if (TryComp<BatteryComponent>(args.Target, out var battery))
{ {
Verb refillBattery = new() Verb refillBattery = new()

View File

@ -48,7 +48,7 @@ public sealed partial class BuildMech : IGraphAction
var cell = container.ContainedEntities[0]; var cell = container.ContainedEntities[0];
if (!entityManager.TryGetComponent<BatteryComponent>(cell, out var batteryComponent)) if (!entityManager.TryGetComponent<PredictedBatteryComponent>(cell, out var batteryComponent))
{ {
Logger.Warning($"Mech construct entity {uid} had an invalid entity in container \"{Container}\"! Aborting build mech action."); Logger.Warning($"Mech construct entity {uid} had an invalid entity in container \"{Container}\"! Aborting build mech action.");
return; return;

View File

@ -1,8 +1,7 @@
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.Coordinates.Helpers; using Content.Shared.Coordinates.Helpers;
using Content.Server.PowerCell; using Content.Shared.PowerCell;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Power.Components;
using Content.Shared.Storage; using Content.Shared.Storage;
namespace Content.Server.Holosign; namespace Content.Server.Holosign;
@ -23,9 +22,8 @@ public sealed class HolosignSystem : EntitySystem
{ {
// TODO: This should probably be using an itemstatus // TODO: This should probably be using an itemstatus
// TODO: I'm too lazy to do this rn but it's literally copy-paste from emag. // TODO: I'm too lazy to do this rn but it's literally copy-paste from emag.
_powerCell.TryGetBatteryFromSlot(uid, out var battery); var charges = _powerCell.GetRemainingUses(uid, component.ChargeUse);
var charges = UsesRemaining(component, battery); var maxCharges = _powerCell.GetMaxUses(uid, component.ChargeUse);
var maxCharges = MaxUses(component, battery);
using (args.PushGroup(nameof(HolosignProjectorComponent))) using (args.PushGroup(nameof(HolosignProjectorComponent)))
{ {
@ -52,25 +50,10 @@ public sealed class HolosignSystem : EntitySystem
// overlapping of the same holo on one tile remains allowed to allow holofan refreshes // overlapping of the same holo on one tile remains allowed to allow holofan refreshes
var holoUid = Spawn(component.SignProto, args.ClickLocation.SnapToGrid(EntityManager)); var holoUid = Spawn(component.SignProto, args.ClickLocation.SnapToGrid(EntityManager));
var xform = Transform(holoUid); var xform = Transform(holoUid);
// TODO: Just make the prototype anchored
if (!xform.Anchored) if (!xform.Anchored)
_transform.AnchorEntity(holoUid, xform); // anchor to prevent any tempering with (don't know what could even interact with it) _transform.AnchorEntity(holoUid, xform); // anchor to prevent any tempering with (don't know what could even interact with it)
args.Handled = true; args.Handled = true;
} }
private int UsesRemaining(HolosignProjectorComponent component, BatteryComponent? battery = null)
{
if (battery == null ||
component.ChargeUse == 0f) return 0;
return (int) (battery.CurrentCharge / component.ChargeUse);
}
private int MaxUses(HolosignProjectorComponent component, BatteryComponent? battery = null)
{
if (battery == null ||
component.ChargeUse == 0f) return 0;
return (int) (battery.MaxCharge / component.ChargeUse);
}
} }

View File

@ -1,12 +1,12 @@
using Content.Server.Actions; using Content.Server.Actions;
using Content.Server.Popups; using Content.Server.Popups;
using Content.Server.Power.EntitySystems;
using Content.Server.PowerCell;
using Content.Shared.Actions; using Content.Shared.Actions;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Light; using Content.Shared.Light;
using Content.Shared.Light.Components; using Content.Shared.Light.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.PowerCell;
using Content.Shared.Rounding; using Content.Shared.Rounding;
using Content.Shared.Toggleable; using Content.Shared.Toggleable;
using JetBrains.Annotations; using JetBrains.Annotations;
@ -25,7 +25,7 @@ namespace Content.Server.Light.EntitySystems
[Dependency] private readonly ActionContainerSystem _actionContainer = default!; [Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!; [Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly BatterySystem _battery = default!; [Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPointLightSystem _lights = default!; [Dependency] private readonly SharedPointLightSystem _lights = default!;
@ -108,13 +108,15 @@ namespace Content.Server.Light.EntitySystems
// Curently every single flashlight has the same number of levels for status and that's all it uses the charge for // Curently every single flashlight has the same number of levels for status and that's all it uses the charge for
// Thus we'll just check if the level changes. // Thus we'll just check if the level changes.
if (!_powerCell.TryGetBatteryFromSlot(ent, out var battery)) if (!_powerCell.TryGetBatteryFromSlot(ent.Owner, out var battery))
return null; return null;
if (MathHelper.CloseToPercent(battery.CurrentCharge, 0) || ent.Comp.Wattage > battery.CurrentCharge) var currentCharge = _battery.GetCharge(battery.Value.AsNullable());
if (MathHelper.CloseToPercent(currentCharge, 0) || ent.Comp.Wattage > currentCharge)
return 0; return 0;
return (byte?) ContentHelpers.RoundToNearestLevels(battery.CurrentCharge / battery.MaxCharge * 255, 255, HandheldLightComponent.StatusLevels); return (byte?)ContentHelpers.RoundToNearestLevels(currentCharge / battery.Value.Comp.MaxCharge * 255, 255, HandheldLightComponent.StatusLevels);
} }
private void OnRemove(Entity<HandheldLightComponent> ent, ref ComponentRemove args) private void OnRemove(Entity<HandheldLightComponent> ent, ref ComponentRemove args)
@ -153,6 +155,8 @@ namespace Content.Server.Light.EntitySystems
_activeLights.Clear(); _activeLights.Clear();
} }
// TODO: Very important: Make this charge rate based instead of instantly removing charge each update step.
// See PredictedBatteryComponent
public override void Update(float frameTime) public override void Update(float frameTime)
{ {
var toRemove = new RemQueue<Entity<HandheldLightComponent>>(); var toRemove = new RemQueue<Entity<HandheldLightComponent>>();
@ -199,8 +203,7 @@ namespace Content.Server.Light.EntitySystems
return false; return false;
} }
if (!_powerCell.TryGetBatteryFromSlot(uid, out var battery) && if (!_powerCell.TryGetBatteryFromSlot(uid.Owner, out var battery))
!TryComp(uid, out battery))
{ {
_audio.PlayPvs(_audio.ResolveSound(component.TurnOnFailSound), uid); _audio.PlayPvs(_audio.ResolveSound(component.TurnOnFailSound), uid);
_popup.PopupEntity(Loc.GetString("handheld-light-component-cell-missing-message"), uid, user); _popup.PopupEntity(Loc.GetString("handheld-light-component-cell-missing-message"), uid, user);
@ -210,7 +213,7 @@ namespace Content.Server.Light.EntitySystems
// To prevent having to worry about frame time in here. // To prevent having to worry about frame time in here.
// Let's just say you need a whole second of charge before you can turn it on. // Let's just say you need a whole second of charge before you can turn it on.
// Simple enough. // Simple enough.
if (component.Wattage > battery.CurrentCharge) if (component.Wattage > _battery.GetCharge(battery.Value.AsNullable()))
{ {
_audio.PlayPvs(_audio.ResolveSound(component.TurnOnFailSound), uid); _audio.PlayPvs(_audio.ResolveSound(component.TurnOnFailSound), uid);
_popup.PopupEntity(Loc.GetString("handheld-light-component-cell-dead-message"), uid, user); _popup.PopupEntity(Loc.GetString("handheld-light-component-cell-dead-message"), uid, user);
@ -227,19 +230,15 @@ namespace Content.Server.Light.EntitySystems
public void TryUpdate(Entity<HandheldLightComponent> uid, float frameTime) public void TryUpdate(Entity<HandheldLightComponent> uid, float frameTime)
{ {
var component = uid.Comp; var component = uid.Comp;
if (!_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery, null) && if (!_powerCell.TryGetBatteryFromSlot(uid.Owner, out var battery))
!TryComp(uid, out battery))
{ {
TurnOff(uid, false); TurnOff(uid, false);
return; return;
} }
if (batteryUid == null)
return;
var appearanceComponent = EntityManager.GetComponentOrNull<AppearanceComponent>(uid); var appearanceComponent = EntityManager.GetComponentOrNull<AppearanceComponent>(uid);
var fraction = battery.CurrentCharge / battery.MaxCharge; var fraction = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge;
if (fraction >= 0.30) if (fraction >= 0.30)
{ {
_appearance.SetData(uid, HandheldLightVisuals.Power, HandheldLightPowerStates.FullPower, appearanceComponent); _appearance.SetData(uid, HandheldLightVisuals.Power, HandheldLightPowerStates.FullPower, appearanceComponent);
@ -253,7 +252,7 @@ namespace Content.Server.Light.EntitySystems
_appearance.SetData(uid, HandheldLightVisuals.Power, HandheldLightPowerStates.Dying, appearanceComponent); _appearance.SetData(uid, HandheldLightVisuals.Power, HandheldLightPowerStates.Dying, appearanceComponent);
} }
if (component.Activated && !_battery.TryUseCharge((batteryUid.Value, battery), component.Wattage * frameTime)) if (component.Activated && !_battery.TryUseCharge(battery.Value.AsNullable(), component.Wattage * frameTime))
TurnOff(uid, false); TurnOff(uid, false);
UpdateLevel(uid); UpdateLevel(uid);

View File

@ -2,7 +2,6 @@ using System.Linq;
using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Systems; using Content.Server.Body.Systems;
using Content.Server.Mech.Components; using Content.Server.Mech.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.ActionBlocker; using Content.Shared.ActionBlocker;
using Content.Shared.Damage.Systems; using Content.Shared.Damage.Systems;
using Content.Shared.DoAfter; using Content.Shared.DoAfter;
@ -14,6 +13,7 @@ using Content.Shared.Mech.EntitySystems;
using Content.Shared.Movement.Events; using Content.Shared.Movement.Events;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Power.Components; using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Tools; using Content.Shared.Tools;
using Content.Shared.Tools.Components; using Content.Shared.Tools.Components;
using Content.Shared.Tools.Systems; using Content.Shared.Tools.Systems;
@ -33,7 +33,7 @@ public sealed partial class MechSystem : SharedMechSystem
{ {
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly AtmosphereSystem _atmosphere = default!; [Dependency] private readonly AtmosphereSystem _atmosphere = default!;
[Dependency] private readonly BatterySystem _battery = default!; [Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly ContainerSystem _container = default!; [Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
@ -88,7 +88,7 @@ public sealed partial class MechSystem : SharedMechSystem
if (TryComp<WiresPanelComponent>(uid, out var panel) && !panel.Open) if (TryComp<WiresPanelComponent>(uid, out var panel) && !panel.Open)
return; return;
if (component.BatterySlot.ContainedEntity == null && TryComp<BatteryComponent>(args.Used, out var battery)) if (component.BatterySlot.ContainedEntity == null && TryComp<PredictedBatteryComponent>(args.Used, out var battery))
{ {
InsertBattery(uid, args.Used, component, battery); InsertBattery(uid, args.Used, component, battery);
_actionBlocker.UpdateCanMove(uid); _actionBlocker.UpdateCanMove(uid);
@ -109,10 +109,10 @@ public sealed partial class MechSystem : SharedMechSystem
private void OnInsertBattery(EntityUid uid, MechComponent component, EntInsertedIntoContainerMessage args) private void OnInsertBattery(EntityUid uid, MechComponent component, EntInsertedIntoContainerMessage args)
{ {
if (args.Container != component.BatterySlot || !TryComp<BatteryComponent>(args.Entity, out var battery)) if (args.Container != component.BatterySlot || !TryComp<PredictedBatteryComponent>(args.Entity, out var battery))
return; return;
component.Energy = battery.CurrentCharge; component.Energy = _battery.GetCharge((args.Entity, battery));
component.MaxEnergy = battery.MaxCharge; component.MaxEnergy = battery.MaxCharge;
Dirty(uid, component); Dirty(uid, component);
@ -337,21 +337,23 @@ public sealed partial class MechSystem : SharedMechSystem
if (battery == null) if (battery == null)
return false; return false;
if (!TryComp<BatteryComponent>(battery, out var batteryComp)) if (!TryComp<PredictedBatteryComponent>(battery, out var batteryComp))
return false; return false;
_battery.SetCharge((battery.Value, batteryComp), batteryComp.CurrentCharge + delta.Float()); _battery.SetCharge((battery.Value, batteryComp), _battery.GetCharge((battery.Value, batteryComp)) + delta.Float());
if (batteryComp.CurrentCharge != component.Energy) //if there's a discrepency, we have to resync them // TODO: Power cells are predicted now, so no need to duplicate the charge level
var charge = _battery.GetCharge((battery.Value, batteryComp));
if (charge != component.Energy) //if there's a discrepency, we have to resync them
{ {
Log.Debug($"Battery charge was not equal to mech charge. Battery {batteryComp.CurrentCharge}. Mech {component.Energy}"); Log.Debug($"Battery charge was not equal to mech charge. Battery {charge}. Mech {component.Energy}");
component.Energy = batteryComp.CurrentCharge; component.Energy = charge;
Dirty(uid, component); Dirty(uid, component);
} }
_actionBlocker.UpdateCanMove(uid); _actionBlocker.UpdateCanMove(uid);
return true; return true;
} }
public void InsertBattery(EntityUid uid, EntityUid toInsert, MechComponent? component = null, BatteryComponent? battery = null) public void InsertBattery(EntityUid uid, EntityUid toInsert, MechComponent? component = null, PredictedBatteryComponent? battery = null)
{ {
if (!Resolve(uid, ref component, false)) if (!Resolve(uid, ref component, false))
return; return;
@ -360,7 +362,7 @@ public sealed partial class MechSystem : SharedMechSystem
return; return;
_container.Insert(toInsert, component.BatterySlot); _container.Insert(toInsert, component.BatterySlot);
component.Energy = battery.CurrentCharge; component.Energy = _battery.GetCharge((toInsert, battery));
component.MaxEnergy = battery.MaxCharge; component.MaxEnergy = battery.MaxCharge;
_actionBlocker.UpdateCanMove(uid); _actionBlocker.UpdateCanMove(uid);

View File

@ -2,7 +2,7 @@ using System.Linq;
using Content.Server.DeviceNetwork; using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Systems; using Content.Server.DeviceNetwork.Systems;
using Content.Server.Power.EntitySystems; // DeltaV using Content.Server.Power.EntitySystems; // DeltaV
using Content.Server.PowerCell; using Content.Shared.PowerCell;
using Content.Shared.DeviceNetwork; using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Events; using Content.Shared.DeviceNetwork.Events;
using Content.Shared.Medical.CrewMonitoring; using Content.Shared.Medical.CrewMonitoring;

View File

@ -5,7 +5,7 @@ using Content.Server.Electrocution;
using Content.Server.EUI; using Content.Server.EUI;
using Content.Server.Ghost; using Content.Server.Ghost;
using Content.Server.Popups; using Content.Server.Popups;
using Content.Server.PowerCell; using Content.Shared.PowerCell;
using Content.Shared.Traits.Assorted; using Content.Shared.Traits.Assorted;
using Content.Shared.Chat; using Content.Shared.Chat;
using Content.Shared.Damage.Components; using Content.Shared.Damage.Components;

View File

@ -1,5 +1,4 @@
using Content.Server.Medical.Components; using Content.Server.Medical.Components;
using Content.Server.PowerCell;
using Content.Shared.Body.Components; using Content.Shared.Body.Components;
using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Damage.Components; using Content.Shared.Damage.Components;
@ -12,6 +11,7 @@ using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.MedicalScanner; using Content.Shared.MedicalScanner;
using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Components;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.PowerCell;
using Content.Shared.Temperature.Components; using Content.Shared.Temperature.Components;
using Content.Shared.Traits.Assorted; using Content.Shared.Traits.Assorted;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
@ -113,7 +113,7 @@ public sealed class HealthAnalyzerSystem : EntitySystem
/// </summary> /// </summary>
private void OnAfterInteract(Entity<HealthAnalyzerComponent> uid, ref AfterInteractEvent args) private void OnAfterInteract(Entity<HealthAnalyzerComponent> uid, ref AfterInteractEvent args)
{ {
if (args.Target == null || !args.CanReach || !HasComp<MobStateComponent>(args.Target) || !_cell.HasDrawCharge(uid, user: args.User)) if (args.Target == null || !args.CanReach || !HasComp<MobStateComponent>(args.Target) || !_cell.HasDrawCharge(uid.Owner, user: args.User))
return; return;
_audio.PlayPvs(uid.Comp.ScanningBeginSound, uid); _audio.PlayPvs(uid.Comp.ScanningBeginSound, uid);
@ -133,7 +133,7 @@ public sealed class HealthAnalyzerSystem : EntitySystem
private void OnDoAfter(Entity<HealthAnalyzerComponent> uid, ref HealthAnalyzerDoAfterEvent args) private void OnDoAfter(Entity<HealthAnalyzerComponent> uid, ref HealthAnalyzerDoAfterEvent args)
{ {
if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid, user: args.User)) if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid.Owner, user: args.User))
return; return;
if (!uid.Comp.Silent) if (!uid.Comp.Silent)

View File

@ -7,6 +7,7 @@ using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems; using Content.Shared.Ninja.Systems;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Power.Components; using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Robust.Shared.Audio.Systems; using Robust.Shared.Audio.Systems;
namespace Content.Server.Ninja.Systems; namespace Content.Server.Ninja.Systems;
@ -17,6 +18,7 @@ namespace Content.Server.Ninja.Systems;
public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
{ {
[Dependency] private readonly BatterySystem _battery = default!; [Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly PredictedBatterySystem _predictedBattery = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedPopupSystem _popup = default!;
@ -37,7 +39,7 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
{ {
var (uid, comp) = ent; var (uid, comp) = ent;
var target = args.Target; var target = args.Target;
if (args.Handled || comp.BatteryUid is not {} battery || !HasComp<PowerNetworkBatteryComponent>(target)) if (args.Handled || comp.BatteryUid is not { } battery || !HasComp<PowerNetworkBatteryComponent>(target))
return; return;
// handles even if battery is full so you can actually see the poup // handles even if battery is full so you can actually see the poup
@ -70,7 +72,7 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
{ {
base.OnDoAfterAttempt(ent, ref args); base.OnDoAfterAttempt(ent, ref args);
if (ent.Comp.BatteryUid is not {} battery || _battery.IsFull(battery)) if (ent.Comp.BatteryUid is not { } battery || _battery.IsFull(battery))
args.Cancel(); args.Cancel();
} }
@ -78,7 +80,7 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
protected override bool TryDrainPower(Entity<BatteryDrainerComponent> ent, EntityUid target) protected override bool TryDrainPower(Entity<BatteryDrainerComponent> ent, EntityUid target)
{ {
var (uid, comp) = ent; var (uid, comp) = ent;
if (comp.BatteryUid == null || !TryComp<BatteryComponent>(comp.BatteryUid.Value, out var battery)) if (comp.BatteryUid == null || !TryComp<PredictedBatteryComponent>(comp.BatteryUid.Value, out var battery))
return false; return false;
if (!TryComp<BatteryComponent>(target, out var targetBattery) || !TryComp<PowerNetworkBatteryComponent>(target, out var pnb)) if (!TryComp<BatteryComponent>(target, out var targetBattery) || !TryComp<PowerNetworkBatteryComponent>(target, out var pnb))
@ -91,7 +93,7 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
} }
var available = targetBattery.CurrentCharge; var available = targetBattery.CurrentCharge;
var required = battery.MaxCharge - battery.CurrentCharge; var required = battery.MaxCharge - _predictedBattery.GetCharge((comp.BatteryUid.Value, battery));
// higher tier storages can charge more // higher tier storages can charge more
var maxDrained = pnb.MaxSupply * comp.DrainTime; var maxDrained = pnb.MaxSupply * comp.DrainTime;
var input = Math.Min(Math.Min(available, required / comp.DrainEfficiency), maxDrained); var input = Math.Min(Math.Min(available, required / comp.DrainEfficiency), maxDrained);
@ -99,13 +101,15 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
return false; return false;
var output = input * comp.DrainEfficiency; var output = input * comp.DrainEfficiency;
_battery.SetCharge((comp.BatteryUid.Value, battery), battery.CurrentCharge + output); // PowerCells use PredictedBatteryComponent
// SMES, substations and APCs use BatteryComponent
_predictedBattery.ChangeCharge((comp.BatteryUid.Value, battery), output);
// TODO: create effect message or something // TODO: create effect message or something
Spawn("EffectSparks", Transform(target).Coordinates); Spawn("EffectSparks", Transform(target).Coordinates);
_audio.PlayPvs(comp.SparkSound, target); _audio.PlayPvs(comp.SparkSound, target);
_popup.PopupEntity(Loc.GetString("battery-drainer-success", ("battery", target)), uid, uid); _popup.PopupEntity(Loc.GetString("battery-drainer-success", ("battery", target)), uid, uid);
// repeat the doafter until battery is full // repeat the doafter until battery is full
return !_battery.IsFull((comp.BatteryUid.Value, battery)); return !_predictedBattery.IsFull((comp.BatteryUid.Value, battery));
} }
} }

View File

@ -1,15 +1,15 @@
using Content.Server.Ninja.Events; using Content.Server.Ninja.Events;
using Content.Server.Power.EntitySystems;
using Content.Shared.Hands.EntitySystems; using Content.Shared.Hands.EntitySystems;
using Content.Shared.Ninja.Components; using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems; using Content.Shared.Ninja.Systems;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Power.EntitySystems;
namespace Content.Server.Ninja.Systems; namespace Content.Server.Ninja.Systems;
public sealed class ItemCreatorSystem : SharedItemCreatorSystem public sealed class ItemCreatorSystem : SharedItemCreatorSystem
{ {
[Dependency] private readonly BatterySystem _battery = default!; [Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!; [Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedPopupSystem _popup = default!;
@ -24,7 +24,7 @@ public sealed class ItemCreatorSystem : SharedItemCreatorSystem
private void OnCreateItem(Entity<ItemCreatorComponent> ent, ref CreateItemEvent args) private void OnCreateItem(Entity<ItemCreatorComponent> ent, ref CreateItemEvent args)
{ {
var (uid, comp) = ent; var (uid, comp) = ent;
if (comp.Battery is not {} battery) if (comp.Battery is not { } battery)
return; return;
args.Handled = true; args.Handled = true;

View File

@ -1,11 +1,10 @@
using Content.Server.Ninja.Events; using Content.Server.Ninja.Events;
using Content.Server.Power.Components;
using Content.Server.PowerCell;
using Content.Shared.Emp; using Content.Shared.Emp;
using Content.Shared.Hands.EntitySystems; using Content.Shared.Hands.EntitySystems;
using Content.Shared.Ninja.Components; using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems; using Content.Shared.Ninja.Systems;
using Content.Shared.Power.Components; using Content.Shared.Power.Components;
using Content.Shared.PowerCell;
using Content.Shared.PowerCell.Components; using Content.Shared.PowerCell.Components;
using Robust.Shared.Containers; using Robust.Shared.Containers;
@ -13,6 +12,7 @@ namespace Content.Server.Ninja.Systems;
/// <summary> /// <summary>
/// Handles power cell upgrading and actions. /// Handles power cell upgrading and actions.
/// TODO: Move all of this to shared and predict it
/// </summary> /// </summary>
public sealed class NinjaSuitSystem : SharedNinjaSuitSystem public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
{ {
@ -51,8 +51,6 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
RaiseLocalEvent(user, ref ev); RaiseLocalEvent(user, ref ev);
} }
// TODO: if/when battery is in shared, put this there too
// TODO: or put MaxCharge in shared along with powercellslot
private void OnSuitInsertAttempt(EntityUid uid, NinjaSuitComponent comp, ContainerIsInsertingAttemptEvent args) private void OnSuitInsertAttempt(EntityUid uid, NinjaSuitComponent comp, ContainerIsInsertingAttemptEvent args)
{ {
// this is for handling battery upgrading, not stopping actions from being added // this is for handling battery upgrading, not stopping actions from being added
@ -61,10 +59,10 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
return; return;
// no power cell for some reason??? allow it // no power cell for some reason??? allow it
if (!_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery)) if (!_powerCell.TryGetBatteryFromSlot(uid, out var battery))
return; return;
if (!TryComp<BatteryComponent>(args.EntityUid, out var inserting)) if (!TryComp<PredictedBatteryComponent>(args.EntityUid, out var inserting))
{ {
args.Cancel(); args.Cancel();
return; return;
@ -73,7 +71,7 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
var user = Transform(uid).ParentUid; var user = Transform(uid).ParentUid;
// can only upgrade power cell, not swap to recharge instantly otherwise ninja could just swap batteries with flashlights in maints for easy power // can only upgrade power cell, not swap to recharge instantly otherwise ninja could just swap batteries with flashlights in maints for easy power
if (GetCellScore(args.EntityUid, inserting) <= GetCellScore(batteryUid.Value, battery)) if (GetCellScore(args.EntityUid, inserting) <= GetCellScore(battery.Value, battery.Value))
{ {
args.Cancel(); args.Cancel();
Popup.PopupEntity(Loc.GetString("ninja-cell-downgrade"), user, user); Popup.PopupEntity(Loc.GetString("ninja-cell-downgrade"), user, user);
@ -90,11 +88,11 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
} }
// this function assigns a score to a power cell depending on the capacity, to be used when comparing which cell is better. // this function assigns a score to a power cell depending on the capacity, to be used when comparing which cell is better.
private float GetCellScore(EntityUid uid, BatteryComponent battcomp) private float GetCellScore(EntityUid uid, PredictedBatteryComponent battcomp)
{ {
// if a cell is able to automatically recharge, boost the score drastically depending on the recharge rate, // if a cell is able to automatically recharge, boost the score drastically depending on the recharge rate,
// this is to ensure a ninja can still upgrade to a micro reactor cell even if they already have a medium or high. // this is to ensure a ninja can still upgrade to a micro reactor cell even if they already have a medium or high.
if (TryComp<BatterySelfRechargerComponent>(uid, out var selfcomp) && selfcomp.AutoRecharge) if (TryComp<PredictedBatterySelfRechargerComponent>(uid, out var selfcomp))
return battcomp.MaxCharge + selfcomp.AutoRechargeRate * AutoRechargeValue; return battcomp.MaxCharge + selfcomp.AutoRechargeRate * AutoRechargeValue;
return battcomp.MaxCharge; return battcomp.MaxCharge;
} }
@ -136,7 +134,6 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
Popup.PopupEntity(Loc.GetString(message), user, user); Popup.PopupEntity(Loc.GetString(message), user, user);
} }
// TODO: Move this to shared when power cells are predicted.
private void OnEmp(Entity<NinjaSuitComponent> ent, ref NinjaEmpEvent args) private void OnEmp(Entity<NinjaSuitComponent> ent, ref NinjaEmpEvent args)
{ {
var (uid, comp) = ent; var (uid, comp) = ent;

View File

@ -2,8 +2,6 @@ using Content.Server.Communications;
using Content.Server.CriminalRecords.Systems; using Content.Server.CriminalRecords.Systems;
using Content.Server.Objectives.Components; using Content.Server.Objectives.Components;
using Content.Server.Objectives.Systems; using Content.Server.Objectives.Systems;
using Content.Server.Power.EntitySystems;
using Content.Server.PowerCell;
using Content.Server.Research.Systems; using Content.Server.Research.Systems;
using Content.Shared.Alert; using Content.Shared.Alert;
using Content.Shared.Doors.Components; using Content.Shared.Doors.Components;
@ -12,6 +10,8 @@ using Content.Shared.Mind;
using Content.Shared.Ninja.Components; using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems; using Content.Shared.Ninja.Systems;
using Content.Shared.Power.Components; using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.PowerCell;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Rounding; using Content.Shared.Rounding;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
@ -24,7 +24,7 @@ namespace Content.Server.Ninja.Systems;
public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
{ {
[Dependency] private readonly AlertsSystem _alerts = default!; [Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly BatterySystem _battery = default!; [Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly CodeConditionSystem _codeCondition = default!; [Dependency] private readonly CodeConditionSystem _codeCondition = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!; [Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly SharedMindSystem _mind = default!; [Dependency] private readonly SharedMindSystem _mind = default!;
@ -39,6 +39,8 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
SubscribeLocalEvent<SpaceNinjaComponent, CriminalRecordsHackedEvent>(OnCriminalRecordsHacked); SubscribeLocalEvent<SpaceNinjaComponent, CriminalRecordsHackedEvent>(OnCriminalRecordsHacked);
} }
// TODO: Make this charge rate based instead of updating it every single tick.
// Or make it client side, since power cells are predicted.
public override void Update(float frameTime) public override void Update(float frameTime)
{ {
var query = EntityQueryEnumerator<SpaceNinjaComponent>(); var query = EntityQueryEnumerator<SpaceNinjaComponent>();
@ -62,7 +64,7 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
return newCount - oldCount; return newCount - oldCount;
} }
// TODO: can probably copy paste borg code here // TODO: Generic charge indicator that is combined with borg code.
/// <summary> /// <summary>
/// Update the alert for the ninja's suit power indicator. /// Update the alert for the ninja's suit power indicator.
/// </summary> /// </summary>
@ -75,10 +77,10 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
return; return;
} }
if (GetNinjaBattery(uid, out _, out var battery)) if (GetNinjaBattery(uid, out var batteryUid, out var batteryComp))
{ {
var severity = ContentHelpers.RoundToLevels(MathF.Max(0f, battery.CurrentCharge), battery.MaxCharge, 8); var severity = ContentHelpers.RoundToLevels(MathF.Max(0f, _battery.GetCharge((batteryUid.Value, batteryComp))), batteryComp.MaxCharge, 8);
_alerts.ShowAlert(uid, comp.SuitPowerAlert, (short) severity); _alerts.ShowAlert(uid, comp.SuitPowerAlert, (short)severity);
} }
else else
{ {
@ -89,17 +91,19 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
/// <summary> /// <summary>
/// Get the battery component in a ninja's suit, if it's worn. /// Get the battery component in a ninja's suit, if it's worn.
/// </summary> /// </summary>
public bool GetNinjaBattery(EntityUid user, [NotNullWhen(true)] out EntityUid? uid, [NotNullWhen(true)] out BatteryComponent? battery) public bool GetNinjaBattery(EntityUid user, [NotNullWhen(true)] out EntityUid? batteryUid, [NotNullWhen(true)] out PredictedBatteryComponent? batteryComp)
{ {
if (TryComp<SpaceNinjaComponent>(user, out var ninja) if (TryComp<SpaceNinjaComponent>(user, out var ninja)
&& ninja.Suit != null && ninja.Suit != null
&& _powerCell.TryGetBatteryFromSlot(ninja.Suit.Value, out uid, out battery)) && _powerCell.TryGetBatteryFromSlot(ninja.Suit.Value, out var battery))
{ {
batteryUid = battery.Value.Owner;
batteryComp = battery.Value.Comp;
return true; return true;
} }
uid = null; batteryUid = null;
battery = null; batteryComp = null;
return false; return false;
} }

View File

@ -1,10 +1,10 @@
using Content.Server.Ninja.Events; using Content.Server.Ninja.Events;
using Content.Server.Power.EntitySystems;
using Content.Shared.Damage.Systems; using Content.Shared.Damage.Systems;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Ninja.Components; using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems; using Content.Shared.Ninja.Systems;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Stunnable; using Content.Shared.Stunnable;
using Content.Shared.Timing; using Content.Shared.Timing;
using Content.Shared.Whitelist; using Content.Shared.Whitelist;
@ -17,7 +17,7 @@ namespace Content.Server.Ninja.Systems;
/// </summary> /// </summary>
public sealed class StunProviderSystem : SharedStunProviderSystem public sealed class StunProviderSystem : SharedStunProviderSystem
{ {
[Dependency] private readonly BatterySystem _battery = default!; [Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!; [Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedAudioSystem _audio = default!;

View File

@ -2,7 +2,6 @@ using Content.Server.AlertLevel;
using Content.Server.Audio; using Content.Server.Audio;
using Content.Server.Chat.Systems; using Content.Server.Chat.Systems;
using Content.Server.Explosion.EntitySystems; using Content.Server.Explosion.EntitySystems;
using Content.Server.Kitchen.Components;
using Content.Server.Pinpointer; using Content.Server.Pinpointer;
using Content.Server.Popups; using Content.Server.Popups;
using Content.Server.Station.Systems; using Content.Server.Station.Systems;
@ -11,7 +10,7 @@ using Content.Shared.Containers.ItemSlots;
using Content.Shared.Coordinates.Helpers; using Content.Shared.Coordinates.Helpers;
using Content.Shared.DoAfter; using Content.Shared.DoAfter;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.Kitchen.Components; using Content.Shared.Kitchen;
using Content.Shared.Maps; using Content.Shared.Maps;
using Content.Shared.Nuke; using Content.Shared.Nuke;
using Content.Shared.Popups; using Content.Shared.Popups;

View File

@ -2,9 +2,9 @@ using Content.Server.Ghost.Roles;
using Content.Server.Ghost.Roles.Components; using Content.Server.Ghost.Roles.Components;
using Content.Server.Instruments; using Content.Server.Instruments;
using Content.Shared.Kitchen.Components; // DeltaV - shared using Content.Shared.Kitchen.Components; // DeltaV - shared
using Content.Server.Store.Systems;
using Content.Shared.Interaction.Events; using Content.Shared.Interaction.Events;
using Content.Shared.Mind.Components; using Content.Shared.Mind.Components;
using Content.Shared.Kitchen;
using Content.Shared.PAI; using Content.Shared.PAI;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Instruments; using Content.Shared.Instruments;

View File

@ -1,4 +1,4 @@
using Content.Server.PowerCell; using Content.Shared.PowerCell;
using Content.Shared.Pinpointer; using Content.Shared.Pinpointer;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Player; using Robust.Shared.Player;

View File

@ -1,10 +0,0 @@
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Power;
namespace Content.Server.Power.Components
{
[RegisterComponent]
public sealed partial class ActiveChargerComponent : Component
{
}
}

View File

@ -1,6 +0,0 @@
namespace Content.Server.Power.Components
{
[RegisterComponent]
public sealed partial class ExaminableBatteryComponent : Component
{}
}

View File

@ -1,8 +1,14 @@
using Content.Shared.Power; using Content.Shared.Power;
using Content.Shared.Power.Components; using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
namespace Content.Server.Power.EntitySystems; namespace Content.Server.Power.EntitySystems;
/// <summary>
/// Responsible for <see cref="BatteryComponent"/>.
/// Unpredicted equivalent of <see cref="PredictedBatterySystem"/>.
/// If you make changes to this make sure to keep the two consistent.
/// </summary>
public sealed partial class BatterySystem public sealed partial class BatterySystem
{ {
public override float ChangeCharge(Entity<BatteryComponent?> ent, float amount) public override float ChangeCharge(Entity<BatteryComponent?> ent, float amount)
@ -13,6 +19,10 @@ public sealed partial class BatterySystem
var newValue = Math.Clamp(ent.Comp.CurrentCharge + amount, 0, ent.Comp.MaxCharge); var newValue = Math.Clamp(ent.Comp.CurrentCharge + amount, 0, ent.Comp.MaxCharge);
var delta = newValue - ent.Comp.CurrentCharge; var delta = newValue - ent.Comp.CurrentCharge;
if (delta == 0f)
return delta;
ent.Comp.CurrentCharge = newValue; ent.Comp.CurrentCharge = newValue;
TrySetChargeCooldown(ent.Owner); TrySetChargeCooldown(ent.Owner);
@ -24,8 +34,8 @@ public sealed partial class BatterySystem
public override float UseCharge(Entity<BatteryComponent?> ent, float amount) public override float UseCharge(Entity<BatteryComponent?> ent, float amount)
{ {
if (amount <= 0 || !Resolve(ent, ref ent.Comp) || ent.Comp.CurrentCharge == 0) if (amount <= 0f || !Resolve(ent, ref ent.Comp) || ent.Comp.CurrentCharge == 0)
return 0; return 0f;
return ChangeCharge(ent, -amount); return ChangeCharge(ent, -amount);
} }
@ -72,6 +82,45 @@ public sealed partial class BatterySystem
RaiseLocalEvent(ent, ref ev); RaiseLocalEvent(ent, ref ev);
} }
/// <summary>
/// Gets the battery's current charge.
/// </summary>
public float GetCharge(Entity<BatteryComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return 0f;
return ent.Comp.CurrentCharge;
}
/// <summary>
/// Gets number of remaining uses for the given charge cost.
/// </summary>
public int GetRemainingUses(Entity<BatteryComponent?> ent, float cost)
{
if (cost <= 0)
return 0;
if (!Resolve(ent, ref ent.Comp))
return 0;
return (int)(ent.Comp.CurrentCharge / cost);
}
/// <summary>
/// Gets number of maximum uses at full charge for the given charge cost.
/// </summary>
public int GetMaxUses(Entity<BatteryComponent?> ent, float cost)
{
if (cost <= 0)
return 0;
if (!Resolve(ent, ref ent.Comp))
return 0;
return (int)(ent.Comp.MaxCharge / cost);
}
public override void TrySetChargeCooldown(Entity<BatterySelfRechargerComponent?> ent) public override void TrySetChargeCooldown(Entity<BatterySelfRechargerComponent?> ent)
{ {
if (!Resolve(ent, ref ent.Comp, false)) if (!Resolve(ent, ref ent.Comp, false))

View File

@ -11,6 +11,11 @@ using Robust.Shared.Timing;
namespace Content.Server.Power.EntitySystems; namespace Content.Server.Power.EntitySystems;
/// <summary>
/// Responsible for <see cref="BatteryComponent"/>.
/// Unpredicted equivalent of <see cref="PredictedBatterySystem"/>.
/// If you make changes to this make sure to keep the two consistent.
/// </summary>
[UsedImplicitly] [UsedImplicitly]
public sealed partial class BatterySystem : SharedBatterySystem public sealed partial class BatterySystem : SharedBatterySystem
{ {
@ -20,7 +25,8 @@ public sealed partial class BatterySystem : SharedBatterySystem
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<ExaminableBatteryComponent, ExaminedEvent>(OnExamine); SubscribeLocalEvent<BatteryComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<BatteryComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<BatteryComponent, RejuvenateEvent>(OnBatteryRejuvenate); SubscribeLocalEvent<BatteryComponent, RejuvenateEvent>(OnBatteryRejuvenate);
SubscribeLocalEvent<PowerNetworkBatteryComponent, RejuvenateEvent>(OnNetBatteryRejuvenate); SubscribeLocalEvent<PowerNetworkBatteryComponent, RejuvenateEvent>(OnNetBatteryRejuvenate);
SubscribeLocalEvent<BatteryComponent, PriceCalculationEvent>(CalculateBatteryPrice); SubscribeLocalEvent<BatteryComponent, PriceCalculationEvent>(CalculateBatteryPrice);
@ -31,27 +37,31 @@ public sealed partial class BatterySystem : SharedBatterySystem
SubscribeLocalEvent<NetworkBatteryPostSync>(PostSync); SubscribeLocalEvent<NetworkBatteryPostSync>(PostSync);
} }
private void OnInit(Entity<BatteryComponent> ent, ref ComponentInit args)
{
DebugTools.Assert(!HasComp<PredictedBatteryComponent>(ent), $"{ent} has both BatteryComponent and PredictedBatteryComponent");
}
private void OnNetBatteryRejuvenate(Entity<PowerNetworkBatteryComponent> ent, ref RejuvenateEvent args) private void OnNetBatteryRejuvenate(Entity<PowerNetworkBatteryComponent> ent, ref RejuvenateEvent args)
{ {
ent.Comp.NetworkBattery.CurrentStorage = ent.Comp.NetworkBattery.Capacity; ent.Comp.NetworkBattery.CurrentStorage = ent.Comp.NetworkBattery.Capacity;
} }
private void OnBatteryRejuvenate(Entity<BatteryComponent> ent, ref RejuvenateEvent args) private void OnBatteryRejuvenate(Entity<BatteryComponent> ent, ref RejuvenateEvent args)
{ {
SetCharge(ent.AsNullable(), ent.Comp.MaxCharge); SetCharge(ent.AsNullable(), ent.Comp.MaxCharge);
} }
private void OnExamine(Entity<ExaminableBatteryComponent> ent, ref ExaminedEvent args) private void OnExamine(Entity<BatteryComponent> ent, ref ExaminedEvent args)
{ {
if (!args.IsInDetailsRange) if (!args.IsInDetailsRange)
return; return;
if (!TryComp<BatteryComponent>(ent, out var battery)) if (!HasComp<ExaminableBatteryComponent>(ent))
return; return;
var chargePercentRounded = 0; var chargePercentRounded = 0;
if (battery.MaxCharge != 0) if (ent.Comp.MaxCharge != 0)
chargePercentRounded = (int)(100 * battery.CurrentCharge / battery.MaxCharge); chargePercentRounded = (int)(100 * ent.Comp.CurrentCharge / ent.Comp.MaxCharge);
args.PushMarkup( args.PushMarkup(
Loc.GetString( Loc.GetString(
"examinable-battery-component-examine-detail", "examinable-battery-component-examine-detail",

View File

@ -1,291 +0,0 @@
using Content.Server.Power.Components;
using Content.Shared.Examine;
using Content.Server.PowerCell;
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.PowerCell.Components;
using Content.Shared.Emp;
using JetBrains.Annotations;
using Robust.Shared.Containers;
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Storage.Components;
using Robust.Server.Containers;
using Content.Shared.Whitelist;
using Content.Server._DV.Augments; // DeltaV - bodies can have an augment power cell
namespace Content.Server.Power.EntitySystems;
[UsedImplicitly]
public sealed class ChargerSystem : SharedChargerSystem
{
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly AugmentPowerCellSystem _augments = default!; // DeltaV - bodies can have an augment power cell
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ChargerComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<ChargerComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<ChargerComponent, EntInsertedIntoContainerMessage>(OnInserted);
SubscribeLocalEvent<ChargerComponent, EntRemovedFromContainerMessage>(OnRemoved);
SubscribeLocalEvent<ChargerComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
SubscribeLocalEvent<ChargerComponent, InsertIntoEntityStorageAttemptEvent>(OnEntityStorageInsertAttempt);
SubscribeLocalEvent<ChargerComponent, ExaminedEvent>(OnChargerExamine);
}
private void OnStartup(EntityUid uid, ChargerComponent component, ComponentStartup args)
{
UpdateStatus(uid, component);
}
private void OnChargerExamine(EntityUid uid, ChargerComponent component, ExaminedEvent args)
{
using (args.PushGroup(nameof(ChargerComponent)))
{
// rate at which the charger charges
args.PushMarkup(Loc.GetString("charger-examine", ("color", "yellow"), ("chargeRate", (int)component.ChargeRate)));
// try to get contents of the charger
if (!_container.TryGetContainer(uid, component.SlotId, out var container))
return;
if (HasComp<PowerCellSlotComponent>(uid))
return;
// if charger is empty and not a power cell type charger, add empty message
// power cells have their own empty message by default, for things like flash lights
if (container.ContainedEntities.Count == 0)
{
args.PushMarkup(Loc.GetString("charger-empty"));
}
else
{
// add how much each item is charged it
foreach (var contained in container.ContainedEntities)
{
if (!TryComp<BatteryComponent>(contained, out var battery))
continue;
var chargePercentage = (battery.CurrentCharge / battery.MaxCharge) * 100;
args.PushMarkup(Loc.GetString("charger-content", ("chargePercentage", (int)chargePercentage)));
}
}
}
}
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<ActiveChargerComponent, ChargerComponent, ContainerManagerComponent>();
while (query.MoveNext(out var uid, out _, out var charger, out var containerComp))
{
if (!_container.TryGetContainer(uid, charger.SlotId, out var container, containerComp))
continue;
if (charger.Status == CellChargerStatus.Empty || charger.Status == CellChargerStatus.Charged || container.ContainedEntities.Count == 0)
continue;
foreach (var contained in container.ContainedEntities)
{
TransferPower(uid, contained, charger, frameTime);
}
}
}
private void OnPowerChanged(EntityUid uid, ChargerComponent component, ref PowerChangedEvent args)
{
UpdateStatus(uid, component);
}
private void OnInserted(EntityUid uid, ChargerComponent component, EntInsertedIntoContainerMessage args)
{
if (!component.Initialized)
return;
if (args.Container.ID != component.SlotId)
return;
UpdateStatus(uid, component);
}
private void OnRemoved(EntityUid uid, ChargerComponent component, EntRemovedFromContainerMessage args)
{
if (args.Container.ID != component.SlotId)
return;
UpdateStatus(uid, component);
}
/// <summary>
/// Verify that the entity being inserted is actually rechargeable.
/// </summary>
private void OnInsertAttempt(EntityUid uid, ChargerComponent component, ContainerIsInsertingAttemptEvent args)
{
if (!component.Initialized)
return;
if (args.Container.ID != component.SlotId)
return;
if (!TryComp<PowerCellSlotComponent>(args.EntityUid, out var cellSlot))
return;
if (!cellSlot.FitsInCharger)
args.Cancel();
}
private void OnEntityStorageInsertAttempt(EntityUid uid, ChargerComponent component, ref InsertIntoEntityStorageAttemptEvent args)
{
if (!component.Initialized || args.Cancelled)
return;
if (!TryComp<PowerCellSlotComponent>(uid, out var cellSlot))
return;
if (!cellSlot.FitsInCharger)
args.Cancelled = true;
}
private void UpdateStatus(EntityUid uid, ChargerComponent component)
{
var status = GetStatus(uid, component);
TryComp(uid, out AppearanceComponent? appearance);
if (!_container.TryGetContainer(uid, component.SlotId, out var container))
return;
_appearance.SetData(uid, CellVisual.Occupied, container.ContainedEntities.Count != 0, appearance);
if (component.Status == status || !TryComp(uid, out ApcPowerReceiverComponent? receiver))
return;
component.Status = status;
if (component.Status == CellChargerStatus.Charging)
{
AddComp<ActiveChargerComponent>(uid);
}
else
{
RemComp<ActiveChargerComponent>(uid);
}
switch (component.Status)
{
case CellChargerStatus.Off:
receiver.Load = 0;
_appearance.SetData(uid, CellVisual.Light, CellChargerStatus.Off, appearance);
break;
case CellChargerStatus.Empty:
receiver.Load = 0;
_appearance.SetData(uid, CellVisual.Light, CellChargerStatus.Empty, appearance);
break;
case CellChargerStatus.Charging:
receiver.Load = component.ChargeRate;
_appearance.SetData(uid, CellVisual.Light, CellChargerStatus.Charging, appearance);
break;
case CellChargerStatus.Charged:
receiver.Load = 0;
_appearance.SetData(uid, CellVisual.Light, CellChargerStatus.Charged, appearance);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private CellChargerStatus GetStatus(EntityUid uid, ChargerComponent component)
{
if (!component.Portable)
{
if (!TryComp(uid, out TransformComponent? transformComponent) || !transformComponent.Anchored)
return CellChargerStatus.Off;
}
if (!TryComp(uid, out ApcPowerReceiverComponent? apcPowerReceiverComponent))
return CellChargerStatus.Off;
if (!component.Portable && !apcPowerReceiverComponent.Powered)
return CellChargerStatus.Off;
if (HasComp<EmpDisabledComponent>(uid))
return CellChargerStatus.Off;
if (!_container.TryGetContainer(uid, component.SlotId, out var container))
return CellChargerStatus.Off;
if (container.ContainedEntities.Count == 0)
return CellChargerStatus.Empty;
if (!SearchForBattery(container.ContainedEntities[0], out var heldEnt, out var heldBattery))
return CellChargerStatus.Off;
if (_battery.IsFull((heldEnt.Value, heldBattery)))
return CellChargerStatus.Charged;
return CellChargerStatus.Charging;
}
private void TransferPower(EntityUid uid, EntityUid targetEntity, ChargerComponent component, float frameTime)
{
if (!TryComp(uid, out ApcPowerReceiverComponent? receiverComponent))
return;
if (!receiverComponent.Powered)
return;
if (_whitelistSystem.IsWhitelistFail(component.Whitelist, targetEntity))
return;
if (!SearchForBattery(targetEntity, out var batteryUid, out var heldBattery))
return;
_battery.SetCharge((batteryUid.Value, heldBattery), heldBattery.CurrentCharge + component.ChargeRate * frameTime);
UpdateStatus(uid, component);
}
// Begin DeltaV - event-based search for battery
public bool SearchForBattery(EntityUid uid, [NotNullWhen(true)] out EntityUid? batteryUid, [NotNullWhen(true)] out BatteryComponent? component)
{
// try get a battery directly on the inserted entity
if (TryComp(uid, out component))
{
batteryUid = uid;
return true;
}
var evt = new SearchForBatteryEvent();
RaiseLocalEvent(uid, ref evt);
if (evt.Handled)
{
batteryUid = evt.Uid;
component = evt.Component;
return evt.Handled;
}
batteryUid = null;
component = null;
return false;
}
// End DeltaV - event-based search for battery
}
// Begin DeltaV - event-based search for battery
/// <summary>
/// Event raised to search for batteries within an entity
/// </summary>
[ByRefEvent]
public struct SearchForBatteryEvent
{
public EntityUid? Uid;
public BatteryComponent? Component;
public bool Handled;
}
// End DeltaV - event-based search for battery

View File

@ -161,11 +161,6 @@ namespace Content.Server.Power.EntitySystems
return !_recQuery.Resolve(uid, ref receiver, false) || receiver.Powered; return !_recQuery.Resolve(uid, ref receiver, false) || receiver.Powered;
} }
public override void SetLoad(SharedApcPowerReceiverComponent comp, float load) // Goobstation - override shared method
{
((ApcPowerReceiverComponent) comp).Load = load; // Goobstation
}
public override bool ResolveApc(EntityUid entity, [NotNullWhen(true)] ref SharedApcPowerReceiverComponent? component) public override bool ResolveApc(EntityUid entity, [NotNullWhen(true)] ref SharedApcPowerReceiverComponent? component)
{ {
if (component != null) if (component != null)

View File

@ -1,10 +1,12 @@
using Content.Server.Administration.Logs; using Content.Server.Administration.Logs;
using Content.Server.Explosion.EntitySystems; using Content.Server.Explosion.EntitySystems;
using Content.Shared.Kitchen.Components;
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Kitchen;
using Content.Shared.Power;
using Content.Shared.Power.Components; using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Rejuvenate; using Content.Shared.Rejuvenate;
namespace Content.Server.Power.EntitySystems; namespace Content.Server.Power.EntitySystems;
@ -16,6 +18,7 @@ public sealed class RiggableSystem : EntitySystem
{ {
[Dependency] private readonly ExplosionSystem _explosionSystem = default!; [Dependency] private readonly ExplosionSystem _explosionSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly PredictedBatterySystem _predictedBattery = default!;
public override void Initialize() public override void Initialize()
{ {
@ -23,6 +26,8 @@ public sealed class RiggableSystem : EntitySystem
SubscribeLocalEvent<RiggableComponent, RejuvenateEvent>(OnRejuvenate); SubscribeLocalEvent<RiggableComponent, RejuvenateEvent>(OnRejuvenate);
SubscribeLocalEvent<RiggableComponent, BeingMicrowavedEvent>(OnMicrowaved); SubscribeLocalEvent<RiggableComponent, BeingMicrowavedEvent>(OnMicrowaved);
SubscribeLocalEvent<RiggableComponent, SolutionContainerChangedEvent>(OnSolutionChanged); SubscribeLocalEvent<RiggableComponent, SolutionContainerChangedEvent>(OnSolutionChanged);
SubscribeLocalEvent<RiggableComponent, ChargeChangedEvent>(OnChargeChanged);
SubscribeLocalEvent<RiggableComponent, PredictedBatteryChargeChangedEvent>(OnChargeChanged);
} }
private void OnRejuvenate(Entity<RiggableComponent> entity, ref RejuvenateEvent args) private void OnRejuvenate(Entity<RiggableComponent> entity, ref RejuvenateEvent args)
@ -34,14 +39,22 @@ public sealed class RiggableSystem : EntitySystem
{ {
if (TryComp<BatteryComponent>(entity, out var batteryComponent)) if (TryComp<BatteryComponent>(entity, out var batteryComponent))
{ {
if (batteryComponent.CurrentCharge == 0) if (batteryComponent.CurrentCharge == 0f)
return; return;
Explode(entity, batteryComponent.CurrentCharge);
args.Handled = true;
} }
args.Handled = true; if (TryComp<PredictedBatteryComponent>(entity, out var predictedBatteryComponent))
{
var charge = _predictedBattery.GetCharge((entity, predictedBatteryComponent));
if (charge == 0f)
return;
// What the fuck are you doing??? Explode(entity, charge);
Explode(entity.Owner, batteryComponent, args.User); args.Handled = true;
}
} }
private void OnSolutionChanged(Entity<RiggableComponent> entity, ref SolutionContainerChangedEvent args) private void OnSolutionChanged(Entity<RiggableComponent> entity, ref SolutionContainerChangedEvent args)
@ -59,14 +72,42 @@ public sealed class RiggableSystem : EntitySystem
} }
} }
public void Explode(EntityUid uid, BatteryComponent? battery = null, EntityUid? cause = null) public void Explode(EntityUid uid, float charge, EntityUid? cause = null)
{ {
if (!Resolve(uid, ref battery)) var radius = MathF.Min(5, MathF.Sqrt(charge) / 9);
return;
var radius = MathF.Min(5, MathF.Sqrt(battery.CurrentCharge) / 9); _explosionSystem.TriggerExplosive(uid, radius: radius, user: cause);
_explosionSystem.TriggerExplosive(uid, radius: radius, user:cause);
QueueDel(uid); QueueDel(uid);
} }
// non-predicted batteries
private void OnChargeChanged(Entity<RiggableComponent> ent, ref ChargeChangedEvent args)
{
if (!ent.Comp.IsRigged)
return;
if (TryComp<BatteryComponent>(ent, out var batteryComponent))
{
if (batteryComponent.CurrentCharge == 0f)
return;
Explode(ent, batteryComponent.CurrentCharge);
}
}
// predicted batteries
private void OnChargeChanged(Entity<RiggableComponent> ent, ref PredictedBatteryChargeChangedEvent args)
{
if (!ent.Comp.IsRigged)
return;
if (TryComp<PredictedBatteryComponent>(ent, out var predictedBatteryComponent))
{
var charge = _predictedBattery.GetCharge((ent.Owner, predictedBatteryComponent));
if (charge == 0f)
return;
Explode(ent, charge);
}
}
} }

View File

@ -2,6 +2,7 @@ using Content.Server.Administration;
using Content.Server.Power.EntitySystems; using Content.Server.Power.EntitySystems;
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.Power.Components; using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Robust.Shared.Console; using Robust.Shared.Console;
namespace Content.Server.Power namespace Content.Server.Power
@ -10,6 +11,7 @@ namespace Content.Server.Power
public sealed class SetBatteryPercentCommand : LocalizedEntityCommands public sealed class SetBatteryPercentCommand : LocalizedEntityCommands
{ {
[Dependency] private readonly BatterySystem _batterySystem = default!; [Dependency] private readonly BatterySystem _batterySystem = default!;
[Dependency] private readonly PredictedBatterySystem _predictedBatterySystem = default!;
public override string Command => "setbatterypercent"; public override string Command => "setbatterypercent";
@ -35,12 +37,15 @@ namespace Content.Server.Power
return; return;
} }
if (!EntityManager.TryGetComponent<BatteryComponent>(id, out var battery)) if (EntityManager.TryGetComponent<BatteryComponent>(id, out var battery))
_batterySystem.SetCharge((id.Value, battery), battery.MaxCharge * percent / 100);
else if (EntityManager.TryGetComponent<PredictedBatteryComponent>(id, out var pBattery))
_predictedBatterySystem.SetCharge((id.Value, pBattery), pBattery.MaxCharge * percent / 100);
else
{ {
shell.WriteLine(Loc.GetString($"cmd-setbatterypercent-battery-not-found", ("id", id))); shell.WriteLine(Loc.GetString($"cmd-setbatterypercent-battery-not-found", ("id", id)));
return; return;
} }
_batterySystem.SetCharge((id.Value, battery), battery.MaxCharge * percent / 100);
// Don't acknowledge b/c people WILL forall this // Don't acknowledge b/c people WILL forall this
} }
} }

View File

@ -1,72 +0,0 @@
using Content.Shared.Power;
using Content.Shared.PowerCell;
using Content.Shared.PowerCell.Components;
namespace Content.Server.PowerCell;
public sealed partial class PowerCellSystem
{
/*
* Handles PowerCellDraw
*/
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<PowerCellDrawComponent, PowerCellSlotComponent>();
while (query.MoveNext(out var uid, out var comp, out var slot))
{
if (!comp.Enabled)
continue;
if (Timing.CurTime < comp.NextUpdateTime)
continue;
comp.NextUpdateTime += comp.Delay;
if (!TryGetBatteryFromSlot(uid, out var batteryEnt, out var battery, slot))
continue;
if (_battery.TryUseCharge((batteryEnt.Value, battery), comp.DrawRate * (float)comp.Delay.TotalSeconds))
continue;
var ev = new PowerCellSlotEmptyEvent();
RaiseLocalEvent(uid, ref ev);
}
}
private void OnDrawChargeChanged(EntityUid uid, PowerCellDrawComponent component, ref ChargeChangedEvent args)
{
// Update the bools for client prediction.
var canUse = component.UseRate <= 0f || args.Charge > component.UseRate;
var canDraw = component.DrawRate <= 0f || args.Charge > 0f;
if (canUse != component.CanUse || canDraw != component.CanDraw)
{
component.CanDraw = canDraw;
component.CanUse = canUse;
Dirty(uid, component);
}
}
private void OnDrawCellChanged(EntityUid uid, PowerCellDrawComponent component, PowerCellChangedEvent args)
{
var canDraw = !args.Ejected && HasCharge(uid, float.MinValue);
var canUse = !args.Ejected && HasActivatableCharge(uid, component);
if (!canDraw)
{
var ev = new PowerCellSlotEmptyEvent();
RaiseLocalEvent(uid, ref ev);
}
if (canUse != component.CanUse || canDraw != component.CanDraw)
{
component.CanDraw = canDraw;
component.CanUse = canUse;
Dirty(uid, component);
}
}
}

View File

@ -1,258 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Kitchen.Components;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Examine;
using Content.Shared.Kitchen.Components; // DeltaV?
using Content.Shared.Popups;
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Content.Shared.PowerCell;
using Content.Shared.PowerCell.Components;
using Content.Shared.Rounding;
using Content.Shared.UserInterface;
using Robust.Shared.Containers;
namespace Content.Server.PowerCell;
/// <summary>
/// Handles Power cells
/// </summary>
public sealed partial class PowerCellSystem : SharedPowerCellSystem
{
[Dependency] private readonly ActivatableUISystem _activatable = default!;
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _sharedAppearanceSystem = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly RiggableSystem _riggableSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PowerCellComponent, ChargeChangedEvent>(OnChargeChanged);
SubscribeLocalEvent<PowerCellComponent, ExaminedEvent>(OnCellExamined);
SubscribeLocalEvent<PowerCellDrawComponent, ChargeChangedEvent>(OnDrawChargeChanged);
SubscribeLocalEvent<PowerCellDrawComponent, PowerCellChangedEvent>(OnDrawCellChanged);
SubscribeLocalEvent<PowerCellSlotComponent, ExaminedEvent>(OnCellSlotExamined);
// funny
SubscribeLocalEvent<PowerCellSlotComponent, BeingMicrowavedEvent>(OnSlotMicrowaved); // DeltaV?
SubscribeLocalEvent<PowerCellSlotComponent, GetChargeEvent>(OnGetCharge);
SubscribeLocalEvent<PowerCellSlotComponent, ChangeChargeEvent>(OnChangeCharge);
}
private void OnSlotMicrowaved(EntityUid uid, PowerCellSlotComponent component, BeingMicrowavedEvent args)
{
if (!_itemSlotsSystem.TryGetSlot(uid, component.CellSlotId, out var slot))
return;
if (slot.Item == null)
return;
RaiseLocalEvent(slot.Item.Value, args);
}
private void OnChargeChanged(EntityUid uid, PowerCellComponent component, ref ChargeChangedEvent args)
{
if (TryComp<RiggableComponent>(uid, out var rig) && rig.IsRigged)
{
_riggableSystem.Explode(uid, cause: null);
return;
}
var frac = args.Charge / args.MaxCharge;
var level = (byte)ContentHelpers.RoundToNearestLevels(frac, 1, PowerCellComponent.PowerCellVisualsLevels);
_sharedAppearanceSystem.SetData(uid, PowerCellVisuals.ChargeLevel, level);
// If this power cell is inside a cell-slot, inform that entity that the power has changed (for updating visuals n such).
if (_containerSystem.TryGetContainingContainer((uid, null, null), out var container)
&& TryComp(container.Owner, out PowerCellSlotComponent? slot)
&& _itemSlotsSystem.TryGetSlot(container.Owner, slot.CellSlotId, out var itemSlot))
{
if (itemSlot.Item == uid)
RaiseLocalEvent(container.Owner, new PowerCellChangedEvent(false));
}
}
protected override void OnCellRemoved(EntityUid uid, PowerCellSlotComponent component, EntRemovedFromContainerMessage args)
{
base.OnCellRemoved(uid, component, args);
if (args.Container.ID != component.CellSlotId)
return;
var ev = new PowerCellSlotEmptyEvent();
RaiseLocalEvent(uid, ref ev);
}
#region Activatable
/// <inheritdoc/>
public override bool HasActivatableCharge(EntityUid uid, PowerCellDrawComponent? battery = null, PowerCellSlotComponent? cell = null, EntityUid? user = null)
{
// Default to true if we don't have the components.
if (!Resolve(uid, ref battery, ref cell, false))
return true;
return HasCharge(uid, battery.UseRate, cell, user);
}
/// <summary>
/// Tries to use the <see cref="PowerCellDrawComponent.UseRate"/> for this entity.
/// </summary>
/// <param name="user">Popup to this user with the relevant detail if specified.</param>
public bool TryUseActivatableCharge(EntityUid uid, PowerCellDrawComponent? battery = null, PowerCellSlotComponent? cell = null, EntityUid? user = null)
{
// Default to true if we don't have the components.
if (!Resolve(uid, ref battery, ref cell, false))
return true;
if (TryUseCharge(uid, battery.UseRate, cell, user))
{
_sharedAppearanceSystem.SetData(uid, PowerCellSlotVisuals.Enabled, HasActivatableCharge(uid, battery, cell, user));
_activatable.CheckUsage(uid);
return true;
}
return false;
}
/// <inheritdoc/>
public override bool HasDrawCharge(
EntityUid uid,
PowerCellDrawComponent? battery = null,
PowerCellSlotComponent? cell = null,
EntityUid? user = null)
{
if (!Resolve(uid, ref battery, ref cell, false))
return true;
return HasCharge(uid, battery.DrawRate, cell, user);
}
#endregion
/// <summary>
/// Returns whether the entity has a slotted battery and charge for the requested action.
/// </summary>
/// <param name="user">Popup to this user with the relevant detail if specified.</param>
public bool HasCharge(EntityUid uid, float charge, PowerCellSlotComponent? component = null, EntityUid? user = null)
{
if (!TryGetBatteryFromSlot(uid, out var battery, component))
{
if (user != null)
_popup.PopupEntity(Loc.GetString("power-cell-no-battery"), uid, user.Value);
return false;
}
if (battery.CurrentCharge < charge)
{
if (user != null)
_popup.PopupEntity(Loc.GetString("power-cell-insufficient"), uid, user.Value);
return false;
}
return true;
}
/// <summary>
/// Tries to use charge from a slotted battery.
/// </summary>
public bool TryUseCharge(EntityUid uid, float charge, PowerCellSlotComponent? component = null, EntityUid? user = null)
{
if (!TryGetBatteryFromSlot(uid, out var batteryEnt, out var battery, component))
{
if (user != null)
_popup.PopupEntity(Loc.GetString("power-cell-no-battery"), uid, user.Value);
return false;
}
if (!_battery.TryUseCharge((batteryEnt.Value, battery), charge))
{
if (user != null)
_popup.PopupEntity(Loc.GetString("power-cell-insufficient"), uid, user.Value);
return false;
}
_sharedAppearanceSystem.SetData(uid, PowerCellSlotVisuals.Enabled, battery.CurrentCharge > 0);
return true;
}
public bool TryGetBatteryFromSlot(EntityUid uid, [NotNullWhen(true)] out BatteryComponent? battery, PowerCellSlotComponent? component = null)
{
return TryGetBatteryFromSlot(uid, out _, out battery, component);
}
public bool TryGetBatteryFromSlot(EntityUid uid,
[NotNullWhen(true)] out EntityUid? batteryEnt,
[NotNullWhen(true)] out BatteryComponent? battery,
PowerCellSlotComponent? component = null)
{
if (!Resolve(uid, ref component, false))
{
batteryEnt = null;
battery = null;
return false;
}
if (_itemSlotsSystem.TryGetSlot(uid, component.CellSlotId, out ItemSlot? slot))
{
batteryEnt = slot.Item;
return TryComp(slot.Item, out battery);
}
batteryEnt = null;
battery = null;
return false;
}
private void OnCellExamined(EntityUid uid, PowerCellComponent component, ExaminedEvent args)
{
TryComp<BatteryComponent>(uid, out var battery);
OnBatteryExamined(uid, battery, args);
}
private void OnCellSlotExamined(EntityUid uid, PowerCellSlotComponent component, ExaminedEvent args)
{
TryGetBatteryFromSlot(uid, out var batteryEnt, out var battery); // Goobstation
OnBatteryExamined(batteryEnt.GetValueOrDefault(uid), battery, args); // Goobstation
}
private void OnBatteryExamined(EntityUid uid, BatteryComponent? component, ExaminedEvent args)
{
if (component != null)
{
var charge = component.CurrentCharge / component.MaxCharge * 100;
args.PushMarkup(Loc.GetString("power-cell-component-examine-details", ("currentCharge", $"{charge:F0}")));
}
else
{
args.PushMarkup(Loc.GetString("power-cell-component-examine-details-no-battery"));
}
}
private void OnGetCharge(Entity<PowerCellSlotComponent> entity, ref GetChargeEvent args)
{
if (!TryGetBatteryFromSlot(entity, out var batteryUid, out _))
return;
RaiseLocalEvent(batteryUid.Value, ref args);
}
private void OnChangeCharge(Entity<PowerCellSlotComponent> entity, ref ChangeChargeEvent args)
{
if (!TryGetBatteryFromSlot(entity, out var batteryUid, out _))
return;
RaiseLocalEvent(batteryUid.Value, ref args);
}
}

View File

@ -1,8 +1,7 @@
using Content.Server.Power.EntitySystems;
using Content.Server.PowerCell;
using Content.Shared.DeviceNetwork.Components; using Content.Shared.DeviceNetwork.Components;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.PowerCell.Components; using Content.Shared.Power.EntitySystems;
using Content.Shared.PowerCell;
using Content.Shared.Radio.EntitySystems; using Content.Shared.Radio.EntitySystems;
using Content.Shared.Radio.Components; using Content.Shared.Radio.Components;
using Content.Shared.DeviceNetwork.Systems; using Content.Shared.DeviceNetwork.Systems;
@ -12,7 +11,7 @@ namespace Content.Server.Radio.EntitySystems;
public sealed class JammerSystem : SharedJammerSystem public sealed class JammerSystem : SharedJammerSystem
{ {
[Dependency] private readonly PowerCellSystem _powerCell = default!; [Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly BatterySystem _battery = default!; [Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedDeviceNetworkJammerSystem _jammer = default!; [Dependency] private readonly SharedDeviceNetworkJammerSystem _jammer = default!;
@ -25,6 +24,8 @@ public sealed class JammerSystem : SharedJammerSystem
SubscribeLocalEvent<RadioSendAttemptEvent>(OnRadioSendAttempt); SubscribeLocalEvent<RadioSendAttemptEvent>(OnRadioSendAttempt);
} }
// TODO: Very important: Make this charge rate based instead of updating every single tick
// See PredictedBatteryComponent
public override void Update(float frameTime) public override void Update(float frameTime)
{ {
var query = EntityQueryEnumerator<ActiveRadioJammerComponent, RadioJammerComponent>(); var query = EntityQueryEnumerator<ActiveRadioJammerComponent, RadioJammerComponent>();
@ -32,9 +33,9 @@ public sealed class JammerSystem : SharedJammerSystem
while (query.MoveNext(out var uid, out var _, out var jam)) while (query.MoveNext(out var uid, out var _, out var jam))
{ {
if (_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery)) if (_powerCell.TryGetBatteryFromSlot(uid, out var battery))
{ {
if (!_battery.TryUseCharge((batteryUid.Value, battery), GetCurrentWattage((uid, jam)) * frameTime)) if (!_battery.TryUseCharge(battery.Value.AsNullable(), GetCurrentWattage((uid, jam)) * frameTime))
{ {
ChangeLEDState(uid, false); ChangeLEDState(uid, false);
RemComp<ActiveRadioJammerComponent>(uid); RemComp<ActiveRadioJammerComponent>(uid);
@ -42,7 +43,7 @@ public sealed class JammerSystem : SharedJammerSystem
} }
else else
{ {
var percentCharged = battery.CurrentCharge / battery.MaxCharge; var percentCharged = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge;
var chargeLevel = percentCharged switch var chargeLevel = percentCharged switch
{ {
> 0.50f => RadioJammerChargeLevel.High, > 0.50f => RadioJammerChargeLevel.High,
@ -64,7 +65,7 @@ public sealed class JammerSystem : SharedJammerSystem
var activated = !HasComp<ActiveRadioJammerComponent>(ent) && var activated = !HasComp<ActiveRadioJammerComponent>(ent) &&
_powerCell.TryGetBatteryFromSlot(ent.Owner, out var battery) && _powerCell.TryGetBatteryFromSlot(ent.Owner, out var battery) &&
battery.CurrentCharge > GetCurrentWattage(ent); _battery.GetCharge(battery.Value.AsNullable()) > GetCurrentWattage(ent);
if (activated) if (activated)
{ {
ChangeLEDState(ent.Owner, true); ChangeLEDState(ent.Owner, true);

View File

@ -27,10 +27,8 @@ public sealed partial class BorgSystem
SubscribeLocalEvent<BorgTransponderComponent, DeviceNetworkPacketEvent>(OnPacketReceived); SubscribeLocalEvent<BorgTransponderComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
} }
public override void Update(float frameTime) public void UpdateTransponder(float frameTime)
{ {
base.Update(frameTime);
var now = _timing.CurTime; var now = _timing.CurTime;
var query = EntityQueryEnumerator<BorgTransponderComponent, BorgChassisComponent, DeviceNetworkComponent, MetaDataComponent>(); var query = EntityQueryEnumerator<BorgTransponderComponent, BorgChassisComponent, DeviceNetworkComponent, MetaDataComponent>();
while (query.MoveNext(out var uid, out var comp, out var chassis, out var device, out var meta)) while (query.MoveNext(out var uid, out var comp, out var chassis, out var device, out var meta))
@ -43,7 +41,7 @@ public sealed partial class BorgSystem
var charge = 0f; var charge = 0f;
if (_powerCell.TryGetBatteryFromSlot(uid, out var battery)) if (_powerCell.TryGetBatteryFromSlot(uid, out var battery))
charge = battery.CurrentCharge / battery.MaxCharge; charge = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge;
var hpPercent = CalcHP(uid); var hpPercent = CalcHP(uid);

View File

@ -91,6 +91,43 @@ public sealed partial class BorgSystem
UpdateUI(uid, component); UpdateUI(uid, component);
} }
public void UpdateBattery(Entity<BorgChassisComponent> ent)
{
UpdateBatteryAlert(ent);
// if we aren't drawing and suddenly get enough power to draw again, reeanble.
if (_powerCell.HasDrawCharge(ent.Owner))
{
Toggle.TryActivate(ent.Owner);
}
UpdateUI(ent, ent);
}
// TODO: Move to client so we don't have to network this periodically.
private void UpdateBatteryAlert(Entity<BorgChassisComponent> ent, PowerCellSlotComponent? slotComponent = null)
{
if (!_powerCell.TryGetBatteryFromSlot((ent.Owner, slotComponent), out var battery))
{
_alerts.ClearAlert(ent.Owner, ent.Comp.BatteryAlert);
_alerts.ShowAlert(ent.Owner, ent.Comp.NoBatteryAlert);
return;
}
var chargePercent = (short)MathF.Round(_battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge * 10f);
// we make sure 0 only shows if they have absolutely no battery.
// also account for floating point imprecision
if (chargePercent == 0 && _powerCell.HasDrawCharge((ent.Owner, null, slotComponent)))
{
chargePercent = 1;
}
_alerts.ClearAlert(ent.Owner, ent.Comp.NoBatteryAlert);
_alerts.ShowAlert(ent.Owner, ent.Comp.BatteryAlert, chargePercent);
}
// TODO: Component states and update this on the client
public void UpdateUI(EntityUid uid, BorgChassisComponent? component = null) public void UpdateUI(EntityUid uid, BorgChassisComponent? component = null)
{ {
if (!Resolve(uid, ref component)) if (!Resolve(uid, ref component))
@ -101,10 +138,26 @@ public sealed partial class BorgSystem
if (_powerCell.TryGetBatteryFromSlot(uid, out var battery)) if (_powerCell.TryGetBatteryFromSlot(uid, out var battery))
{ {
hasBattery = true; hasBattery = true;
chargePercent = battery.CurrentCharge / battery.MaxCharge; chargePercent = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge;
} }
var state = new BorgBuiState(chargePercent, hasBattery); var state = new BorgBuiState(chargePercent, hasBattery);
_ui.SetUiState(uid, BorgUiKey.Key, state); _ui.SetUiState(uid, BorgUiKey.Key, state);
} }
// periodically update the charge indicator
// TODO: Move this to the client.
public void UpdateBattery(float frameTime)
{
var curTime = _timing.CurTime;
var query = EntityQueryEnumerator<BorgChassisComponent>();
while (query.MoveNext(out var uid, out var borgChassis))
{
if (curTime < borgChassis.NextBatteryUpdate)
continue;
UpdateBattery((uid, borgChassis));
borgChassis.NextBatteryUpdate = curTime + TimeSpan.FromSeconds(1);
}
}
} }

View File

@ -5,7 +5,6 @@ using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers; using Content.Server.Administration.Managers;
using Content.Server.DeviceNetwork.Systems; using Content.Server.DeviceNetwork.Systems;
using Content.Server.Hands.Systems; using Content.Server.Hands.Systems;
using Content.Server.PowerCell;
using Content.Shared.Alert; using Content.Shared.Alert;
using Content.Shared.Body.Events; using Content.Shared.Body.Events;
using Content.Shared.Database; using Content.Shared.Database;
@ -18,6 +17,8 @@ using Content.Shared.Mobs;
using Content.Shared.Mobs.Systems; using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Systems; using Content.Shared.Movement.Systems;
using Content.Shared.Pointing; using Content.Shared.Pointing;
using Content.Shared.Power;
using Content.Shared.Power.EntitySystems;
using Content.Shared.PowerCell; using Content.Shared.PowerCell;
using Content.Shared.PowerCell.Components; using Content.Shared.PowerCell.Components;
using Content.Shared.Roles; using Content.Shared.Roles;
@ -62,6 +63,7 @@ public sealed partial class BorgSystem : SharedBorgSystem
[Dependency] private readonly SharedInteractionSystem _interaction = default!; // DeltaV [Dependency] private readonly SharedInteractionSystem _interaction = default!; // DeltaV
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly ISharedPlayerManager _player = default!; [Dependency] private readonly ISharedPlayerManager _player = default!;
[Dependency] private readonly PredictedBatterySystem _battery = default!;
public static readonly ProtoId<JobPrototype> BorgJobId = "Borg"; public static readonly ProtoId<JobPrototype> BorgJobId = "Borg";
@ -77,6 +79,7 @@ public sealed partial class BorgSystem : SharedBorgSystem
SubscribeLocalEvent<BorgChassisComponent, MobStateChangedEvent>(OnMobStateChanged); SubscribeLocalEvent<BorgChassisComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<BorgChassisComponent, BeingGibbedEvent>(OnBeingGibbed); SubscribeLocalEvent<BorgChassisComponent, BeingGibbedEvent>(OnBeingGibbed);
SubscribeLocalEvent<BorgChassisComponent, PowerCellChangedEvent>(OnPowerCellChanged); SubscribeLocalEvent<BorgChassisComponent, PowerCellChangedEvent>(OnPowerCellChanged);
SubscribeLocalEvent<BorgChassisComponent, PredictedBatteryChargeChangedEvent>(OnBatteryChargeChanged);
SubscribeLocalEvent<BorgChassisComponent, PowerCellSlotEmptyEvent>(OnPowerCellSlotEmpty); SubscribeLocalEvent<BorgChassisComponent, PowerCellSlotEmptyEvent>(OnPowerCellSlotEmpty);
SubscribeLocalEvent<BorgChassisComponent, GetCharactedDeadIcEvent>(OnGetDeadIC); SubscribeLocalEvent<BorgChassisComponent, GetCharactedDeadIcEvent>(OnGetDeadIC);
SubscribeLocalEvent<BorgChassisComponent, GetCharacterUnrevivableIcEvent>(OnGetUnrevivableIC); SubscribeLocalEvent<BorgChassisComponent, GetCharacterUnrevivableIcEvent>(OnGetUnrevivableIC);
@ -210,17 +213,14 @@ public sealed partial class BorgSystem : SharedBorgSystem
_container.EmptyContainer(component.ModuleContainer); _container.EmptyContainer(component.ModuleContainer);
} }
private void OnPowerCellChanged(EntityUid uid, BorgChassisComponent component, PowerCellChangedEvent args) private void OnPowerCellChanged(Entity<BorgChassisComponent> ent, ref PowerCellChangedEvent args)
{ {
UpdateBatteryAlert((uid, component)); UpdateBattery(ent);
// if we aren't drawing and suddenly get enough power to draw again, reeanble.
if (_powerCell.HasDrawCharge(uid))
{
Toggle.TryActivate(uid);
} }
UpdateUI(uid, component); private void OnBatteryChargeChanged(Entity<BorgChassisComponent> ent, ref PredictedBatteryChargeChangedEvent args)
{
UpdateBattery(ent);
} }
private void OnPowerCellSlotEmpty(EntityUid uid, BorgChassisComponent component, ref PowerCellSlotEmptyEvent args) private void OnPowerCellSlotEmpty(EntityUid uid, BorgChassisComponent component, ref PowerCellSlotEmptyEvent args)
@ -287,28 +287,6 @@ public sealed partial class BorgSystem : SharedBorgSystem
args.Cancel(); args.Cancel();
} }
private void UpdateBatteryAlert(Entity<BorgChassisComponent> ent, PowerCellSlotComponent? slotComponent = null)
{
if (!_powerCell.TryGetBatteryFromSlot(ent, out var battery, slotComponent))
{
_alerts.ClearAlert(ent.Owner, ent.Comp.BatteryAlert);
_alerts.ShowAlert(ent.Owner, ent.Comp.NoBatteryAlert);
return;
}
var chargePercent = (short) MathF.Round(battery.CurrentCharge / battery.MaxCharge * 10f);
// we make sure 0 only shows if they have absolutely no battery.
// also account for floating point imprecision
if (chargePercent == 0 && _powerCell.HasDrawCharge(ent, cell: slotComponent))
{
chargePercent = 1;
}
_alerts.ClearAlert(ent.Owner, ent.Comp.NoBatteryAlert);
_alerts.ShowAlert(ent.Owner, ent.Comp.BatteryAlert, chargePercent);
}
public bool TryEjectPowerCell(EntityUid uid, BorgChassisComponent component, [NotNullWhen(true)] out List<EntityUid>? ents) public bool TryEjectPowerCell(EntityUid uid, BorgChassisComponent component, [NotNullWhen(true)] out List<EntityUid>? ents)
{ {
ents = null; ents = null;
@ -358,4 +336,12 @@ public sealed partial class BorgSystem : SharedBorgSystem
return true; return true;
} }
public override void Update(float frameTime)
{
base.Update(frameTime);
UpdateTransponder(frameTime);
UpdateBattery(frameTime);
}
} }

View File

@ -1,9 +1,10 @@
using System.Text; using System.Text;
using Content.Server.Destructible; using Content.Server.Destructible;
using Content.Server.PowerCell;
using Content.Shared.Speech.Components; using Content.Shared.Speech.Components;
using Content.Shared.Damage.Components; using Content.Shared.Damage.Components;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using Content.Shared.Power.EntitySystems;
using Content.Shared.PowerCell;
using Content.Shared.Speech; using Content.Shared.Speech;
using Robust.Shared.Random; using Robust.Shared.Random;
@ -12,6 +13,7 @@ namespace Content.Server.Speech.EntitySystems;
public sealed class DamagedSiliconAccentSystem : EntitySystem public sealed class DamagedSiliconAccentSystem : EntitySystem
{ {
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!; [Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly DestructibleSystem _destructibleSystem = default!; [Dependency] private readonly DestructibleSystem _destructibleSystem = default!;
@ -34,7 +36,7 @@ public sealed class DamagedSiliconAccentSystem : EntitySystem
} }
else if (_powerCell.TryGetBatteryFromSlot(uid, out var battery)) else if (_powerCell.TryGetBatteryFromSlot(uid, out var battery))
{ {
currentChargeLevel = battery.CurrentCharge / battery.MaxCharge; currentChargeLevel = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge;
} }
currentChargeLevel = Math.Clamp(currentChargeLevel, 0.0f, 1.0f); currentChargeLevel = Math.Clamp(currentChargeLevel, 0.0f, 1.0f);
// Corrupt due to low power (drops characters on longer messages) // Corrupt due to low power (drops characters on longer messages)

View File

@ -1,6 +1,6 @@
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Power.Events; using Content.Server.Power.Events;
using Content.Server.Power.EntitySystems;
using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Damage.Events; using Content.Shared.Damage.Events;
using Content.Shared.Examine; using Content.Shared.Examine;
@ -9,6 +9,7 @@ using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Power; using Content.Shared.Power;
using Content.Shared.Power.Components; using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Stunnable; using Content.Shared.Stunnable;
namespace Content.Server.Stunnable.Systems namespace Content.Server.Stunnable.Systems
@ -17,7 +18,7 @@ namespace Content.Server.Stunnable.Systems
{ {
[Dependency] private readonly RiggableSystem _riggableSystem = default!; [Dependency] private readonly RiggableSystem _riggableSystem = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly BatterySystem _battery = default!; [Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly ItemToggleSystem _itemToggle = default!; [Dependency] private readonly ItemToggleSystem _itemToggle = default!;
public override void Initialize() public override void Initialize()
@ -27,13 +28,13 @@ namespace Content.Server.Stunnable.Systems
SubscribeLocalEvent<StunbatonComponent, ExaminedEvent>(OnExamined); SubscribeLocalEvent<StunbatonComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<StunbatonComponent, SolutionContainerChangedEvent>(OnSolutionChange); SubscribeLocalEvent<StunbatonComponent, SolutionContainerChangedEvent>(OnSolutionChange);
SubscribeLocalEvent<StunbatonComponent, StaminaDamageOnHitAttemptEvent>(OnStaminaHitAttempt); SubscribeLocalEvent<StunbatonComponent, StaminaDamageOnHitAttemptEvent>(OnStaminaHitAttempt);
SubscribeLocalEvent<StunbatonComponent, ChargeChangedEvent>(OnChargeChanged); SubscribeLocalEvent<StunbatonComponent, PredictedBatteryChargeChangedEvent>(OnChargeChanged);
} }
private void OnStaminaHitAttempt(Entity<StunbatonComponent> entity, ref StaminaDamageOnHitAttemptEvent args) private void OnStaminaHitAttempt(Entity<StunbatonComponent> entity, ref StaminaDamageOnHitAttemptEvent args)
{ {
if (!_itemToggle.IsActivated(entity.Owner) || if (!_itemToggle.IsActivated(entity.Owner) ||
!TryComp<BatteryComponent>(entity.Owner, out var battery) || !_battery.TryUseCharge((entity.Owner, battery), entity.Comp.EnergyPerUse)) !TryComp<PredictedBatteryComponent>(entity.Owner, out var battery) || !_battery.TryUseCharge((entity.Owner, battery), entity.Comp.EnergyPerUse))
{ {
args.Cancelled = true; args.Cancelled = true;
} }
@ -46,9 +47,9 @@ namespace Content.Server.Stunnable.Systems
: Loc.GetString("comp-stunbaton-examined-off"); : Loc.GetString("comp-stunbaton-examined-off");
args.PushMarkup(onMsg); args.PushMarkup(onMsg);
if (TryComp<BatteryComponent>(entity.Owner, out var battery)) if (TryComp<PredictedBatteryComponent>(entity.Owner, out var battery))
{ {
var count = (int) (battery.CurrentCharge / entity.Comp.EnergyPerUse); var count = _battery.GetRemainingUses((entity.Owner, battery), entity.Comp.EnergyPerUse);
args.PushMarkup(Loc.GetString("melee-battery-examine", ("color", "yellow"), ("count", count))); args.PushMarkup(Loc.GetString("melee-battery-examine", ("color", "yellow"), ("count", count)));
} }
} }
@ -57,7 +58,7 @@ namespace Content.Server.Stunnable.Systems
{ {
base.TryTurnOn(entity, ref args); base.TryTurnOn(entity, ref args);
if (!TryComp<BatteryComponent>(entity, out var battery) || battery.CurrentCharge < entity.Comp.EnergyPerUse) if (!TryComp<PredictedBatteryComponent>(entity, out var battery) || _battery.GetCharge((entity, battery)) < entity.Comp.EnergyPerUse)
{ {
args.Cancelled = true; args.Cancelled = true;
if (args.User != null) if (args.User != null)
@ -69,7 +70,7 @@ namespace Content.Server.Stunnable.Systems
if (TryComp<RiggableComponent>(entity, out var rig) && rig.IsRigged) if (TryComp<RiggableComponent>(entity, out var rig) && rig.IsRigged)
{ {
_riggableSystem.Explode(entity.Owner, battery, args.User); _riggableSystem.Explode(entity.Owner, _battery.GetCharge((entity, battery)), args.User);
} }
} }
@ -78,13 +79,14 @@ namespace Content.Server.Stunnable.Systems
{ {
// Explode if baton is activated and rigged. // Explode if baton is activated and rigged.
if (!TryComp<RiggableComponent>(entity, out var riggable) || if (!TryComp<RiggableComponent>(entity, out var riggable) ||
!TryComp<BatteryComponent>(entity, out var battery)) !TryComp<PredictedBatteryComponent>(entity, out var battery))
return; return;
if (_itemToggle.IsActivated(entity.Owner) && riggable.IsRigged) if (_itemToggle.IsActivated(entity.Owner) && riggable.IsRigged)
_riggableSystem.Explode(entity.Owner, battery); _riggableSystem.Explode(entity.Owner, _battery.GetCharge((entity, battery)));
} }
// TODO: Not used anywhere?
private void SendPowerPulse(EntityUid target, EntityUid? user, EntityUid used) private void SendPowerPulse(EntityUid target, EntityUid? user, EntityUid used)
{ {
RaiseLocalEvent(target, new PowerPulseEvent() RaiseLocalEvent(target, new PowerPulseEvent()
@ -94,10 +96,10 @@ namespace Content.Server.Stunnable.Systems
}); });
} }
private void OnChargeChanged(Entity<StunbatonComponent> entity, ref ChargeChangedEvent args) private void OnChargeChanged(Entity<StunbatonComponent> entity, ref PredictedBatteryChargeChangedEvent args)
{ {
if (TryComp<BatteryComponent>(entity.Owner, out var battery) && if (TryComp<PredictedBatteryComponent>(entity.Owner, out var battery) &&
battery.CurrentCharge < entity.Comp.EnergyPerUse) _battery.GetCharge((entity.Owner, battery)) < entity.Comp.EnergyPerUse)
{ {
_itemToggle.TryDeactivate(entity.Owner, predicted: false); _itemToggle.TryDeactivate(entity.Owner, predicted: false);
} }

View File

@ -1,4 +1,3 @@
using Content.Server.PowerCell;
using Content.Shared.Item.ItemToggle; using Content.Shared.Item.ItemToggle;
using Content.Shared.PowerCell; using Content.Shared.PowerCell;
using Content.Shared.Weapons.Misc; using Content.Shared.Weapons.Misc;

View File

@ -1,75 +0,0 @@
using Content.Shared.Power;
using Content.Shared.PowerCell.Components;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
namespace Content.Server.Weapons.Ranged.Systems;
public sealed partial class GunSystem
{
protected override void InitializeBattery()
{
base.InitializeBattery();
// Hitscan
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, ComponentStartup>(OnBatteryStartup);
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, ChargeChangedEvent>(OnBatteryChargeChange);
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, PowerCellChangedEvent>(OnPowerCellChanged);
// Projectile
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, ComponentStartup>(OnBatteryStartup);
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, ChargeChangedEvent>(OnBatteryChargeChange);
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, PowerCellChangedEvent>(OnPowerCellChanged);
}
private void OnBatteryStartup<T>(Entity<T> entity, ref ComponentStartup args) where T : BatteryAmmoProviderComponent
{
UpdateShots(entity, entity.Comp);
}
private void OnBatteryChargeChange<T>(Entity<T> entity, ref ChargeChangedEvent args) where T : BatteryAmmoProviderComponent
{
UpdateShots(entity, entity.Comp, args.Charge, args.MaxCharge);
}
private void OnPowerCellChanged<T>(Entity<T> entity, ref PowerCellChangedEvent args) where T : BatteryAmmoProviderComponent
{
UpdateShots(entity, entity.Comp);
}
private void UpdateShots(EntityUid uid, BatteryAmmoProviderComponent component)
{
var ev = new GetChargeEvent();
RaiseLocalEvent(uid, ref ev);
UpdateShots(uid, component, ev.CurrentCharge, ev.MaxCharge);
}
private void UpdateShots(EntityUid uid, BatteryAmmoProviderComponent component, float charge, float maxCharge)
{
var shots = (int) (charge / component.FireCost);
var maxShots = (int) (maxCharge / component.FireCost);
if (component.Shots != shots || component.Capacity != maxShots)
{
Dirty(uid, component);
}
component.Shots = shots;
if (maxShots > 0)
component.Capacity = maxShots;
UpdateBatteryAppearance(uid, component);
var updateAmmoEv = new UpdateClientAmmoEvent();
RaiseLocalEvent(uid, ref updateAmmoEv);
}
protected override void TakeCharge(Entity<BatteryAmmoProviderComponent> entity)
{
// Take charge from either the BatteryComponent or PowerCellSlotComponent.
var ev = new ChangeChargeEvent(-entity.Comp.FireCost);
RaiseLocalEvent(entity, ref ev);
}
}

View File

@ -1,6 +1,7 @@
using Content.Server.Power.EntitySystems; using Content.Server.Power.EntitySystems;
using Content.Server.Xenoarchaeology.Artifact.XAE.Components; using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
using Content.Shared.Power.Components; using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Xenoarchaeology.Artifact; using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.XAE; using Content.Shared.Xenoarchaeology.Artifact.XAE;
@ -12,20 +13,29 @@ namespace Content.Server.Xenoarchaeology.Artifact.XAE;
public sealed class XAEChargeBatterySystem : BaseXAESystem<XAEChargeBatteryComponent> public sealed class XAEChargeBatterySystem : BaseXAESystem<XAEChargeBatteryComponent>
{ {
[Dependency] private readonly BatterySystem _battery = default!; [Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly PredictedBatterySystem _predictedBattery = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!;
/// <summary> Pre-allocated and re-used collection.</summary> /// <summary> Pre-allocated and re-used collection.</summary>
private readonly HashSet<Entity<BatteryComponent>> _batteryEntities = new(); private readonly HashSet<Entity<BatteryComponent>> _batteryEntities = new();
private readonly HashSet<Entity<PredictedBatteryComponent>> _pBatteryEntities = new();
/// <inheritdoc /> /// <inheritdoc />
protected override void OnActivated(Entity<XAEChargeBatteryComponent> ent, ref XenoArtifactNodeActivatedEvent args) protected override void OnActivated(Entity<XAEChargeBatteryComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{ {
var chargeBatteryComponent = ent.Comp;
_batteryEntities.Clear(); _batteryEntities.Clear();
_lookup.GetEntitiesInRange(args.Coordinates, chargeBatteryComponent.Radius, _batteryEntities); _pBatteryEntities.Clear();
_lookup.GetEntitiesInRange(args.Coordinates, ent.Comp.Radius, _batteryEntities);
foreach (var battery in _batteryEntities) foreach (var battery in _batteryEntities)
{ {
_battery.SetCharge(battery.AsNullable(), battery.Comp.MaxCharge); _battery.SetCharge(battery.AsNullable(), battery.Comp.MaxCharge);
} }
_lookup.GetEntitiesInRange(args.Coordinates, ent.Comp.Radius, _pBatteryEntities);
foreach (var pBattery in _pBatteryEntities)
{
_predictedBattery.SetCharge(pBattery.AsNullable(), pBattery.Comp.MaxCharge);
}
} }
} }

View File

@ -0,0 +1,10 @@
namespace Content.Shared.Kitchen;
/// <summary>
/// Raised on an entity when it is inside a microwave and it starts cooking.
/// </summary>
public sealed class BeingMicrowavedEvent(EntityUid microwave, EntityUid? user) : HandledEntityEventArgs
{
public EntityUid Microwave = microwave;
public EntityUid? User = user;
}

View File

@ -111,18 +111,4 @@ namespace Content.Shared.Kitchen.Components
[DataField, ViewVariables(VVAccess.ReadWrite)] [DataField, ViewVariables(VVAccess.ReadWrite)]
public bool CanMicrowaveIdsSafely = true; public bool CanMicrowaveIdsSafely = true;
} }
public sealed class BeingMicrowavedEvent : HandledEntityEventArgs
{
public EntityUid Microwave;
public EntityUid? User;
public uint Time;
public BeingMicrowavedEvent(EntityUid microwave, EntityUid? user, uint time)
{
Microwave = microwave;
User = user;
Time = time;
}
}
} }

View File

@ -307,6 +307,7 @@ public abstract partial class SharedMechSystem : EntitySystem
/// <summary> /// <summary>
/// Attempts to change the amount of energy in the mech. /// Attempts to change the amount of energy in the mech.
/// TODO: Power cells are predicted now, so no need to duplicate the charge level
/// </summary> /// </summary>
/// <param name="uid">The mech itself</param> /// <param name="uid">The mech itself</param>
/// <param name="delta">The change in energy</param> /// <param name="delta">The change in energy</param>

View File

@ -5,14 +5,44 @@ namespace Content.Shared.Power;
/// <summary> /// <summary>
/// Raised when a battery's charge or capacity changes (capacity affects relative charge percentage). /// Raised when a battery's charge or capacity changes (capacity affects relative charge percentage).
/// Only raised for entities with <see cref="BatteryComponent"/>.
/// </summary> /// </summary>
[ByRefEvent] [ByRefEvent]
public readonly record struct ChargeChangedEvent(float Charge, float MaxCharge); public readonly record struct ChargeChangedEvent(float Charge, float MaxCharge);
/// <summary>
/// Raised when a predicted battery's charge or capacity changes (capacity affects relative charge percentage).
/// Unlike <see cref="ChargeChangedEvent"/> this is not raised repeatedly each time the charge changes, but only when the charge rate is changed
/// or a charge amount was added or removed instantaneously. The current charge can be inferred from the time of the last update and the charge and
/// charge rate at that time.
/// Only raised for entities with <see cref="PredictedBatteryComponent"/>.
/// </summary>
[ByRefEvent]
public readonly record struct PredictedBatteryChargeChangedEvent(float CurrentCharge, float CurrentChargeRate, TimeSpan CurrentTime, float MaxCharge);
/// <summary>
/// Raised when a battery changes its state between full, empty, or neither.
/// Used only for <see cref="PredictedBatteryComponent"/>.
/// </summary>
[ByRefEvent]
public record struct PredictedBatteryStateChangedEvent(BatteryState OldState, BatteryState NewState);
/// <summary>
/// Raised to calculate a predicted battery's recharge rate.
/// Subscribe to this to offset its current charge rate.
/// Used only for <see cref="PredictedBatteryComponent"/>.
/// </summary>
[ByRefEvent]
public record struct RefreshChargeRateEvent(float MaxCharge)
{
public readonly float MaxCharge = MaxCharge;
public float NewChargeRate;
}
/// <summary> /// <summary>
/// Event that supports multiple battery types. /// Event that supports multiple battery types.
/// Raised when it is necessary to get information about battery charges. /// Raised when it is necessary to get information about battery charges.
/// Works with either <see cref="BatteryComponent"/> or <see cref="PowerCellSlotComponent"/>. /// Works with either <see cref="BatteryComponent"/>, <see cref="PredictedBatteryComponent"/>, or <see cref="PowerCellSlotComponent"/>.
/// If there are multiple batteries then the results will be summed up. /// If there are multiple batteries then the results will be summed up.
/// </summary> /// </summary>
[ByRefEvent] [ByRefEvent]
@ -25,7 +55,7 @@ public record struct GetChargeEvent
/// <summary> /// <summary>
/// Method event that supports multiple battery types. /// Method event that supports multiple battery types.
/// Raised when it is necessary to change the current battery charge by some value. /// Raised when it is necessary to change the current battery charge by some value.
/// Works with either <see cref="BatteryComponent"/> or <see cref="PowerCellSlotComponent"/>. /// Works with either <see cref="BatteryComponent"/>, <see cref="PredictedBatteryComponent"/>, or <see cref="PowerCellSlotComponent"/>.
/// If there are multiple batteries then they will be changed in order of subscription until the total value was reached. /// If there are multiple batteries then they will be changed in order of subscription until the total value was reached.
/// </summary> /// </summary>
[ByRefEvent] [ByRefEvent]

View File

@ -5,6 +5,8 @@ namespace Content.Shared.Power.Components;
/// <summary> /// <summary>
/// Battery node on the pow3r network. Needs other components to connect to actual networks. /// Battery node on the pow3r network. Needs other components to connect to actual networks.
/// Use this for batteries that cannot be predicted.
/// Use <see cref="PredictedBatteryComponent"/> otherwise.
/// </summary> /// </summary>
[RegisterComponent] [RegisterComponent]
[Virtual] [Virtual]

View File

@ -5,6 +5,7 @@ namespace Content.Shared.Power.Components;
/// <summary> /// <summary>
/// Self-recharging battery. /// Self-recharging battery.
/// To be used in combination with <see cref="BatteryComponent"/>. /// To be used in combination with <see cref="BatteryComponent"/>.
/// For <see cref="PredictedBatteryComponent"/> use <see cref="PredictedBatterySelfRechargerComponent"/> instead.
/// </summary> /// </summary>
[RegisterComponent, AutoGenerateComponentPause] [RegisterComponent, AutoGenerateComponentPause]
public sealed partial class BatterySelfRechargerComponent : Component public sealed partial class BatterySelfRechargerComponent : Component
@ -16,7 +17,7 @@ public sealed partial class BatterySelfRechargerComponent : Component
public bool AutoRecharge = true; public bool AutoRecharge = true;
/// <summary> /// <summary>
/// At what rate does the entity automatically recharge? /// At what rate does the entity automatically recharge? In watts.
/// </summary> /// </summary>
[DataField] [DataField]
public float AutoRechargeRate; public float AutoRechargeRate;

View File

@ -1,19 +1,24 @@
using Content.Shared.Whitelist; using Content.Shared.Whitelist;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Power.Components; namespace Content.Shared.Power.Components;
[RegisterComponent, NetworkedComponent] [RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ChargerComponent : Component public sealed partial class ChargerComponent : Component
{ {
[ViewVariables] /// <summary>
public CellChargerStatus Status; /// The charge rate of the charger, in watts.
/// </summary>
[DataField, AutoNetworkedField]
public float ChargeRate = 20.0f;
/// <summary> /// <summary>
/// The charge rate of the charger, in watts /// Passive draw when no power cell is inserted, in watts.
/// This should be larger than 0 or the charger will be considered as powered even without a LV supply.
/// </summary> /// </summary>
[DataField] [DataField, AutoNetworkedField]
public float ChargeRate = 20.0f; public float PassiveDraw = 1f;
/// <summary> /// <summary>
/// The container ID that is holds the entities being charged. /// The container ID that is holds the entities being charged.
@ -24,13 +29,29 @@ public sealed partial class ChargerComponent : Component
/// <summary> /// <summary>
/// A whitelist for what entities can be charged by this Charger. /// A whitelist for what entities can be charged by this Charger.
/// </summary> /// </summary>
[DataField] [DataField, AutoNetworkedField]
public EntityWhitelist? Whitelist; public EntityWhitelist? Whitelist;
/// <summary> /// <summary>
/// Indicates whether the charger is portable and thus subject to EMP effects /// Indicates whether the charger is portable and thus subject to EMP effects
/// and bypasses checks for transform, anchored, and ApcPowerReceiverComponent. /// and bypasses checks for transform, anchored, and ApcPowerReceiverComponent.
/// </summary> /// </summary>
[DataField] [DataField, AutoNetworkedField]
public bool Portable = false; public bool Portable = false;
} }
[Serializable, NetSerializable]
public enum CellChargerStatus
{
Off,
Empty,
Charging,
Charged,
}
[Serializable, NetSerializable]
public enum CellVisual
{
Occupied, // If there's an item in it
Light,
}

View File

@ -0,0 +1,10 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Power.Components;
/// <summary>
/// Allows the charge of a battery to be seen by examination.
/// Works with either <see cref="BatteryComponent"/> or <see cref="PredictedBatteryComponent"/>.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ExaminableBatteryComponent : Component;

View File

@ -0,0 +1,10 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Power.Components;
/// <summary>
/// This entity is currently inside the charging slot of an entity with <see cref="ChargerComponent"/>.
/// Added regardless whether or not the charger is powered.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class InsideChargerComponent : Component;

View File

@ -0,0 +1,94 @@
using Content.Shared.Power.EntitySystems;
using Content.Shared.Guidebook;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Power.Components;
/// <summary>
/// Predicted equivalent to <see cref="BatteryComponent"/>.
/// Use this for electrical power storages that only have a constant charge rate or instantaneous power draw.
/// Devices being directly charged by the power network do not fulfill that requirement as their power supply ramps up over time.
/// </summary>
/// <remarks>
/// We cannot simply network <see cref="BatteryComponent"/> since it would get dirtied every single tick when it updates.
/// This component solves this by requiring a constant charge rate and having the client infer the current charge from the rate
/// and the timestamp the charge was last networked at. This can possibly be expanded in the future by adding a second time derivative.
/// </remarks>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
[Access(typeof(PredictedBatterySystem))]
public sealed partial class PredictedBatteryComponent : Component
{
/// <summary>
/// Maximum charge of the battery in joules (ie. watt seconds)
/// </summary>
[DataField, AutoNetworkedField, ViewVariables]
[GuidebookData]
public float MaxCharge;
/// <summary>
/// The price per one joule. Default is 1 speso for 10kJ.
/// </summary>
[DataField]
public float PricePerJoule = 0.0001f;
/// <summary>
/// Time stamp of the last networked update.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoNetworkedField, AutoPausedField, ViewVariables]
public TimeSpan LastUpdate = TimeSpan.Zero;
/// <summary>
/// The intial charge to be set on map init.
/// </summary>
[DataField]
public float StartingCharge;
/// <summary>
/// The charge at the last update in joules (i.e. watt seconds).
/// </summary>
[DataField, AutoNetworkedField, ViewVariables]
public float LastCharge;
/// <summary>
/// The current charge rate in watt.
/// </summary>
/// <remarks>
/// Not a datafield as this is only cached and recalculated on component startup.
/// </remarks>
[ViewVariables, AutoNetworkedField]
public float ChargeRate;
/// <summary>
/// The current charge state of the battery.
/// Used to track state changes for raising <see cref="PredictedBatteryStateChangedEvent"/>.
/// </summary>
/// <remarks>
/// Not a datafield as this is only cached and recalculated in an update loop.
/// </remarks>
[ViewVariables, AutoNetworkedField]
public BatteryState State = BatteryState.Neither;
}
/// <summary>
/// Charge level status of the battery.
/// </summary>
[Serializable, NetSerializable]
public enum BatteryState : byte
{
/// <summary>
/// Full charge.
/// </summary>
Full,
/// <summary>
/// No charge.
/// </summary>
Empty,
/// <summary>
/// Neither full nor empty.
/// </summary>
Neither,
}

View File

@ -0,0 +1,33 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Power.Components;
/// <summary>
/// Self-recharging battery.
/// To be used in combination with <see cref="PredictedBatteryComponent"/>.
/// For <see cref="BatteryComponent"/> use <see cref="BatterySelfRechargerComponent"/> instead.
/// </summary>
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState, AutoGenerateComponentPause]
public sealed partial class PredictedBatterySelfRechargerComponent : Component
{
/// <summary>
/// At what rate does the entity automatically recharge? In watts.
/// </summary>
[DataField, AutoNetworkedField, ViewVariables]
public float AutoRechargeRate;
/// <summary>
/// How long should the entity stop automatically recharging if a charge is used?
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan AutoRechargePauseTime = TimeSpan.Zero;
/// <summary>
/// Do not auto recharge if this timestamp has yet to happen, set for the auto recharge pause system.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoNetworkedField, AutoPausedField, ViewVariables]
public TimeSpan? NextAutoRecharge = TimeSpan.FromSeconds(0);
}

View File

@ -0,0 +1,51 @@
using Content.Shared.PowerCell.Components;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Power.Components;
/// <summary>
/// Marker component that makes an entity with <see cref="PredictedBatteryComponent"/> update its appearance data for use with visualizers.
/// Also works with an entity with <see cref="PowerCellSlotComponent"/> and will relay the state of the inserted powercell.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class PredictedBatteryVisualsComponent : Component;
/// <summary>
/// Keys for the appearance data.
/// </summary>
[Serializable, NetSerializable]
public enum BatteryVisuals : byte
{
/// <summary>
/// The current charge state of the battery.
/// Either full, empty, or neither.
/// Uses a <see cref="BatteryState"/>.
/// </summary>
State,
/// <summary>
/// Is the battery currently charging or discharging?
/// Uses a <see cref="BatteryChargingState"/>.
/// </summary>
Charging,
}
/// <summary>
/// Charge level status of the battery.
/// </summary>
[Serializable, NetSerializable]
public enum BatteryChargingState : byte
{
/// <summary>
/// PredictedBatteryComponent.ChargeRate &gt; 0
/// </summary>
Charging,
/// <summary>
/// PredictedBatteryComponent.ChargeRate &lt; 0
/// </summary>
Decharging,
/// <summary>
/// PredictedBatteryComponent.ChargeRate == 0
/// </summary>
Constant,
}

View File

@ -0,0 +1,296 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Emp;
using Content.Shared.Examine;
using Content.Shared.Power.Components;
using Content.Shared.PowerCell;
using Content.Shared.PowerCell.Components;
using Content.Shared.Storage.Components;
using Content.Shared.Whitelist;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
namespace Content.Shared.Power.EntitySystems;
public sealed class ChargerSystem : EntitySystem
{
[Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly SharedPowerReceiverSystem _receiver = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ChargerComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<ChargerComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<ChargerComponent, EntInsertedIntoContainerMessage>(OnInserted);
SubscribeLocalEvent<ChargerComponent, EntRemovedFromContainerMessage>(OnRemoved);
SubscribeLocalEvent<ChargerComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
SubscribeLocalEvent<ChargerComponent, InsertIntoEntityStorageAttemptEvent>(OnEntityStorageInsertAttempt);
SubscribeLocalEvent<ChargerComponent, ExaminedEvent>(OnChargerExamine);
SubscribeLocalEvent<ChargerComponent, EmpPulseEvent>(OnEmpPulse);
SubscribeLocalEvent<ChargerComponent, EmpDisabledRemovedEvent>(OnEmpRemoved);
SubscribeLocalEvent<InsideChargerComponent, RefreshChargeRateEvent>(OnRefreshChargeRate);
SubscribeLocalEvent<InsideChargerComponent, PredictedBatteryStateChangedEvent>(OnStatusChanged);
}
private void OnStartup(Entity<ChargerComponent> ent, ref ComponentStartup args)
{
UpdateStatus(ent);
}
private void OnChargerExamine(EntityUid uid, ChargerComponent component, ExaminedEvent args)
{
using (args.PushGroup(nameof(ChargerComponent)))
{
// rate at which the charger charges
args.PushMarkup(Loc.GetString("charger-examine", ("color", "yellow"), ("chargeRate", (int)component.ChargeRate)));
// try to get contents of the charger
if (!_container.TryGetContainer(uid, component.SlotId, out var container))
return;
if (HasComp<PowerCellSlotComponent>(uid))
return;
// if charger is empty and not a power cell type charger, add empty message
// power cells have their own empty message by default, for things like flash lights
if (container.ContainedEntities.Count == 0)
{
args.PushMarkup(Loc.GetString("charger-empty"));
}
else
{
// add how much each item is charged it
foreach (var contained in container.ContainedEntities)
{
if (!SearchForBattery(contained, out var battery))
continue;
var chargePercentage = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge * 100;
args.PushMarkup(Loc.GetString("charger-content", ("chargePercentage", (int)chargePercentage)));
}
}
}
}
private void OnPowerChanged(Entity<ChargerComponent> ent, ref PowerChangedEvent args)
{
RefreshAllBatteries(ent);
UpdateStatus(ent);
}
private void OnInserted(Entity<ChargerComponent> ent, ref EntInsertedIntoContainerMessage args)
{
if (_timing.ApplyingState)
return; // Already networked in the same gamestate
if (args.Container.ID != ent.Comp.SlotId)
return;
AddComp<InsideChargerComponent>(args.Entity);
if (SearchForBattery(args.Entity, out var battery))
_battery.RefreshChargeRate(battery.Value.AsNullable());
UpdateStatus(ent);
}
private void OnRemoved(Entity<ChargerComponent> ent, ref EntRemovedFromContainerMessage args)
{
if (_timing.ApplyingState)
return; // Already networked in the same gamestate
if (args.Container.ID != ent.Comp.SlotId)
return;
RemComp<InsideChargerComponent>(args.Entity);
if (SearchForBattery(args.Entity, out var battery))
_battery.RefreshChargeRate(battery.Value.AsNullable());
UpdateStatus(ent);
}
/// <summary>
/// Verify that the entity being inserted is actually rechargeable.
/// </summary>
private void OnInsertAttempt(EntityUid uid, ChargerComponent component, ContainerIsInsertingAttemptEvent args)
{
if (!component.Initialized)
return;
if (args.Container.ID != component.SlotId)
return;
if (!TryComp<PowerCellSlotComponent>(args.EntityUid, out var cellSlot))
return;
if (!cellSlot.FitsInCharger)
args.Cancel();
}
private void OnEntityStorageInsertAttempt(EntityUid uid, ChargerComponent component, ref InsertIntoEntityStorageAttemptEvent args)
{
if (!component.Initialized || args.Cancelled)
return;
if (args.Container.ID != component.SlotId)
return;
if (!TryComp<PowerCellSlotComponent>(uid, out var cellSlot))
return;
if (!cellSlot.FitsInCharger)
args.Cancelled = true;
}
private void OnEmpPulse(Entity<ChargerComponent> ent, ref EmpPulseEvent args)
{
args.Affected = true;
args.Disabled = true;
RefreshAllBatteries(ent);
UpdateStatus(ent);
}
private void OnEmpRemoved(Entity<ChargerComponent> ent, ref EmpDisabledRemovedEvent args)
{
RefreshAllBatteries(ent);
UpdateStatus(ent);
}
private void OnRefreshChargeRate(Entity<InsideChargerComponent> ent, ref RefreshChargeRateEvent args)
{
var chargerUid = Transform(ent).ParentUid;
if (HasComp<EmpDisabledComponent>(chargerUid))
return;
if (!TryComp<ChargerComponent>(chargerUid, out var chargerComp))
return;
if (!chargerComp.Portable && !_receiver.IsPowered(chargerUid))
return;
if (_whitelist.IsWhitelistFail(chargerComp.Whitelist, ent.Owner))
return;
args.NewChargeRate += chargerComp.ChargeRate;
}
private void OnStatusChanged(Entity<InsideChargerComponent> ent, ref PredictedBatteryStateChangedEvent args)
{
// If the battery is full update the visuals and power draw of the charger.
var chargerUid = Transform(ent).ParentUid;
if (!TryComp<ChargerComponent>(chargerUid, out var chargerComp))
return;
UpdateStatus((chargerUid, chargerComp));
}
// Start DeltaV - event-based search for battery
public bool SearchForBattery(EntityUid uid, [NotNullWhen(true)] out Entity<PredictedBatteryComponent>? battery)
{
// try get a battery directly on the inserted entity
if (TryComp<PredictedBatteryComponent>(uid, out var batteryComp))
{
battery = (uid, batteryComp);
return true;
}
// or by checking for a power cell slot on the inserted entity
if (_powerCell.TryGetBatteryFromSlot(uid, out battery))
return true;
var evt = new SearchForBatteryEvent();
RaiseLocalEvent(uid, ref evt);
if (evt.Handled && evt.Uid.HasValue)
{
battery = (evt.Uid.Value, evt.Component!);
return true;
}
battery = null;
return false;
}
// End DeltaV - event-based search for battery
private void RefreshAllBatteries(Entity<ChargerComponent> ent)
{
// try to get contents of the charger
if (!_container.TryGetContainer(ent.Owner, ent.Comp.SlotId, out var container))
return;
foreach (var item in container.ContainedEntities)
{
if (SearchForBattery(item, out var battery))
_battery.RefreshChargeRate(battery.Value.AsNullable());
}
}
private void UpdateStatus(Entity<ChargerComponent> ent)
{
TryComp<AppearanceComponent>(ent, out var appearance);
if (!_container.TryGetContainer(ent.Owner, ent.Comp.SlotId, out var container))
return;
_appearance.SetData(ent.Owner, CellVisual.Occupied, container.ContainedEntities.Count != 0, appearance);
var status = GetStatus(ent);
switch (status)
{
case CellChargerStatus.Charging:
// TODO: If someone ever adds chargers that can charge multiple batteries at once then set this to the total draw rate.
_receiver.SetLoad(ent.Owner, ent.Comp.ChargeRate);
break;
default:
// Don't set the load to 0 or the charger will be considered as powered even if the LV connection is unpowered.
// TODO: Fix this on an ApcPowerReceiver level.
_receiver.SetLoad(ent.Owner, ent.Comp.PassiveDraw);
break;
}
_appearance.SetData(ent.Owner, CellVisual.Light, status, appearance);
}
private CellChargerStatus GetStatus(Entity<ChargerComponent> ent)
{
if (!ent.Comp.Portable && !Transform(ent).Anchored)
return CellChargerStatus.Off;
if (!ent.Comp.Portable && !_receiver.IsPowered(ent.Owner))
return CellChargerStatus.Off;
if (HasComp<EmpDisabledComponent>(ent))
return CellChargerStatus.Off;
if (!_container.TryGetContainer(ent.Owner, ent.Comp.SlotId, out var container))
return CellChargerStatus.Off;
if (container.ContainedEntities.Count == 0)
return CellChargerStatus.Empty;
// Use the first stored battery for visuals. If someone ever makes a multi-slot charger then this will need to be changed.
if (!SearchForBattery(container.ContainedEntities[0], out var battery))
return CellChargerStatus.Off;
if (_battery.IsFull(battery.Value.AsNullable()))
return CellChargerStatus.Charged;
return CellChargerStatus.Charging;
}
}
// Begin DeltaV - event-based search for battery
/// <summary>
/// Event raised to search for batteries within an entity
/// </summary>
[ByRefEvent]
public struct SearchForBatteryEvent
{
public EntityUid? Uid;
public PredictedBatteryComponent? Component;
public bool Handled;
}
// End DeltaV - event-based search for battery

View File

@ -0,0 +1,278 @@
using Content.Shared.Power.Components;
using JetBrains.Annotations;
namespace Content.Shared.Power.EntitySystems;
/// <summary>
/// Responsible for <see cref="PredictedBatteryComponent"/>.
/// Predicted equivalent of <see cref="Content.Server.Power.EntitySystems.BatterySystem"/>.
/// If you make changes to this make sure to keep the two consistent.
/// </summary>
public sealed partial class PredictedBatterySystem
{
/// <summary>
/// Changes the battery's charge by the given amount
/// and resets the self-recharge cooldown if it exists.
/// A positive value will add charge, a negative value will remove charge.
/// </summary>
/// <returns>The actually changed amount.</returns>
[PublicAPI]
public float ChangeCharge(Entity<PredictedBatteryComponent?> ent, float amount)
{
if (!Resolve(ent, ref ent.Comp))
return 0;
var oldValue = GetCharge(ent);
var newValue = Math.Clamp(oldValue + amount, 0, ent.Comp.MaxCharge);
var delta = newValue - oldValue;
if (delta == 0f)
return 0f;
var curTime = _timing.CurTime;
ent.Comp.LastCharge = newValue;
ent.Comp.LastUpdate = curTime;
Dirty(ent);
TrySetChargeCooldown(ent.Owner);
var changedEv = new PredictedBatteryChargeChangedEvent(newValue, ent.Comp.ChargeRate, curTime, ent.Comp.MaxCharge);
RaiseLocalEvent(ent, ref changedEv);
// Raise events if the battery status changed between full, empty, or neither.
UpdateState(ent);
return delta;
}
/// <summary>
/// Removes the given amount of charge from the battery
/// and resets the self-recharge cooldown if it exists.
/// </summary>
/// <returns>The actually changed amount.</returns>
[PublicAPI]
public float UseCharge(Entity<PredictedBatteryComponent?> ent, float amount)
{
if (amount <= 0f)
return 0f;
return ChangeCharge(ent, -amount);
}
/// <summary>
/// If sufficient charge is available on the battery, use it. Otherwise, don't.
/// Resets the self-recharge cooldown if it exists.
/// Always returns false on the client.
/// </summary>
/// <returns>If the full amount was able to be removed.</returns>
[PublicAPI]
public bool TryUseCharge(Entity<PredictedBatteryComponent?> ent, float amount)
{
if (!Resolve(ent, ref ent.Comp, false) || amount > GetCharge(ent))
return false;
UseCharge(ent, amount);
return true;
}
/// <summary>
/// Sets the battery's charge.
/// </summary>
[PublicAPI]
public void SetCharge(Entity<PredictedBatteryComponent?> ent, float value)
{
if (!Resolve(ent, ref ent.Comp))
return;
var oldValue = GetCharge(ent);
var newValue = Math.Clamp(value, 0, ent.Comp.MaxCharge);
var delta = newValue - oldValue;
if (delta == 0f)
return;
var curTime = _timing.CurTime;
ent.Comp.LastCharge = newValue;
ent.Comp.LastUpdate = curTime;
Dirty(ent);
var ev = new PredictedBatteryChargeChangedEvent(newValue, ent.Comp.ChargeRate, curTime, ent.Comp.MaxCharge);
RaiseLocalEvent(ent, ref ev);
// Raise events if the battery status changed between full, empty, or neither.
UpdateState(ent);
}
/// <summary>
/// Sets the battery's maximum charge.
/// </summary>
[PublicAPI]
public void SetMaxCharge(Entity<PredictedBatteryComponent?> ent, float value)
{
if (!Resolve(ent, ref ent.Comp))
return;
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (value == ent.Comp.MaxCharge)
return;
ent.Comp.MaxCharge = Math.Max(value, 0);
ent.Comp.LastCharge = GetCharge(ent); // This clamps it using the new max.
var curTime = _timing.CurTime;
ent.Comp.LastUpdate = curTime;
Dirty(ent);
var ev = new PredictedBatteryChargeChangedEvent(ent.Comp.LastCharge, ent.Comp.ChargeRate, curTime, ent.Comp.MaxCharge);
RaiseLocalEvent(ent, ref ev);
// Raise events if the battery status changed between full, empty, or neither.
UpdateState(ent);
}
/// <summary>
/// Updates the battery's charge state and sends an event if it changed.
/// </summary>
[PublicAPI]
public void UpdateState(Entity<PredictedBatteryComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp))
return;
var oldState = ent.Comp.State;
var newState = BatteryState.Neither;
var charge = GetCharge(ent);
if (charge == ent.Comp.MaxCharge)
newState = BatteryState.Full;
else if (charge == 0f)
newState = BatteryState.Empty;
if (oldState == newState)
return;
ent.Comp.State = newState;
Dirty(ent);
var changedEv = new PredictedBatteryStateChangedEvent(oldState, newState);
RaiseLocalEvent(ent, ref changedEv);
}
/// <summary>
/// Gets the battery's current charge.
/// </summary>
[PublicAPI]
public float GetCharge(Entity<PredictedBatteryComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return 0f;
var curTime = _timing.CurTime;
// We have a constant charge rate, so the charge changes linearly over time.
var dt = (curTime - ent.Comp.LastUpdate).TotalSeconds;
var charge = Math.Clamp(ent.Comp.LastCharge + (float)(dt * ent.Comp.ChargeRate), 0f, ent.Comp.MaxCharge);
return charge;
}
/// <summary>
/// Gets number of remaining uses for the given charge cost.
/// </summary>
[PublicAPI]
public int GetRemainingUses(Entity<PredictedBatteryComponent?> ent, float cost)
{
if (cost <= 0)
return 0;
if (!Resolve(ent, ref ent.Comp))
return 0;
return (int)(GetCharge(ent) / cost);
}
/// <summary>
/// Gets number of maximum uses at full charge for the given charge cost.
/// </summary>
[PublicAPI]
public int GetMaxUses(Entity<PredictedBatteryComponent?> ent, float cost)
{
if (cost <= 0)
return 0;
if (!Resolve(ent, ref ent.Comp))
return 0;
return (int)(ent.Comp.MaxCharge / cost);
}
/// <summary>
/// Refreshes the battery's current charge rate by raising a <see cref="RefreshChargeRateEvent"/>.
/// </summary>
/// <returns>The new charge rate.</returns>
[PublicAPI]
public float RefreshChargeRate(Entity<PredictedBatteryComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return 0f;
ent.Comp.LastCharge = GetCharge(ent); // Prevent the new rate from modifying the current charge.
var curTime = _timing.CurTime;
ent.Comp.LastUpdate = curTime;
var refreshEv = new RefreshChargeRateEvent(ent.Comp.MaxCharge);
RaiseLocalEvent(ent, ref refreshEv);
ent.Comp.ChargeRate = refreshEv.NewChargeRate;
Dirty(ent);
// Inform other systems about the new rate;
var changedEv = new PredictedBatteryChargeChangedEvent(ent.Comp.LastCharge, ent.Comp.ChargeRate, curTime, ent.Comp.MaxCharge);
RaiseLocalEvent(ent, ref changedEv);
return refreshEv.NewChargeRate;
}
/// <summary>
/// Checks if the entity has a self recharge and puts it on cooldown if applicable.
/// Uses the cooldown time given in the component.
/// </summary>
[PublicAPI]
public void TrySetChargeCooldown(Entity<PredictedBatterySelfRechargerComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return;
if (ent.Comp.AutoRechargePauseTime == TimeSpan.Zero)
return; // no recharge pause
if (_timing.CurTime + ent.Comp.AutoRechargePauseTime <= ent.Comp.NextAutoRecharge)
return; // the current pause is already longer
SetChargeCooldown(ent, ent.Comp.AutoRechargePauseTime);
}
/// <summary>
/// Puts the entity's self recharge on cooldown for the specified time.
/// </summary>
[PublicAPI]
public void SetChargeCooldown(Entity<PredictedBatterySelfRechargerComponent?> ent, TimeSpan cooldown)
{
if (!Resolve(ent, ref ent.Comp))
return;
ent.Comp.NextAutoRecharge = _timing.CurTime + cooldown;
Dirty(ent);
RefreshChargeRate(ent.Owner); // Apply the new charge rate.
}
/// <summary>
/// Returns whether the battery is full.
/// </summary>
[PublicAPI]
public bool IsFull(Entity<PredictedBatteryComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp))
return false;
return GetCharge(ent) >= ent.Comp.MaxCharge;
}
}

View File

@ -0,0 +1,182 @@
using Content.Shared.Cargo;
using Content.Shared.Emp;
using Content.Shared.Examine;
using Content.Shared.Power.Components;
using Content.Shared.Rejuvenate;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Shared.Power.EntitySystems;
/// <summary>
/// Responsible for <see cref="PredictedBatteryComponent"/>.
/// Predicted equivalent of <see cref="Content.Server.Power.EntitySystems.BatterySystem"/>.
/// If you make changes to this make sure to keep the two consistent.
/// </summary>
public sealed partial class PredictedBatterySystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PredictedBatteryComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<PredictedBatteryComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<PredictedBatteryComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<PredictedBatteryComponent, EmpPulseEvent>(OnEmpPulse);
SubscribeLocalEvent<PredictedBatteryComponent, RejuvenateEvent>(OnRejuvenate);
SubscribeLocalEvent<PredictedBatteryComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<PredictedBatteryComponent, PriceCalculationEvent>(CalculateBatteryPrice);
SubscribeLocalEvent<PredictedBatteryComponent, ChangeChargeEvent>(OnChangeCharge);
SubscribeLocalEvent<PredictedBatteryComponent, GetChargeEvent>(OnGetCharge);
SubscribeLocalEvent<PredictedBatterySelfRechargerComponent, RefreshChargeRateEvent>(OnRefreshChargeRate);
SubscribeLocalEvent<PredictedBatterySelfRechargerComponent, ComponentStartup>(OnRechargerStartup);
SubscribeLocalEvent<PredictedBatterySelfRechargerComponent, ComponentRemove>(OnRechargerRemove);
SubscribeLocalEvent<PredictedBatteryVisualsComponent, PredictedBatteryChargeChangedEvent>(OnVisualsChargeChanged);
SubscribeLocalEvent<PredictedBatteryVisualsComponent, PredictedBatteryStateChangedEvent>(OnVisualsStateChanged);
}
private void OnInit(Entity<PredictedBatteryComponent> ent, ref ComponentInit args)
{
DebugTools.Assert(!HasComp<BatteryComponent>(ent), $"{ent} has both BatteryComponent and PredictedBatteryComponent");
}
private void OnStartup(Entity<PredictedBatteryComponent> ent, ref ComponentStartup args)
{
// In case a recharging component was added before the battery component itself.
// Doing this only on map init is not enough because the charge rate is not a datafield, but cached, so it would get lost when reloading the game.
// If we would make it a datafield then the integration tests would complain about modifying it before map init.
RefreshChargeRate(ent.AsNullable());
}
private void OnMapInit(Entity<PredictedBatteryComponent> ent, ref MapInitEvent args)
{
SetCharge(ent.AsNullable(), ent.Comp.StartingCharge);
RefreshChargeRate(ent.AsNullable());
}
private void OnRejuvenate(Entity<PredictedBatteryComponent> ent, ref RejuvenateEvent args)
{
SetCharge(ent.AsNullable(), ent.Comp.MaxCharge);
}
private void OnEmpPulse(Entity<PredictedBatteryComponent> ent, ref EmpPulseEvent args)
{
args.Affected = true;
UseCharge(ent.AsNullable(), args.EnergyConsumption);
}
private void OnExamine(Entity<PredictedBatteryComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
if (!HasComp<ExaminableBatteryComponent>(ent))
return;
var chargePercentRounded = 0;
var currentCharge = GetCharge(ent.AsNullable());
if (ent.Comp.MaxCharge != 0)
chargePercentRounded = (int)(100 * currentCharge / ent.Comp.MaxCharge);
args.PushMarkup(
Loc.GetString(
"examinable-battery-component-examine-detail",
("percent", chargePercentRounded),
("markupPercentColor", "green")
)
);
}
/// <summary>
/// Gets the price for the power contained in an entity's battery.
/// </summary>
private void CalculateBatteryPrice(Entity<PredictedBatteryComponent> ent, ref PriceCalculationEvent args)
{
args.Price += GetCharge(ent.AsNullable()) * ent.Comp.PricePerJoule;
}
private void OnChangeCharge(Entity<PredictedBatteryComponent> ent, ref ChangeChargeEvent args)
{
if (args.ResidualValue == 0)
return;
args.ResidualValue -= ChangeCharge(ent.AsNullable(), args.ResidualValue);
}
private void OnGetCharge(Entity<PredictedBatteryComponent> ent, ref GetChargeEvent args)
{
args.CurrentCharge += GetCharge(ent.AsNullable());
args.MaxCharge += ent.Comp.MaxCharge;
}
private void OnRefreshChargeRate(Entity<PredictedBatterySelfRechargerComponent> ent, ref RefreshChargeRateEvent args)
{
if (_timing.CurTime < ent.Comp.NextAutoRecharge)
return; // Still on cooldown
args.NewChargeRate += ent.Comp.AutoRechargeRate;
}
public override void Update(float frameTime)
{
var curTime = _timing.CurTime;
// Update self-recharging cooldowns.
var rechargerQuery = EntityQueryEnumerator<PredictedBatterySelfRechargerComponent, PredictedBatteryComponent>();
while (rechargerQuery.MoveNext(out var uid, out var recharger, out var battery))
{
if (recharger.NextAutoRecharge == null || curTime < recharger.NextAutoRecharge)
continue;
recharger.NextAutoRecharge = null; // Don't refresh every tick.
Dirty(uid, recharger);
RefreshChargeRate((uid, battery)); // Cooldown is over, apply the new recharge rate.
}
// Raise events when the battery is full or empty so that other systems can react and visuals can get updated.
// This is not doing that many calculations, it only has to get the current charge and only raises events if something did change.
// If this turns out to be too expensive and shows up on grafana consider updating it less often.
var batteryQuery = EntityQueryEnumerator<PredictedBatteryComponent>();
while (batteryQuery.MoveNext(out var uid, out var battery))
{
if (battery.ChargeRate == 0f)
continue; // No need to check if it's constant.
UpdateState((uid, battery));
}
}
private void OnRechargerStartup(Entity<PredictedBatterySelfRechargerComponent> ent, ref ComponentStartup args)
{
// In case this component is added after the battery component.
RefreshChargeRate(ent.Owner);
}
private void OnRechargerRemove(Entity<PredictedBatterySelfRechargerComponent> ent, ref ComponentRemove args)
{
// We use ComponentRemove to make sure this component no longer subscribes to the refresh event.
RefreshChargeRate(ent.Owner);
}
private void OnVisualsChargeChanged(Entity<PredictedBatteryVisualsComponent> ent, ref PredictedBatteryChargeChangedEvent args)
{
// Update the appearance data for the charge rate.
// We have a separate component for this to not duplicate the networking cost unless we actually use it.
var state = BatteryChargingState.Constant;
if (args.CurrentChargeRate > 0f)
state = BatteryChargingState.Charging;
else if (args.CurrentChargeRate < 0f)
state = BatteryChargingState.Decharging;
_appearance.SetData(ent.Owner, BatteryVisuals.Charging, state);
}
private void OnVisualsStateChanged(Entity<PredictedBatteryVisualsComponent> ent, ref PredictedBatteryStateChangedEvent args)
{
// Update the appearance data for the fill level (empty, full, in-between).
// We have a separate component for this to not duplicate the networking cost unless we actually use it.
_appearance.SetData(ent.Owner, BatteryVisuals.State, args.NewState);
}
}

View File

@ -1,5 +1,6 @@
using Content.Shared.Emp; using Content.Shared.Emp;
using Content.Shared.Power.Components; using Content.Shared.Power.Components;
using JetBrains.Annotations;
namespace Content.Shared.Power.EntitySystems; namespace Content.Shared.Power.EntitySystems;
@ -21,19 +22,23 @@ public abstract class SharedBatterySystem : EntitySystem
} }
/// <summary> /// <summary>
/// Changes the battery's charge by the given amount. /// Changes the battery's charge by the given amount
/// and resets the self-recharge cooldown if it exists.
/// A positive value will add charge, a negative value will remove charge. /// A positive value will add charge, a negative value will remove charge.
/// </summary> /// </summary>
/// <returns>The actually changed amount.</returns> /// <returns>The actually changed amount.</returns>
[PublicAPI]
public virtual float ChangeCharge(Entity<BatteryComponent?> ent, float amount) public virtual float ChangeCharge(Entity<BatteryComponent?> ent, float amount)
{ {
return 0f; return 0f;
} }
/// <summary> /// <summary>
/// Removes the given amount of charge from the battery. /// Removes the given amount of charge from the battery
/// and resets the self-recharge cooldown if it exists.
/// </summary> /// </summary>
/// <returns>The actually changed amount.</returns> /// <returns>The actually changed amount.</returns>
[PublicAPI]
public virtual float UseCharge(Entity<BatteryComponent?> ent, float amount) public virtual float UseCharge(Entity<BatteryComponent?> ent, float amount)
{ {
return 0f; return 0f;
@ -41,9 +46,11 @@ public abstract class SharedBatterySystem : EntitySystem
/// <summary> /// <summary>
/// If sufficient charge is available on the battery, use it. Otherwise, don't. /// If sufficient charge is available on the battery, use it. Otherwise, don't.
/// Resets the self-recharge cooldown if it exists.
/// Always returns false on the client. /// Always returns false on the client.
/// </summary> /// </summary>
/// <returns>If the full amount was able to be removed.</returns> /// <returns>If the full amount was able to be removed.</returns>
[PublicAPI]
public virtual bool TryUseCharge(Entity<BatteryComponent?> ent, float amount) public virtual bool TryUseCharge(Entity<BatteryComponent?> ent, float amount)
{ {
return false; return false;
@ -52,21 +59,25 @@ public abstract class SharedBatterySystem : EntitySystem
/// <summary> /// <summary>
/// Sets the battery's charge. /// Sets the battery's charge.
/// </summary> /// </summary>
[PublicAPI]
public virtual void SetCharge(Entity<BatteryComponent?> ent, float value) { } public virtual void SetCharge(Entity<BatteryComponent?> ent, float value) { }
/// <summary> /// <summary>
/// Sets the battery's maximum charge. /// Sets the battery's maximum charge.
/// </summary> /// </summary>
[PublicAPI]
public virtual void SetMaxCharge(Entity<BatteryComponent?> ent, float value) { } public virtual void SetMaxCharge(Entity<BatteryComponent?> ent, float value) { }
/// <summary> /// <summary>
/// Checks if the entity has a self recharge and puts it on cooldown if applicable. /// Checks if the entity has a self recharge and puts it on cooldown if applicable.
/// Uses the cooldown time given in the component. /// Uses the cooldown time given in the component.
/// </summary> /// </summary>
[PublicAPI]
public virtual void TrySetChargeCooldown(Entity<BatterySelfRechargerComponent?> ent) { } public virtual void TrySetChargeCooldown(Entity<BatterySelfRechargerComponent?> ent) { }
/// <summary> /// <summary>
/// Puts the entity's self recharge on cooldown for the specified time. /// Puts the entity's self recharge on cooldown for the specified time.
/// </summary> /// </summary>
[PublicAPI]
public virtual void SetChargeCooldown(Entity<BatterySelfRechargerComponent?> ent, TimeSpan cooldown) { } public virtual void SetChargeCooldown(Entity<BatterySelfRechargerComponent?> ent, TimeSpan cooldown) { }
} }

View File

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

View File

@ -17,13 +17,6 @@ public abstract class SharedPowerReceiverSystem : EntitySystem
public abstract bool ResolveApc(EntityUid entity, [NotNullWhen(true)] ref SharedApcPowerReceiverComponent? component); public abstract bool ResolveApc(EntityUid entity, [NotNullWhen(true)] ref SharedApcPowerReceiverComponent? component);
/// <summary>
/// Goobstation - Lets shared code set power load.
/// </summary>
public virtual void SetLoad(SharedApcPowerReceiverComponent comp, float load)
{
}
public void SetNeedsPower(EntityUid uid, bool value, SharedApcPowerReceiverComponent? receiver = null) public void SetNeedsPower(EntityUid uid, bool value, SharedApcPowerReceiverComponent? receiver = null)
{ {
if (!ResolveApc(uid, ref receiver) || receiver.NeedsPower == value) if (!ResolveApc(uid, ref receiver) || receiver.NeedsPower == value)
@ -99,6 +92,17 @@ public abstract class SharedPowerReceiverSystem : EntitySystem
// NOOP on server because client has 0 idea of load so we can't raise it properly in shared. // NOOP on server because client has 0 idea of load so we can't raise it properly in shared.
} }
/// <summary>
/// Sets the power load of this power receiver.
/// </summary>
public void SetLoad(Entity<SharedApcPowerReceiverComponent?> entity, float load)
{
if (!ResolveApc(entity.Owner, ref entity.Comp))
return;
entity.Comp.Load = load;
}
/// <summary> /// <summary>
/// Checks if entity is APC-powered device, and if it have power. /// Checks if entity is APC-powered device, and if it have power.
/// </summary> /// </summary>

View File

@ -1,20 +0,0 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Power
{
[Serializable, NetSerializable]
public enum CellChargerStatus
{
Off,
Empty,
Charging,
Charged,
}
[Serializable, NetSerializable]
public enum CellVisual
{
Occupied, // If there's an item in it
Light,
}
}

View File

@ -1,26 +1,11 @@
using Content.Shared.Power.Components;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.PowerCell; namespace Content.Shared.PowerCell.Components;
/// <summary> /// <summary>
/// This component enables power-cell related interactions (e.g., entity white-lists, cell sizes, examine, rigging). /// This component enables power-cell related interactions (e.g. EntityWhitelists, cell sizes, examine, rigging).
/// The actual power functionality is provided by the server-side BatteryComponent. /// The actual power functionality is provided by the <see cref="PredictedBatteryComponent"/>.
/// </summary> /// </summary>
[NetworkedComponent] [RegisterComponent, NetworkedComponent]
[RegisterComponent] public sealed partial class PowerCellComponent : Component;
public sealed partial class PowerCellComponent : Component
{
public const int PowerCellVisualsLevels = 2;
}
[Serializable, NetSerializable]
public enum PowerCellVisuals : byte
{
ChargeLevel
}
[Serializable, NetSerializable]
public enum PowerCellSlotVisuals : byte
{
Enabled
}

View File

@ -0,0 +1,35 @@
using Robust.Shared.GameStates;
namespace Content.Shared.PowerCell.Components;
/// <summary>
/// Indicates that the entity's ActivatableUI requires power or else it closes.
/// </summary>
/// <remarks>
/// With ActivatableUI it will activate and deactivate when the ui is opened and closed, drawing power inbetween.
/// </remarks>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(PowerCellSystem))]
public sealed partial class PowerCellDrawComponent : Component
{
/// <summary>
/// Whether drawing is enabled.
/// Having no cell will still disable it.
/// </summary>
[DataField, AutoNetworkedField, ViewVariables]
public bool Enabled = true;
/// <summary>
/// How much the entity draws while the UI is open (in Watts).
/// Set to 0 if you just wish to check for power upon opening the UI.
/// </summary>
[DataField, AutoNetworkedField, ViewVariables]
public float DrawRate = 1f;
/// <summary>
/// How much power is used whenever the entity is "used" (in Joules).
/// This is used to ensure the UI won't open again without a minimum use power.
/// </summary>
[DataField, AutoNetworkedField]
public float UseCharge;
}

View File

@ -1,8 +1,9 @@
using Content.Shared.Containers.ItemSlots; using Content.Shared.Containers.ItemSlots;
using Robust.Shared.GameStates;
namespace Content.Shared.PowerCell.Components; namespace Content.Shared.PowerCell.Components;
[RegisterComponent] [RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class PowerCellSlotComponent : Component public sealed partial class PowerCellSlotComponent : Component
{ {
/// <summary> /// <summary>
@ -10,29 +11,17 @@ public sealed partial class PowerCellSlotComponent : Component
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Given that <see cref="PowerCellSystem"/> needs to verify that a given cell has the correct cell-size before /// Given that <see cref="PowerCellSystem"/> needs to verify that a given cell has the correct cell-size before
/// inserting anyways, there is no need to specify a separate entity whitelist. In this slot's yaml definition. /// inserting anyways, there is no need to specify a separate entity whitelist in this slot's yaml definition.
/// </remarks> /// </remarks>
[DataField("cellSlotId", required: true)] [DataField(required: true)]
public string CellSlotId = string.Empty; public string CellSlotId = string.Empty;
/// <summary> /// <summary>
/// Can this entity be inserted directly into a charging station? If false, you need to manually remove the power /// Can this entity be inserted directly into a charging station? If false, you need to manually remove the power
/// cell and recharge it separately. /// cell and recharge it separately.
/// </summary> /// </summary>
[DataField("fitsInCharger")] [DataField, AutoNetworkedField]
public bool FitsInCharger = true; public bool FitsInCharger = true;
} }
/// <summary>
/// Raised directed at an entity with a power cell slot when the power cell inside has its charge updated or is ejected/inserted.
/// </summary>
public sealed class PowerCellChangedEvent : EntityEventArgs
{
public readonly bool Ejected;
public PowerCellChangedEvent(bool ejected)
{
Ejected = ejected;
}
}

View File

@ -1,68 +0,0 @@
using Content.Shared.Item.ItemToggle.Components;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.PowerCell;
/// <summary>
/// Indicates that the entity's ActivatableUI requires power or else it closes.
/// </summary>
/// <remarks>
/// With ActivatableUI it will activate and deactivate when the ui is opened and closed, drawing power inbetween.
/// </remarks>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
public sealed partial class PowerCellDrawComponent : Component
{
#region Prediction
/// <summary>
/// Whether there is any charge available to draw.
/// </summary>
[DataField, AutoNetworkedField]
public bool CanDraw;
/// <summary>
/// Whether there is sufficient charge to use.
/// </summary>
[DataField, AutoNetworkedField]
public bool CanUse;
#endregion
/// <summary>
/// Whether drawing is enabled.
/// Having no cell will still disable it.
/// </summary>
[DataField, AutoNetworkedField]
public bool Enabled = true;
/// <summary>
/// How much the entity draws while the UI is open (in Watts).
/// Set to 0 if you just wish to check for power upon opening the UI.
/// </summary>
[DataField]
public float DrawRate = 1f;
/// <summary>
/// How much power is used whenever the entity is "used" (in Joules).
/// This is used to ensure the UI won't open again without a minimum use power.
/// </summary>
/// <remarks>
/// This is not a rate how the datafield name implies, but a one-time cost.
/// </remarks>
[DataField]
public float UseRate;
/// <summary>
/// When the next automatic power draw will occur
/// </summary>
[DataField("nextUpdate", customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan NextUpdateTime;
/// <summary>
/// How long to wait between power drawing.
/// </summary>
[DataField]
public TimeSpan Delay = TimeSpan.FromSeconds(1);
}

View File

@ -5,3 +5,9 @@ namespace Content.Shared.PowerCell;
/// </summary> /// </summary>
[ByRefEvent] [ByRefEvent]
public readonly record struct PowerCellSlotEmptyEvent; public readonly record struct PowerCellSlotEmptyEvent;
/// <summary>
/// Raised directed at an entity with a power cell slot when a power cell is ejected/inserted.
/// </summary>
[ByRefEvent]
public record struct PowerCellChangedEvent(bool Ejected);

View File

@ -0,0 +1,145 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Power.Components;
using Content.Shared.PowerCell.Components;
using JetBrains.Annotations;
namespace Content.Shared.PowerCell;
public sealed partial class PowerCellSystem
{
/// <summary>
/// Gets the power cell battery inside a power cell slot.
/// </summary>
[PublicAPI]
public bool TryGetBatteryFromSlot(
Entity<PowerCellSlotComponent?> ent,
[NotNullWhen(true)] out Entity<PredictedBatteryComponent>? battery)
{
if (!Resolve(ent, ref ent.Comp, false))
{
battery = null;
return false;
}
if (!_itemSlots.TryGetSlot(ent.Owner, ent.Comp.CellSlotId, out ItemSlot? slot))
{
battery = null;
return false;
}
if (!TryComp<PredictedBatteryComponent>(slot.Item, out var batteryComp))
{
battery = null;
return false;
}
battery = (slot.Item.Value, batteryComp);
return true;
}
/// <summary>
/// Returns whether the entity has a slotted battery and charge for the requested action.
/// </summary>
/// <param name="ent">The power cell.</param>
/// <param name="charge">The charge that is needed.</param>
/// <param name="user">Show a popup to this user with the relevant details if specified.</param>
/// <param name="predicted">Whether to predict the popup or not.</param>
[PublicAPI]
public bool HasCharge(Entity<PowerCellSlotComponent?> ent, float charge, EntityUid? user = null, bool predicted = false)
{
if (!TryGetBatteryFromSlot(ent, out var battery))
{
if (user == null)
return false;
if (predicted)
_popup.PopupClient(Loc.GetString("power-cell-no-battery"), ent.Owner, user.Value);
else
_popup.PopupEntity(Loc.GetString("power-cell-no-battery"), ent.Owner, user.Value);
return false;
}
if (_battery.GetCharge(battery.Value.AsNullable()) < charge)
{
if (user == null)
return false;
if (predicted)
_popup.PopupClient(Loc.GetString("power-cell-insufficient"), ent.Owner, user.Value);
else
_popup.PopupEntity(Loc.GetString("power-cell-insufficient"), ent.Owner, user.Value);
return false;
}
return true;
}
/// <summary>
/// Tries to use charge from a slotted battery.
/// </summary>
/// <param name="ent">The power cell.</param>
/// <param name="charge">The charge that is needed.</param>
/// <param name="user">Show a popup to this user with the relevant details if specified.</param>
/// <param name="predicted">Whether to predict the popup or not.</param>
[PublicAPI]
public bool TryUseCharge(Entity<PowerCellSlotComponent?> ent, float charge, EntityUid? user = null, bool predicted = false)
{
if (!TryGetBatteryFromSlot(ent, out var battery))
{
if (user == null)
return false;
if (predicted)
_popup.PopupClient(Loc.GetString("power-cell-no-battery"), ent.Owner, user.Value);
else
_popup.PopupEntity(Loc.GetString("power-cell-no-battery"), ent.Owner, user.Value);
return false;
}
if (!_battery.TryUseCharge((battery.Value, battery), charge))
{
if (user == null)
return false;
if (predicted)
_popup.PopupClient(Loc.GetString("power-cell-insufficient"), ent.Owner, user.Value);
else
_popup.PopupEntity(Loc.GetString("power-cell-insufficient"), ent.Owner, user.Value);
return false;
}
return true;
}
/// <summary>
/// Gets number of remaining uses for the given charge cost.
/// </summary>
/// <param name="ent">The power cell.</param>
/// <param name="cost">The cost per use.</param>
[PublicAPI]
public int GetRemainingUses(Entity<PowerCellSlotComponent?> ent, float cost)
{
if (!TryGetBatteryFromSlot(ent, out var battery))
return 0;
return _battery.GetRemainingUses(battery.Value.AsNullable(), cost);
}
/// <summary>
/// Gets number of maximum uses at full charge for the given charge cost.
/// </summary>
/// <param name="ent">The power cell.</param>
/// <param name="cost">The cost per use.</param>
[PublicAPI]
public int GetMaxUses(Entity<PowerCellSlotComponent?> ent, float cost)
{
if (!TryGetBatteryFromSlot(ent, out var battery))
return 0;
return _battery.GetMaxUses(battery.Value.AsNullable(), cost);
}
}

View File

@ -0,0 +1,76 @@
using Content.Shared.PowerCell.Components;
using JetBrains.Annotations;
namespace Content.Shared.PowerCell;
public sealed partial class PowerCellSystem
{
/// <summary>
/// Enables or disables the power cell draw.
/// </summary>
[PublicAPI]
public void SetDrawEnabled(Entity<PowerCellDrawComponent?> ent, bool enabled)
{
if (!Resolve(ent, ref ent.Comp, false) || ent.Comp.Enabled == enabled)
return;
ent.Comp.Enabled = enabled;
Dirty(ent, ent.Comp);
if (TryGetBatteryFromSlot(ent.Owner, out var battery))
_battery.RefreshChargeRate(battery.Value.AsNullable());
}
/// <summary>
/// Returns whether the entity has a slotted battery and <see cref="PowerCellDrawComponent.UseCharge"/> charge.
/// </summary>
/// <param name="ent">The device with the power cell slot.</param>
/// <param name="user">Show a popup to this user with the relevant details if specified.</param>
/// <param name="user">Whether to predict the popup or not.</param>
[PublicAPI]
public bool HasActivatableCharge(Entity<PowerCellDrawComponent?, PowerCellSlotComponent?> ent, EntityUid? user = null, bool predicted = false)
{
// Default to true if we don't have the components.
if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2, false))
return true;
return HasCharge((ent, ent.Comp2), ent.Comp1.UseCharge, user, predicted);
}
/// <summary>
/// Tries to use the <see cref="PowerCellDrawComponent.UseCharge"/> for this entity.
/// </summary>
/// <param name="ent">The device with the power cell slot.</param>
/// <param name="user">Show a popup to this user with the relevant details if specified.</param>
/// <param name="user">Whether to predict the popup or not.</param>
[PublicAPI]
public bool TryUseActivatableCharge(Entity<PowerCellDrawComponent?, PowerCellSlotComponent?> ent, EntityUid? user = null, bool predicted = false)
{
// Default to true if we don't have the components.
if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2, false))
return true;
if (TryUseCharge((ent, ent.Comp2), ent.Comp1.UseCharge, user, predicted))
return true;
return false;
}
/// <summary>
/// Whether the power cell has any power at all for the draw rate.
/// </summary>
/// <param name="ent">The device with the power cell slot.</param>
/// <param name="user">Show a popup to this user with the relevant details if specified.</param>
/// <param name="user">Whether to predict the popup or not.</param>
[PublicAPI]
public bool HasDrawCharge(Entity<PowerCellDrawComponent?, PowerCellSlotComponent?> ent, EntityUid? user = null, bool predicted = false)
{
// Default to true if we don't have the components.
if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2, false))
return true;
// 1 second of charge at the required draw rate.
return HasCharge((ent, ent.Comp2), ent.Comp1.DrawRate, user, predicted);
}
}

View File

@ -0,0 +1,40 @@
using Content.Shared.Emp;
using Content.Shared.Kitchen;
using Content.Shared.Power;
using Content.Shared.PowerCell.Components;
using Content.Shared.Rejuvenate;
namespace Content.Shared.PowerCell;
public sealed partial class PowerCellSystem
{
public void InitializeRelay()
{
SubscribeLocalEvent<PowerCellSlotComponent, BeingMicrowavedEvent>(RelayToCell);
SubscribeLocalEvent<PowerCellSlotComponent, RejuvenateEvent>(RelayToCell);
SubscribeLocalEvent<PowerCellSlotComponent, GetChargeEvent>(RelayToCell);
SubscribeLocalEvent<PowerCellSlotComponent, ChangeChargeEvent>(RelayToCell);
SubscribeLocalEvent<PowerCellComponent, EmpAttemptEvent>(RelayToCellSlot); // Prevent the ninja from EMPing its own battery
SubscribeLocalEvent<PowerCellComponent, PredictedBatteryChargeChangedEvent>(RelayToCellSlot);
SubscribeLocalEvent<PowerCellComponent, PredictedBatteryStateChangedEvent>(RelayToCellSlot); // For shutting down devices if the battery is empty
SubscribeLocalEvent<PowerCellComponent, RefreshChargeRateEvent>(RelayToCellSlot); // Allow devices to charge/drain inserted batteries
}
private void RelayToCell<T>(Entity<PowerCellSlotComponent> ent, ref T args) where T : notnull
{
if (!_itemSlots.TryGetSlot(ent.Owner, ent.Comp.CellSlotId, out var slot) || !slot.Item.HasValue)
return;
// Relay the event to the power cell.
RaiseLocalEvent(slot.Item.Value, ref args);
}
private void RelayToCellSlot<T>(Entity<PowerCellComponent> ent, ref T args) where T : notnull
{
var parent = Transform(ent).ParentUid;
// Relay the event to the slot entity.
if (HasComp<PowerCellSlotComponent>(parent))
RaiseLocalEvent(parent, ref args);
}
}

View File

@ -0,0 +1,154 @@
using Content.Shared.Containers.ItemSlots;
using Content.Shared.PowerCell.Components;
using Content.Shared.Examine;
using Content.Shared.Popups;
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
namespace Content.Shared.PowerCell;
public sealed partial class PowerCellSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
public override void Initialize()
{
base.Initialize();
InitializeRelay();
SubscribeLocalEvent<PowerCellSlotComponent, ContainerIsInsertingAttemptEvent>(OnCellSlotInsertAttempt);
SubscribeLocalEvent<PowerCellSlotComponent, EntInsertedIntoContainerMessage>(OnCellSlotInserted);
SubscribeLocalEvent<PowerCellSlotComponent, EntRemovedFromContainerMessage>(OnCellSlotRemoved);
SubscribeLocalEvent<PowerCellSlotComponent, ExaminedEvent>(OnCellSlotExamined);
SubscribeLocalEvent<PowerCellSlotComponent, PredictedBatteryStateChangedEvent>(OnCellSlotStateChanged);
SubscribeLocalEvent<PowerCellComponent, ExaminedEvent>(OnCellExamined);
SubscribeLocalEvent<PowerCellDrawComponent, RefreshChargeRateEvent>(OnDrawRefreshChargeRate);
SubscribeLocalEvent<PowerCellDrawComponent, ComponentStartup>(OnDrawStartup);
SubscribeLocalEvent<PowerCellDrawComponent, ComponentRemove>(OnDrawRemove);
}
private void OnCellSlotInsertAttempt(Entity<PowerCellSlotComponent> ent, ref ContainerIsInsertingAttemptEvent args)
{
if (!ent.Comp.Initialized)
return;
if (args.Container.ID != ent.Comp.CellSlotId)
return;
// TODO: Can't this just use the ItemSlot's whitelist?
if (!HasComp<PowerCellComponent>(args.EntityUid))
args.Cancel();
}
private void OnCellSlotInserted(Entity<PowerCellSlotComponent> ent, ref EntInsertedIntoContainerMessage args)
{
if (args.Container.ID != ent.Comp.CellSlotId)
return;
if (_timing.ApplyingState)
return; // The change in appearance data is already networked separately.
var ev = new PowerCellChangedEvent(false);
RaiseLocalEvent(ent, ref ev);
_battery.RefreshChargeRate(args.Entity);
// Only update the visuals if we actually use them.
if (!HasComp<PredictedBatteryVisualsComponent>(ent))
return;
// Set the data to that of the power cell
if (_appearance.TryGetData(args.Entity, BatteryVisuals.State, out BatteryState state))
_appearance.SetData(ent.Owner, BatteryVisuals.State, state);
// Set the data to that of the power cell
if (_appearance.TryGetData(args.Entity, BatteryVisuals.Charging, out BatteryChargingState charging))
_appearance.SetData(ent.Owner, BatteryVisuals.Charging, charging);
}
private void OnCellSlotRemoved(Entity<PowerCellSlotComponent> ent, ref EntRemovedFromContainerMessage args)
{
if (args.Container.ID != ent.Comp.CellSlotId)
return;
if (_timing.ApplyingState)
return; // The change in appearance data is already networked separately.
var ev = new PowerCellChangedEvent(true);
RaiseLocalEvent(ent, ref ev);
var emptyEv = new PowerCellSlotEmptyEvent();
RaiseLocalEvent(ent, ref emptyEv);
_battery.RefreshChargeRate(args.Entity);
// Only update the visuals if we actually use them.
if (!HasComp<PredictedBatteryVisualsComponent>(ent))
return;
// Set the appearance to empty.
_appearance.SetData(ent.Owner, BatteryVisuals.State, BatteryState.Empty);
_appearance.SetData(ent.Owner, BatteryVisuals.Charging, BatteryChargingState.Constant);
}
private void OnCellSlotStateChanged(Entity<PowerCellSlotComponent> ent, ref PredictedBatteryStateChangedEvent args)
{
if (args.NewState != BatteryState.Empty)
return;
// Inform the device that the battery is empty.
var ev = new PowerCellSlotEmptyEvent();
RaiseLocalEvent(ent, ref ev);
}
private void OnCellSlotExamined(Entity<PowerCellSlotComponent> ent, ref ExaminedEvent args)
{
if (TryGetBatteryFromSlot(ent.AsNullable(), out var battery))
OnBatteryExamined(battery.Value, ref args);
else
args.PushMarkup(Loc.GetString("power-cell-component-examine-details-no-battery"));
}
private void OnCellExamined(Entity<PowerCellComponent> ent, ref ExaminedEvent args)
{
if (TryComp<PredictedBatteryComponent>(ent, out var battery))
OnBatteryExamined((ent.Owner, battery), ref args);
}
private void OnBatteryExamined(Entity<PredictedBatteryComponent> ent, ref ExaminedEvent args)
{
var charge = _battery.GetCharge(ent.AsNullable()) / ent.Comp.MaxCharge * 100;
args.PushMarkup(Loc.GetString("power-cell-component-examine-details", ("currentCharge", $"{charge:F0}")));
}
private void OnDrawRefreshChargeRate(Entity<PowerCellDrawComponent> ent, ref RefreshChargeRateEvent args)
{
if (ent.Comp.Enabled)
args.NewChargeRate -= ent.Comp.DrawRate;
}
private void OnDrawStartup(Entity<PowerCellDrawComponent> ent, ref ComponentStartup args)
{
if (ent.Comp.Enabled)
_battery.RefreshChargeRate(ent.Owner);
}
private void OnDrawRemove(Entity<PowerCellDrawComponent> ent, ref ComponentRemove args)
{
// We use ComponentRemove to make sure this component no longer subscribes to the refresh event.
if (ent.Comp.Enabled)
_battery.RefreshChargeRate(ent.Owner);
}
}

View File

@ -1,125 +0,0 @@
using Content.Shared._DV.Silicons;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Emp;
using Content.Shared.PowerCell.Components;
using Content.Shared.Rejuvenate;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
namespace Content.Shared.PowerCell;
public abstract class SharedPowerCellSystem : EntitySystem
{
[Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedSiliconEmpSystem _siliconEmp = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PowerCellDrawComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<PowerCellSlotComponent, RejuvenateEvent>(OnRejuvenate);
SubscribeLocalEvent<PowerCellSlotComponent, EntInsertedIntoContainerMessage>(OnCellInserted);
SubscribeLocalEvent<PowerCellSlotComponent, EntRemovedFromContainerMessage>(OnCellRemoved);
SubscribeLocalEvent<PowerCellSlotComponent, ContainerIsInsertingAttemptEvent>(OnCellInsertAttempt);
SubscribeLocalEvent<PowerCellComponent, EmpAttemptEvent>(OnCellEmpAttempt);
}
private void OnMapInit(Entity<PowerCellDrawComponent> ent, ref MapInitEvent args)
{
ent.Comp.NextUpdateTime = Timing.CurTime + ent.Comp.Delay;
}
private void OnRejuvenate(EntityUid uid, PowerCellSlotComponent component, RejuvenateEvent args)
{
if (!_itemSlots.TryGetSlot(uid, component.CellSlotId, out var itemSlot) || !itemSlot.Item.HasValue)
return;
// charge entity batteries and remove booby traps.
RaiseLocalEvent(itemSlot.Item.Value, args);
}
private void OnCellInsertAttempt(EntityUid uid, PowerCellSlotComponent component, ContainerIsInsertingAttemptEvent args)
{
if (!component.Initialized)
return;
if (args.Container.ID != component.CellSlotId)
return;
if (!HasComp<PowerCellComponent>(args.EntityUid))
{
args.Cancel();
}
}
private void OnCellInserted(EntityUid uid, PowerCellSlotComponent component, EntInsertedIntoContainerMessage args)
{
if (!component.Initialized)
return;
if (args.Container.ID != component.CellSlotId)
return;
_appearance.SetData(uid, PowerCellSlotVisuals.Enabled, true);
RaiseLocalEvent(uid, new PowerCellChangedEvent(false), false);
}
protected virtual void OnCellRemoved(EntityUid uid, PowerCellSlotComponent component, EntRemovedFromContainerMessage args)
{
if (args.Container.ID != component.CellSlotId)
return;
_appearance.SetData(uid, PowerCellSlotVisuals.Enabled, false);
RaiseLocalEvent(uid, new PowerCellChangedEvent(true), false);
}
private void OnCellEmpAttempt(Entity<PowerCellComponent> entity, ref EmpAttemptEvent args)
{
var parent = Transform(entity).ParentUid;
if (_siliconEmp.ShouldTakeDamageInsteadOfPowerDrain(parent)) // DeltaV - Silicon EMP
return;
// relay the attempt event to the slot so it can cancel it
if (HasComp<PowerCellSlotComponent>(parent))
RaiseLocalEvent(parent, ref args);
}
public void SetDrawEnabled(Entity<PowerCellDrawComponent?> ent, bool enabled)
{
if (!Resolve(ent, ref ent.Comp, false) || ent.Comp.Enabled == enabled)
return;
if (enabled)
ent.Comp.NextUpdateTime = Timing.CurTime;
ent.Comp.Enabled = enabled;
Dirty(ent, ent.Comp);
}
/// <summary>
/// Returns whether the entity has a slotted battery and <see cref="PowerCellDrawComponent.UseRate"/> charge.
/// </summary>
/// <param name="uid"></param>
/// <param name="battery"></param>
/// <param name="cell"></param>
/// <param name="user">Popup to this user with the relevant detail if specified.</param>
public abstract bool HasActivatableCharge(
EntityUid uid,
PowerCellDrawComponent? battery = null,
PowerCellSlotComponent? cell = null,
EntityUid? user = null);
/// <summary>
/// Whether the power cell has any power at all for the draw rate.
/// </summary>
public abstract bool HasDrawCharge(
EntityUid uid,
PowerCellDrawComponent? battery = null,
PowerCellSlotComponent? cell = null,
EntityUid? user = null);
}

View File

@ -10,7 +10,7 @@ namespace Content.Shared.PowerCell;
public sealed class ToggleCellDrawSystem : EntitySystem public sealed class ToggleCellDrawSystem : EntitySystem
{ {
[Dependency] private readonly ItemToggleSystem _toggle = default!; [Dependency] private readonly ItemToggleSystem _toggle = default!;
[Dependency] private readonly SharedPowerCellSystem _cell = default!; [Dependency] private readonly PowerCellSystem _cell = default!;
public override void Initialize() public override void Initialize()
{ {
@ -29,8 +29,8 @@ public sealed class ToggleCellDrawSystem : EntitySystem
private void OnActivateAttempt(Entity<ToggleCellDrawComponent> ent, ref ItemToggleActivateAttemptEvent args) private void OnActivateAttempt(Entity<ToggleCellDrawComponent> ent, ref ItemToggleActivateAttemptEvent args)
{ {
if (!_cell.HasDrawCharge(ent, user: args.User) if (!_cell.HasDrawCharge(ent.Owner, user: args.User, predicted: true)
|| !_cell.HasActivatableCharge(ent, user: args.User)) || !_cell.HasActivatableCharge(ent.Owner, user: args.User, predicted: true))
args.Cancelled = true; args.Cancelled = true;
} }

View File

@ -4,6 +4,7 @@ using Robust.Shared.Containers;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Silicons.Borgs.Components; namespace Content.Shared.Silicons.Borgs.Components;
@ -12,7 +13,8 @@ namespace Content.Shared.Silicons.Borgs.Components;
/// "brain", legs, modules, and battery. Essentially the master component /// "brain", legs, modules, and battery. Essentially the master component
/// for borg logic. /// for borg logic.
/// </summary> /// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem)), AutoGenerateComponentState] [RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem))]
[AutoGenerateComponentState, AutoGenerateComponentPause]
public sealed partial class BorgChassisComponent : Component public sealed partial class BorgChassisComponent : Component
{ {
#region Brain #region Brain
@ -79,6 +81,14 @@ public sealed partial class BorgChassisComponent : Component
[DataField] [DataField]
public ProtoId<AlertPrototype> NoBatteryAlert = "BorgBatteryNone"; public ProtoId<AlertPrototype> NoBatteryAlert = "BorgBatteryNone";
/// <summary>
/// The next update time for the battery charge level.
/// Used for the alert and borg UI.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan NextBatteryUpdate = TimeSpan.Zero;
/// <summary> /// <summary>
/// If the entity can open own UI. /// If the entity can open own UI.
/// </summary> /// </summary>

View File

@ -158,13 +158,13 @@ public sealed class EntityStorageComponentState : ComponentState
/// Raised on the entity being inserted whenever checking if an entity can be inserted into an entity storage. /// Raised on the entity being inserted whenever checking if an entity can be inserted into an entity storage.
/// </summary> /// </summary>
[ByRefEvent] [ByRefEvent]
public record struct InsertIntoEntityStorageAttemptEvent(EntityUid ItemToInsert, bool Cancelled = false); public record struct InsertIntoEntityStorageAttemptEvent(BaseContainer Container, EntityUid ItemToInsert, bool Cancelled = false);
/// <summary> /// <summary>
/// Raised on the entity storage whenever checking if an entity can be inserted into it. /// Raised on the entity storage whenever checking if an entity can be inserted into it.
/// </summary> /// </summary>
[ByRefEvent] [ByRefEvent]
public record struct EntityStorageInsertedIntoAttemptEvent(EntityUid ItemToInsert, bool Cancelled = false); public record struct EntityStorageInsertedIntoAttemptEvent(BaseContainer Container, EntityUid ItemToInsert, bool Cancelled = false);
/// <summary> /// <summary>
/// Raised on the Container's owner whenever an entity storage tries to dump its /// Raised on the Container's owner whenever an entity storage tries to dump its

View File

@ -341,14 +341,14 @@ public abstract class SharedEntityStorageSystem : EntitySystem
return false; return false;
// Allow other systems to prevent inserting the item: e.g. the item is actually a ghost. // Allow other systems to prevent inserting the item: e.g. the item is actually a ghost.
var attemptEvent = new InsertIntoEntityStorageAttemptEvent(toInsert); var attemptEvent = new InsertIntoEntityStorageAttemptEvent(component.Contents, toInsert);
RaiseLocalEvent(toInsert, ref attemptEvent); RaiseLocalEvent(toInsert, ref attemptEvent);
if (attemptEvent.Cancelled) if (attemptEvent.Cancelled)
return false; return false;
// Allow other components on the container to prevent inserting the item: e.g. the container is folded // Allow other components on the container to prevent inserting the item: e.g. the container is folded
var containerAttemptEvent = new EntityStorageInsertedIntoAttemptEvent(toInsert); var containerAttemptEvent = new EntityStorageInsertedIntoAttemptEvent(component.Contents, toInsert);
RaiseLocalEvent(container, ref containerAttemptEvent); RaiseLocalEvent(container, ref containerAttemptEvent);
if (containerAttemptEvent.Cancelled) if (containerAttemptEvent.Cancelled)

View File

@ -1,4 +1,3 @@
using Content.Server.Stunnable.Components;
using Content.Shared.ActionBlocker; using Content.Shared.ActionBlocker;
using Content.Shared.Item.ItemToggle.Components; using Content.Shared.Item.ItemToggle.Components;

View File

@ -1,13 +1,10 @@
using Content.Shared.PowerCell; using Content.Shared.PowerCell.Components;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
namespace Content.Shared.UserInterface; namespace Content.Shared.UserInterface;
/// <summary> /// <summary>
/// Specifies that the attached entity requires <see cref="PowerCellDrawComponent"/> power. /// Specifies that the attached entity requires <see cref="PowerCellDrawComponent"/> power to open the activatable UI.
/// </summary> /// </summary>
[RegisterComponent, NetworkedComponent] [RegisterComponent, NetworkedComponent]
public sealed partial class ActivatableUIRequiresPowerCellComponent : Component public sealed partial class ActivatableUIRequiresPowerCellComponent : Component;
{
}

View File

@ -1,27 +1,29 @@
using Content.Shared.Item.ItemToggle; using Content.Shared.Item.ItemToggle;
using Content.Shared.Item.ItemToggle.Components; using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.PowerCell; using Content.Shared.PowerCell;
using Robust.Shared.Containers; using Content.Shared.Power;
using Content.Shared.Power.Components;
namespace Content.Shared.UserInterface; namespace Content.Shared.UserInterface;
public sealed partial class ActivatableUISystem public sealed partial class ActivatableUISystem
{ {
[Dependency] private readonly ItemToggleSystem _toggle = default!; [Dependency] private readonly ItemToggleSystem _toggle = default!;
[Dependency] private readonly SharedPowerCellSystem _cell = default!; [Dependency] private readonly PowerCellSystem _cell = default!;
private void InitializePower() private void InitializePower()
{ {
SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, ActivatableUIOpenAttemptEvent>(OnBatteryOpenAttempt); SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, ItemToggledEvent>(OnToggled);
SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, BoundUIOpenedEvent>(OnBatteryOpened); SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, BoundUIOpenedEvent>(OnBatteryOpened);
SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, BoundUIClosedEvent>(OnBatteryClosed); SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, BoundUIClosedEvent>(OnBatteryClosed);
SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, ItemToggledEvent>(OnToggled); SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, PredictedBatteryStateChangedEvent>(OnBatteryStateChanged);
SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, ActivatableUIOpenAttemptEvent>(OnBatteryOpenAttempt);
} }
private void OnToggled(Entity<ActivatableUIRequiresPowerCellComponent> ent, ref ItemToggledEvent args) private void OnToggled(Entity<ActivatableUIRequiresPowerCellComponent> ent, ref ItemToggledEvent args)
{ {
// only close ui when losing power // only close ui when losing power
if (!TryComp<ActivatableUIComponent>(ent, out var activatable) || args.Activated) if (args.Activated || !TryComp<ActivatableUIComponent>(ent, out var activatable))
return; return;
if (activatable.Key == null) if (activatable.Key == null)
@ -55,35 +57,25 @@ public sealed partial class ActivatableUISystem
_toggle.TryDeactivate(uid); _toggle.TryDeactivate(uid);
} }
/// <summary> private void OnBatteryStateChanged(Entity<ActivatableUIRequiresPowerCellComponent> ent, ref PredictedBatteryStateChangedEvent args)
/// Call if you want to check if the UI should close due to a recent battery usage.
/// </summary>
public void CheckUsage(EntityUid uid, ActivatableUIComponent? active = null, ActivatableUIRequiresPowerCellComponent? component = null, PowerCellDrawComponent? draw = null)
{ {
if (!Resolve(uid, ref component, ref draw, ref active, false)) // Deactivate when empty.
if (args.NewState != BatteryState.Empty)
return; return;
if (active.Key == null) var activatable = Comp<ActivatableUIComponent>(ent);
{ if (activatable.Key != null)
Log.Error($"Encountered null key in activatable ui on entity {ToPrettyString(uid)}"); _uiSystem.CloseUi(ent.Owner, activatable.Key);
return;
}
if (_cell.HasActivatableCharge(uid))
return;
_uiSystem.CloseUi(uid, active.Key);
} }
private void OnBatteryOpenAttempt(EntityUid uid, ActivatableUIRequiresPowerCellComponent component, ActivatableUIOpenAttemptEvent args) private void OnBatteryOpenAttempt(EntityUid uid, ActivatableUIRequiresPowerCellComponent component, ActivatableUIOpenAttemptEvent args)
{ {
if (!TryComp<PowerCellDrawComponent>(uid, out var draw)) if (args.Cancelled)
return; return;
// Check if we have the appropriate drawrate / userate to even open it. // Check if we have the appropriate drawrate / userate to even open it.
if (args.Cancelled || if (!_cell.HasActivatableCharge(uid, user: args.User, predicted: true) ||
!_cell.HasActivatableCharge(uid, draw, user: args.User) || !_cell.HasDrawCharge(uid, user: args.User, predicted: true))
!_cell.HasDrawCharge(uid, draw, user: args.User))
{ {
args.Cancel(); args.Cancel();
} }

View File

@ -1,18 +1,70 @@
using Content.Shared.Power.Components;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Weapons.Ranged.Components; namespace Content.Shared.Weapons.Ranged.Components;
public abstract partial class BatteryAmmoProviderComponent : AmmoProviderComponent /// <summary>
/// Ammo provider that uses electric charge from a battery to provide ammunition to a weapon.
/// This works with both <see cref="BatteryComponent"/> and <see cref="PredictedBatteryComponent"/>
/// </summary>
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState(raiseAfterAutoHandleState: true), AutoGenerateComponentPause]
public sealed partial class BatteryAmmoProviderComponent : AmmoProviderComponent
{ {
/// <summary> /// <summary>
/// How much battery it costs to fire once. /// The projectile or hitscan entity to spawn when firing.
/// </summary> /// </summary>
[DataField("fireCost")] // Shitmed Change [DataField("proto", required: true)]
public EntProtoId Prototype;
/// <summary>
/// How much charge it costs to fire once, in watts.
/// </summary>
[DataField, AutoNetworkedField]
public float FireCost = 100; public float FireCost = 100;
// Batteries aren't predicted which means we need to track the battery and manually count it ourselves woo! /// <summary>
/// Timestamp for the next update for the shot counter and visuals.
/// This is the expected time at which the next integer will be reached.
/// Null if the charge rate is 0, meaning the shot amount is constant.
/// Only used for predicted batteries.
/// </summary>
/// <remarks>
/// Not a datafield since this is refreshed along with the battery's charge rate anyways.
/// </remarks>
[ViewVariables, AutoNetworkedField, AutoPausedField]
public TimeSpan? NextUpdate;
[ViewVariables(VVAccess.ReadWrite)] /// <summary>
/// The time between reaching full charges at the current charge rate.
/// Only used for predicted batteries.
/// </summary>
/// <remarks>
/// Not a datafield since this is refreshed along with the battery's charge rate anyways.
/// </remarks>
[ViewVariables, AutoNetworkedField]
public TimeSpan ChargeTime = TimeSpan.Zero;
/// <summary>
/// The current amount of available shots.
/// BatteryComponent is not predicted, so we need to manually network this for the ammo indicator and examination text.
/// </summary>
/// <remarks>
/// Not a datafield since this is only cached and refreshed on component startup.
/// TODO: If we ever fully predict all batteries then remove this and just read the charge on the client.
/// </remarks>
[ViewVariables, AutoNetworkedField]
public int Shots; public int Shots;
[ViewVariables(VVAccess.ReadWrite)] /// <summary>
/// The maximum amount of available shots.
/// BatteryComponent is not predicted, so we need to manually network this for the ammo indicator and examination text.
/// </summary>
/// <remarks>
/// Not a datafield since this is only cached and refreshed on component startup.
/// TODO: If we ever fully predict all batteries then remove this and just read the charge on the client.
/// </remarks>
[ViewVariables, AutoNetworkedField]
public int Capacity; public int Capacity;
} }

View File

@ -1,11 +0,0 @@
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Weapons.Ranged.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class HitscanBatteryAmmoProviderComponent : BatteryAmmoProviderComponent
{
[DataField("proto", required: true)]
public EntProtoId HitscanEntityProto;
}

View File

@ -1,12 +0,0 @@
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Weapons.Ranged.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class ProjectileBatteryAmmoProviderComponent : BatteryAmmoProviderComponent
{
[ViewVariables(VVAccess.ReadWrite), DataField("proto", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Prototype = default!;
}

View File

@ -1,4 +0,0 @@
namespace Content.Shared.Weapons.Ranged.Events;
[ByRefEvent]
public readonly record struct UpdateClientAmmoEvent();

View File

@ -17,6 +17,7 @@ public sealed class BatteryWeaponFireModesSystem : EntitySystem
[Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!; [Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly SharedGunSystem _gun = default!;
public override void Initialize() public override void Initialize()
{ {
@ -126,21 +127,14 @@ public sealed class BatteryWeaponFireModesSystem : EntitySystem
_popupSystem.PopupClient(Loc.GetString("gun-set-fire-mode", ("mode", prototype.Name)), uid, user.Value); _popupSystem.PopupClient(Loc.GetString("gun-set-fire-mode", ("mode", prototype.Name)), uid, user.Value);
} }
if (TryComp(uid, out ProjectileBatteryAmmoProviderComponent? projectileBatteryAmmoProviderComponent)) if (TryComp(uid, out BatteryAmmoProviderComponent? batteryAmmoProviderComponent))
{ {
// TODO: Have this get the info directly from the batteryComponent when power is moved to shared. batteryAmmoProviderComponent.Prototype = fireMode.Prototype;
var OldFireCost = projectileBatteryAmmoProviderComponent.FireCost; batteryAmmoProviderComponent.FireCost = fireMode.FireCost;
projectileBatteryAmmoProviderComponent.Prototype = fireMode.Prototype;
projectileBatteryAmmoProviderComponent.FireCost = fireMode.FireCost;
float FireCostDiff = (float)fireMode.FireCost / (float)OldFireCost; Dirty(uid, batteryAmmoProviderComponent);
projectileBatteryAmmoProviderComponent.Shots = (int)Math.Round(projectileBatteryAmmoProviderComponent.Shots / FireCostDiff);
projectileBatteryAmmoProviderComponent.Capacity = (int)Math.Round(projectileBatteryAmmoProviderComponent.Capacity / FireCostDiff);
Dirty(uid, projectileBatteryAmmoProviderComponent); _gun.UpdateShots((uid, batteryAmmoProviderComponent));
var updateClientAmmoEvent = new UpdateClientAmmoEvent();
RaiseLocalEvent(uid, ref updateClientAmmoEvent);
} }
} }
} }

View File

@ -2,13 +2,13 @@ using Content.Shared.Damage;
using Content.Shared.Damage.Events; using Content.Shared.Damage.Events;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.Projectiles; using Content.Shared.Projectiles;
using Content.Shared.Power;
using Content.Shared.PowerCell;
using Content.Shared.Weapons.Hitscan.Components; using Content.Shared.Weapons.Hitscan.Components;
using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events; using Content.Shared.Weapons.Ranged.Events;
using Robust.Shared.GameStates;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Weapons.Ranged.Systems; namespace Content.Shared.Weapons.Ranged.Systems;
@ -16,169 +16,202 @@ public abstract partial class SharedGunSystem
{ {
protected virtual void InitializeBattery() protected virtual void InitializeBattery()
{ {
// Trying to dump comp references hence the below SubscribeLocalEvent<BatteryAmmoProviderComponent, ComponentStartup>(OnBatteryStartup);
// Hitscan SubscribeLocalEvent<BatteryAmmoProviderComponent, AfterAutoHandleStateEvent>(OnAfterAutoHandleState);
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, ComponentGetState>(OnBatteryGetState); SubscribeLocalEvent<BatteryAmmoProviderComponent, TakeAmmoEvent>(OnBatteryTakeAmmo);
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, ComponentHandleState>(OnBatteryHandleState); SubscribeLocalEvent<BatteryAmmoProviderComponent, GetAmmoCountEvent>(OnBatteryAmmoCount);
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, TakeAmmoEvent>(OnBatteryTakeAmmo); SubscribeLocalEvent<BatteryAmmoProviderComponent, ExaminedEvent>(OnBatteryExamine);
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, GetAmmoCountEvent>(OnBatteryAmmoCount); SubscribeLocalEvent<BatteryAmmoProviderComponent, DamageExamineEvent>(OnBatteryDamageExamine);
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, ExaminedEvent>(OnBatteryExamine); SubscribeLocalEvent<BatteryAmmoProviderComponent, PowerCellChangedEvent>(OnPowerCellChanged);
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, DamageExamineEvent>(OnBatteryDamageExamine); SubscribeLocalEvent<BatteryAmmoProviderComponent, PredictedBatteryChargeChangedEvent>(OnPredictedChargeChanged);
SubscribeLocalEvent<BatteryAmmoProviderComponent, ChargeChangedEvent>(OnChargeChanged);
// Projectile
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, ComponentGetState>(OnBatteryGetState);
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, ComponentHandleState>(OnBatteryHandleState);
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, TakeAmmoEvent>(OnBatteryTakeAmmo);
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, GetAmmoCountEvent>(OnBatteryAmmoCount);
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, ExaminedEvent>(OnBatteryExamine);
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, DamageExamineEvent>(OnBatteryDamageExamine);
} }
private void OnBatteryHandleState(EntityUid uid, BatteryAmmoProviderComponent component, ref ComponentHandleState args) private void OnBatteryExamine(Entity<BatteryAmmoProviderComponent> ent, ref ExaminedEvent args)
{ {
if (args.Current is not BatteryAmmoProviderComponentState state) args.PushMarkup(Loc.GetString("gun-battery-examine", ("color", AmmoExamineColor), ("count", ent.Comp.Shots)));
return;
component.Shots = state.Shots;
component.Capacity = state.MaxShots;
component.FireCost = state.FireCost;
UpdateAmmoCount(uid, prediction: false);
if (component is HitscanBatteryAmmoProviderComponent hitscan && state.Prototype is { } proto) // Shitmed Change
hitscan.HitscanEntityProto = proto;
} }
private void OnBatteryGetState(EntityUid uid, BatteryAmmoProviderComponent component, ref ComponentGetState args) private void OnBatteryDamageExamine(Entity<BatteryAmmoProviderComponent> ent, ref DamageExamineEvent args)
{ {
var state = new BatteryAmmoProviderComponentState() // Shitmed Change var proto = ProtoManager.Index<EntityPrototype>(ent.Comp.Prototype);
DamageSpecifier? damageSpec = null;
var damageType = string.Empty;
if (proto.TryGetComponent<ProjectileComponent>(out var projectileComp, Factory))
{ {
Shots = component.Shots, if (!projectileComp.Damage.Empty)
MaxShots = component.Capacity, {
FireCost = component.FireCost, damageType = Loc.GetString("damage-projectile");
}; damageSpec = projectileComp.Damage * Damageable.UniversalProjectileDamageModifier;
if (TryComp<HitscanBatteryAmmoProviderComponent>(uid, out var hitscan)) // Shitmed Change
state.Prototype = hitscan.HitscanEntityProto;
args.State = state; // Shitmed Change
} }
private void OnBatteryExamine(EntityUid uid, BatteryAmmoProviderComponent component, ExaminedEvent args)
{
args.PushMarkup(Loc.GetString("gun-battery-examine", ("color", AmmoExamineColor), ("count", component.Shots)));
} }
else if (proto.TryGetComponent<HitscanBasicDamageComponent>(out var hitscanComp, Factory))
private void OnBatteryDamageExamine<T>(Entity<T> entity, ref DamageExamineEvent args) where T : BatteryAmmoProviderComponent
{ {
var damageSpec = GetDamage(entity.Comp); if (!hitscanComp.Damage.Empty)
{
damageType = Loc.GetString("damage-hitscan");
damageSpec = hitscanComp.Damage * Damageable.UniversalHitscanDamageModifier;
}
}
if (damageSpec == null) if (damageSpec == null)
return; return;
var damageType = entity.Comp switch
{
HitscanBatteryAmmoProviderComponent => Loc.GetString("damage-hitscan"),
ProjectileBatteryAmmoProviderComponent => Loc.GetString("damage-projectile"),
_ => throw new ArgumentOutOfRangeException(),
};
_damageExamine.AddDamageExamine(args.Message, Damageable.ApplyUniversalAllModifiers(damageSpec), damageType); _damageExamine.AddDamageExamine(args.Message, Damageable.ApplyUniversalAllModifiers(damageSpec), damageType);
} }
private DamageSpecifier? GetDamage(BatteryAmmoProviderComponent component) private void OnBatteryTakeAmmo(Entity<BatteryAmmoProviderComponent> ent, ref TakeAmmoEvent args)
{ {
if (component is ProjectileBatteryAmmoProviderComponent battery) var shots = Math.Min(args.Shots, ent.Comp.Shots);
{
if (ProtoManager.Index<EntityPrototype>(battery.Prototype).Components
.TryGetValue(Factory.GetComponentName<ProjectileComponent>(), out var projectile))
{
var p = (ProjectileComponent) projectile.Component;
if (!p.Damage.Empty)
{
return p.Damage * Damageable.UniversalProjectileDamageModifier;
}
}
return null;
}
if (component is HitscanBatteryAmmoProviderComponent hitscan)
{
var dmg = ProtoManager.Index(hitscan.HitscanEntityProto);
if (!dmg.TryGetComponent<HitscanBasicDamageComponent>(out var basicDamageComp, Factory))
return null;
return basicDamageComp.Damage * Damageable.UniversalHitscanDamageModifier;
}
return null;
}
private void OnBatteryTakeAmmo(EntityUid uid, BatteryAmmoProviderComponent component, TakeAmmoEvent args)
{
var shots = Math.Min(args.Shots, component.Shots);
// Don't dirty if it's an empty fire.
if (shots == 0) if (shots == 0)
return; return;
for (var i = 0; i < shots; i++) for (var i = 0; i < shots; i++)
{ {
args.Ammo.Add(GetShootable(component, args.Coordinates)); args.Ammo.Add(GetShootable(ent, args.Coordinates));
component.Shots--;
} }
TakeCharge((uid, component)); TakeCharge(ent, shots);
UpdateBatteryAppearance(uid, component);
Dirty(uid, component);
} }
private void OnBatteryAmmoCount(EntityUid uid, BatteryAmmoProviderComponent component, ref GetAmmoCountEvent args) private void OnBatteryAmmoCount(Entity<BatteryAmmoProviderComponent> ent, ref GetAmmoCountEvent args)
{ {
args.Count = component.Shots; args.Count = ent.Comp.Shots;
args.Capacity = component.Capacity; args.Capacity = ent.Comp.Capacity;
} }
/// <summary> /// <summary>
/// Update the battery (server-only) whenever fired. /// Use up the required amount of battery charge for firing.
/// </summary> /// </summary>
protected virtual void TakeCharge(Entity<BatteryAmmoProviderComponent> entity) public void TakeCharge(Entity<BatteryAmmoProviderComponent> ent, int shots = 1)
{ {
UpdateAmmoCount(entity, prediction: false); // Take charge from either the BatteryComponent, PredictedBatteryComponent or PowerCellSlotComponent.
} var ev = new ChangeChargeEvent(-ent.Comp.FireCost * shots);
RaiseLocalEvent(ent, ref ev);
protected void UpdateBatteryAppearance(EntityUid uid, BatteryAmmoProviderComponent component) // UpdateShots is already called by the resulting PredictedBatteryChargeChangedEvent or ChargeChangedEvent
{
if (!TryComp<AppearanceComponent>(uid, out var appearance))
return;
Appearance.SetData(uid, AmmoVisuals.HasAmmo, component.Shots != 0, appearance);
Appearance.SetData(uid, AmmoVisuals.AmmoCount, component.Shots, appearance);
Appearance.SetData(uid, AmmoVisuals.AmmoMax, component.Capacity, appearance);
} }
private (EntityUid? Entity, IShootable) GetShootable(BatteryAmmoProviderComponent component, EntityCoordinates coordinates) private (EntityUid? Entity, IShootable) GetShootable(BatteryAmmoProviderComponent component, EntityCoordinates coordinates)
{ {
switch (component)
{ var ent = Spawn(component.Prototype, coordinates);
case ProjectileBatteryAmmoProviderComponent proj:
var ent = Spawn(proj.Prototype, coordinates);
return (ent, EnsureShootable(ent)); return (ent, EnsureShootable(ent));
case HitscanBatteryAmmoProviderComponent hitscan:
var hitscanEnt = Spawn(hitscan.HitscanEntityProto);
return (hitscanEnt, EnsureShootable(hitscanEnt));
default:
throw new ArgumentOutOfRangeException();
}
} }
[Serializable, NetSerializable] public void UpdateShots(Entity<BatteryAmmoProviderComponent> ent)
private sealed class BatteryAmmoProviderComponentState : ComponentState
{ {
public int Shots; var oldShots = ent.Comp.Shots;
public int MaxShots; var oldCapacity = ent.Comp.Capacity;
public float FireCost; (var newShots, var newCapacity) = GetShots(ent);
public EntProtoId? Prototype; // Shitmed Change
// Only dirty if necessary.
if (oldShots == newShots && oldCapacity == newCapacity)
return;
ent.Comp.Shots = newShots;
if (newCapacity > 0) // Don't make the capacity 0 when removing a power cell as this will make it be visualized as full instead of empty.
ent.Comp.Capacity = newCapacity;
// Update the ammo counter predictively if the change was predicted. On the server this does nothing.
UpdateAmmoCount(ent.Owner);
Dirty(ent); // Dirtying will update the client's ammo counter in an AfterAutoHandleStateEvent subscription in case it was not predicted.
if (!TryComp<AppearanceComponent>(ent, out var appearance))
return;
// Update the visuals.
Appearance.SetData(ent.Owner, AmmoVisuals.HasAmmo, newShots != 0, appearance);
Appearance.SetData(ent.Owner, AmmoVisuals.AmmoCount, newShots, appearance);
if (newCapacity > 0) // Don't make the capacity 0 when removing a power cell as this will make it be visualized as full instead of empty.
Appearance.SetData(ent.Owner, AmmoVisuals.AmmoMax, newCapacity, appearance);
}
// For server side changes the client's ammo counter needs to be updated as well.
private void OnAfterAutoHandleState(Entity<BatteryAmmoProviderComponent> ent, ref AfterAutoHandleStateEvent args)
{
UpdateAmmoCount(ent); // Need to have prediction set to true because the state is applied repeatedly while prediction is running.
}
// For when a power cell gets inserted or removed.
private void OnPowerCellChanged(Entity<BatteryAmmoProviderComponent> ent, ref PowerCellChangedEvent args)
{
UpdateShots(ent);
}
// For predicted batteries.
// If the entity is has a PowerCellSlotComponent then this event is relayed from the power cell to the slot entity.
private void OnPredictedChargeChanged(Entity<BatteryAmmoProviderComponent> ent, ref PredictedBatteryChargeChangedEvent args)
{
// Update the visuals and charge counter UI.
UpdateShots(ent);
// Queue the update for when the autorecharge reaches enough charge for another shot.
UpdateNextUpdate(ent, args.CurrentCharge, args.MaxCharge, args.CurrentChargeRate);
}
// For unpredicted batteries.
private void OnChargeChanged(Entity<BatteryAmmoProviderComponent> ent, ref ChargeChangedEvent args)
{
// Update the visuals and charge counter UI.
UpdateShots(ent);
// No need to queue an update here since unpredicted batteries already update periodically as they charge/discharge.
}
private void UpdateNextUpdate(Entity<BatteryAmmoProviderComponent> ent, float currentCharge, float maxCharge, float currentChargeRate)
{
// Don't queue any updates if charge is constant.
ent.Comp.NextUpdate = null;
// ETA of the next full charge.
if (currentChargeRate > 0f && currentCharge != maxCharge)
{
ent.Comp.NextUpdate = Timing.CurTime + TimeSpan.FromSeconds((ent.Comp.FireCost - (currentCharge % ent.Comp.FireCost)) / currentChargeRate);
ent.Comp.ChargeTime = TimeSpan.FromSeconds(ent.Comp.FireCost / currentChargeRate);
}
else if (currentChargeRate < 0f && currentCharge != 0f)
{
ent.Comp.NextUpdate = Timing.CurTime + TimeSpan.FromSeconds(-(currentCharge % ent.Comp.FireCost) / currentChargeRate);
ent.Comp.ChargeTime = TimeSpan.FromSeconds(-ent.Comp.FireCost / currentChargeRate);
}
Dirty(ent);
}
// Shots are only chached, not a DataField, so we need to refresh this when the game is loaded.
private void OnBatteryStartup(Entity<BatteryAmmoProviderComponent> ent, ref ComponentStartup args)
{
UpdateShots(ent);
}
/// <summary>
/// Gets the current and maximum amount of shots from this entity's battery.
/// This works for BatteryComponent, PredictedBatteryComponent and PowercellSlotComponent.
/// </summary>
public (int, int) GetShots(Entity<BatteryAmmoProviderComponent> ent)
{
var ev = new GetChargeEvent();
RaiseLocalEvent(ent, ref ev);
var currentShots = (int)(ev.CurrentCharge / ent.Comp.FireCost);
var maxShots = (int)(ev.MaxCharge / ent.Comp.FireCost);
return (currentShots, maxShots);
}
/// <summary>
/// Update loop for refreshing the ammo counter for charging/draining predicted batteries.
/// This is not needed for unpredicted batteries since those already raise ChargeChangedEvent periodically.
/// </summary>
private void UpdateBattery(float frameTime)
{
var curTime = Timing.CurTime;
var hitscanQuery = EntityQueryEnumerator<BatteryAmmoProviderComponent>();
while (hitscanQuery.MoveNext(out var uid, out var provider))
{
if (provider.NextUpdate == null || curTime < provider.NextUpdate)
continue;
UpdateShots((uid, provider));
provider.NextUpdate += provider.ChargeTime; // Queue another update for when we reach the next full charge.
Dirty(uid, provider);
// TODO: Stop updating when full or empty.
}
} }
} }

View File

@ -678,6 +678,11 @@ public abstract partial class SharedGunSystem : EntitySystem
RaiseLocalEvent(uid, ref ammoEv); RaiseLocalEvent(uid, ref ammoEv);
return ammoEv.Capacity; return ammoEv.Capacity;
} }
public override void Update(float frameTime)
{
UpdateBattery(frameTime);
}
} }
/// <summary> /// <summary>

View File

@ -532,7 +532,7 @@
amount: 6 amount: 6
whitelist: whitelist:
components: components:
- HitscanBatteryAmmoProvider - BatteryAmmoProvider
blacklist: blacklist:
components: components:
- PowerCell - PowerCell

View File

@ -633,7 +633,7 @@
damage: 35 damage: 35
sound: /Audio/Weapons/egloves.ogg sound: /Audio/Weapons/egloves.ogg
- type: LandAtCursor # it deals stamina damage when thrown - type: LandAtCursor # it deals stamina damage when thrown
- type: Battery - type: PredictedBattery
maxCharge: 1000 maxCharge: 1000
startingCharge: 1000 startingCharge: 1000
- type: GuideHelp - type: GuideHelp

View File

@ -241,10 +241,10 @@
startValue: 0.1 startValue: 0.1
endValue: 2.0 endValue: 2.0
isLooped: true isLooped: true
- type: Battery - type: PredictedBattery
maxCharge: 600 #lights drain 3/s but recharge of 2 makes this 1/s. Therefore 600 is 10 minutes of light. maxCharge: 600 #lights drain 3/s but recharge of 2 makes this 1/s. Therefore 600 is 10 minutes of light.
startingCharge: 600 startingCharge: 600
- type: BatterySelfRecharger - type: PredictedBatterySelfRecharger
autoRechargeRate: 2 #recharge of 2 makes total drain 1w / s so max charge is 1:1 with time. Time to fully charge should be 5 minutes. Having recharge gives light an extended flicker period which gives you some warning to return to light area. autoRechargeRate: 2 #recharge of 2 makes total drain 1w / s so max charge is 1:1 with time. Time to fully charge should be 5 minutes. Having recharge gives light an extended flicker period which gives you some warning to return to light area.
- type: entity - type: entity

View File

@ -306,10 +306,10 @@
startValue: 0.1 startValue: 0.1
endValue: 2.0 endValue: 2.0
isLooped: true isLooped: true
- type: Battery - type: PredictedBattery
maxCharge: 600 maxCharge: 600
startingCharge: 600 startingCharge: 600
- type: BatterySelfRecharger - type: PredictedBatterySelfRecharger
autoRechargeRate: 2 autoRechargeRate: 2
- type: Item - type: Item
size: Normal size: Normal

View File

@ -19,13 +19,13 @@
- type: HTN - type: HTN
rootTask: rootTask:
task: SimpleRangedHostileCompound task: SimpleRangedHostileCompound
- type: HitscanBatteryAmmoProvider - type: BatteryAmmoProvider
proto: RedLaser proto: RedLaser
fireCost: 62.5 fireCost: 62.5
- type: Battery - type: PredictedBattery
maxCharge: 1000 maxCharge: 1000
startingCharge: 1000 startingCharge: 1000
- type: BatterySelfRecharger - type: PredictedBatterySelfRecharger
autoRechargeRate: 40 autoRechargeRate: 40
- type: Gun - type: Gun
fireRate: 0.75 fireRate: 0.75

View File

@ -50,12 +50,12 @@
- type: MovementSpeedModifier - type: MovementSpeedModifier
baseWalkSpeed: 5 baseWalkSpeed: 5
baseSprintSpeed: 7 baseSprintSpeed: 7
- type: ProjectileBatteryAmmoProvider - type: BatteryAmmoProvider
proto: WatcherBolt proto: WatcherBolt
fireCost: 50 fireCost: 50
- type: BatterySelfRecharger - type: PredictedBatterySelfRecharger
autoRechargeRate: 50 autoRechargeRate: 50
- type: Battery - type: PredictedBattery
maxCharge: 1000 maxCharge: 1000
startingCharge: 1000 startingCharge: 1000
- type: Gun - type: Gun
@ -125,7 +125,7 @@
radius: 1 radius: 1
energy: 3 energy: 3
color: orangered color: orangered
- type: ProjectileBatteryAmmoProvider - type: BatteryAmmoProvider
proto: WatcherBoltMagmawing proto: WatcherBoltMagmawing
fireCost: 50 fireCost: 50

View File

@ -168,13 +168,13 @@
damage: damage:
types: types:
Heat: 5 Heat: 5
- type: HitscanBatteryAmmoProvider - type: BatteryAmmoProvider
proto: RedLaser proto: RedLaser
fireCost: 140 fireCost: 140
- type: Battery - type: PredictedBattery
maxCharge: 1000 maxCharge: 1000
startingCharge: 1000 startingCharge: 1000
- type: BatterySelfRecharger - type: PredictedBatterySelfRecharger
autoRechargeRate: 50 autoRechargeRate: 50
- type: Gun - type: Gun
fireRate: 0.3 fireRate: 0.3

View File

@ -49,12 +49,12 @@
- type: Tag - type: Tag
tags: tags:
- FootstepSound - FootstepSound
- type: HitscanBatteryAmmoProvider - type: BatteryAmmoProvider
proto: RedLightLaser proto: RedLightLaser
fireCost: 50 fireCost: 50
- type: BatterySelfRecharger - type: PredictedBatterySelfRecharger
autoRechargeRate: 50 autoRechargeRate: 50
- type: Battery - type: PredictedBattery
maxCharge: 1000 maxCharge: 1000
startingCharge: 1000 startingCharge: 1000
- type: Gun - type: Gun

View File

@ -8,5 +8,5 @@
onUse: false # above component does the toggling onUse: false # above component does the toggling
- type: PowerCellDraw - type: PowerCellDraw
drawRate: 0 drawRate: 0
useRate: 20 useCharge: 20
- type: ToggleCellDraw - type: ToggleCellDraw

View File

@ -43,7 +43,7 @@
components: components:
- type: PowerCellDraw - type: PowerCellDraw
drawRate: 1 drawRate: 1
useRate: 0 useCharge: 0
- type: ToggleCellDraw - type: ToggleCellDraw
- type: entity - type: entity

View File

@ -8,7 +8,12 @@
size: Huge size: Huge
- type: Sprite - type: Sprite
sprite: Objects/Power/portable_recharger.rsi sprite: Objects/Power/portable_recharger.rsi
layers:
- map: ["enum.PowerChargerVisualLayers.Base"]
state: charging state: charging
- map: ["enum.PowerChargerVisualLayers.Light"]
state: charging-unlit
shader: unshaded
- type: Clothing - type: Clothing
equippedPrefix: charging equippedPrefix: charging
quickEquip: false quickEquip: false
@ -20,9 +25,6 @@
slotId: charger_slot slotId: charger_slot
portable: true portable: true
- type: PowerChargerVisuals - type: PowerChargerVisuals
- type: ApcPowerReceiver
needsPower: false
powerLoad: 0
- type: StaticPrice - type: StaticPrice
price: 500 price: 500
- type: Tag - type: Tag
@ -37,9 +39,10 @@
ejectOnUse: true # Mono ejectOnUse: true # Mono
whitelist: whitelist:
components: components:
- HitscanBatteryAmmoProvider - BatteryAmmoProvider
- ProjectileBatteryAmmoProvider blacklist:
blacklist: # DeltaV - Disallows recharging of superweapons
tags: tags:
# Begin DeltaV additions - Disallows recharging of superweapons
- GamblagatorCartridge - GamblagatorCartridge
- BeamDevastator - BeamDevastator
# End DeltaV additions - Disallows recharging of superweapons

View File

@ -5,7 +5,7 @@
components: components:
- type: Item - type: Item
storedRotation: -90 storedRotation: -90
- type: Battery - type: PredictedBattery
pricePerJoule: 0.15 pricePerJoule: 0.15
- type: PowerCell - type: PowerCell
- type: Explosive - type: Explosive
@ -30,9 +30,16 @@
- type: Tag - type: Tag
tags: tags:
- PowerCell - PowerCell
- type: Appearance
- type: PowerCellVisuals
- type: Riggable - type: Riggable
- type: Appearance
- type: PredictedBatteryVisuals
- type: GenericVisualizer
visuals:
enum.BatteryVisuals.State:
enum.PowerCellVisualLayers.Unshaded:
Full: {visible: true, state: o2}
Neither: {visible: true, state: o1}
Empty: {visible: false}
- type: entity - type: entity
name: potato battery name: potato battery
@ -43,7 +50,7 @@
- type: Sprite - type: Sprite
layers: layers:
- state: potato - state: potato
- type: Battery - type: PredictedBattery
maxCharge: 70 maxCharge: 70
startingCharge: 70 startingCharge: 70
- type: Tag - type: Tag
@ -67,7 +74,7 @@
- map: [ "enum.PowerCellVisualLayers.Unshaded" ] - map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2 state: o2
shader: unshaded shader: unshaded
- type: Battery - type: PredictedBattery
maxCharge: 360 maxCharge: 360
startingCharge: 360 startingCharge: 360
- type: Tag - type: Tag
@ -87,7 +94,7 @@
state: o2 state: o2
shader: unshaded shader: unshaded
visible: false visible: false
- type: Battery - type: PredictedBattery
maxCharge: 360 maxCharge: 360
startingCharge: 0 startingCharge: 0
@ -97,7 +104,7 @@
name: small-capacity nuclear power cell name: small-capacity nuclear power cell
description: A self rechargeable power cell, designed for fast recharge rate at the expense of capacity. description: A self rechargeable power cell, designed for fast recharge rate at the expense of capacity.
components: components:
- type: BatterySelfRecharger - type: PredictedBatterySelfRecharger
autoRechargeRate: 36 # 10 seconds to recharge autoRechargeRate: 36 # 10 seconds to recharge
autoRechargePauseTime: 30 autoRechargePauseTime: 30
@ -115,7 +122,7 @@
- map: [ "enum.PowerCellVisualLayers.Unshaded" ] - map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2 state: o2
shader: unshaded shader: unshaded
- type: Battery - type: PredictedBattery
maxCharge: 720 maxCharge: 720
startingCharge: 720 startingCharge: 720
@ -132,7 +139,7 @@
state: o2 state: o2
shader: unshaded shader: unshaded
visible: false visible: false
- type: Battery - type: PredictedBattery
maxCharge: 720 maxCharge: 720
startingCharge: 0 startingCharge: 0
@ -150,7 +157,7 @@
- map: [ "enum.PowerCellVisualLayers.Unshaded" ] - map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2 state: o2
shader: unshaded shader: unshaded
- type: Battery - type: PredictedBattery
maxCharge: 1080 maxCharge: 1080
startingCharge: 1080 startingCharge: 1080
- type: ReverseEngineering # DeltaV: Upgrade line of high -> hyper -> microreactor - type: ReverseEngineering # DeltaV: Upgrade line of high -> hyper -> microreactor
@ -171,7 +178,7 @@
state: o2 state: o2
shader: unshaded shader: unshaded
visible: false visible: false
- type: Battery - type: PredictedBattery
maxCharge: 1080 maxCharge: 1080
startingCharge: 0 startingCharge: 0
@ -189,7 +196,7 @@
- map: [ "enum.PowerCellVisualLayers.Unshaded" ] - map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2 state: o2
shader: unshaded shader: unshaded
- type: Battery - type: PredictedBattery
maxCharge: 1800 maxCharge: 1800
startingCharge: 1800 startingCharge: 1800
- type: ReverseEngineering # DeltaV - type: ReverseEngineering # DeltaV
@ -210,7 +217,7 @@
state: o2 state: o2
shader: unshaded shader: unshaded
visible: false visible: false
- type: Battery - type: PredictedBattery
maxCharge: 1800 maxCharge: 1800
startingCharge: 0 startingCharge: 0
@ -228,10 +235,10 @@
- map: [ "enum.PowerCellVisualLayers.Unshaded" ] - map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2 state: o2
shader: unshaded shader: unshaded
- type: Battery - type: PredictedBattery
maxCharge: 720 maxCharge: 720
startingCharge: 720 startingCharge: 720
- type: BatterySelfRecharger - type: PredictedBatterySelfRecharger
autoRechargeRate: 12 # takes 1 minute to charge itself back to full autoRechargeRate: 12 # takes 1 minute to charge itself back to full
- type: entity - type: entity
@ -247,7 +254,7 @@
state: o2 state: o2
shader: unshaded shader: unshaded
visible: false visible: false
- type: Battery - type: PredictedBattery
startingCharge: 0 startingCharge: 0
- type: entity - type: entity
@ -263,10 +270,10 @@
- map: [ "enum.PowerCellVisualLayers.Unshaded" ] - map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2 state: o2
shader: unshaded shader: unshaded
- type: Battery - type: PredictedBattery
maxCharge: 1200 maxCharge: 1200
startingCharge: 1200 startingCharge: 1200
- type: BatterySelfRecharger - type: PredictedBatterySelfRecharger
autoRechargeRate: 40 autoRechargeRate: 40
- type: ReverseEngineering # DeltaV - type: ReverseEngineering # DeltaV
difficulty: 4 difficulty: 4
@ -314,7 +321,7 @@
- map: [ "enum.PowerCellVisualLayers.Unshaded" ] - map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2 state: o2
shader: unshaded shader: unshaded
- type: Battery - type: PredictedBattery
maxCharge: 1400 maxCharge: 1400
startingCharge: 1400 startingCharge: 1400
@ -332,7 +339,7 @@
- map: [ "enum.PowerCellVisualLayers.Unshaded" ] - map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2 state: o2
shader: unshaded shader: unshaded
- type: Battery - type: PredictedBattery
maxCharge: 2700 maxCharge: 2700
startingCharge: 2700 startingCharge: 2700
@ -350,7 +357,7 @@
- map: [ "enum.PowerCellVisualLayers.Unshaded" ] - map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2 state: o2
shader: unshaded shader: unshaded
- type: Battery - type: PredictedBattery
maxCharge: 6200 maxCharge: 6200
startingCharge: 6200 startingCharge: 6200
@ -368,7 +375,7 @@
state: o2 state: o2
shader: unshaded shader: unshaded
visible: false visible: false
- type: Battery - type: PredictedBattery
maxCharge: 1400 maxCharge: 1400
startingCharge: 0 startingCharge: 0
@ -386,7 +393,7 @@
state: o2 state: o2
shader: unshaded shader: unshaded
visible: false visible: false
- type: Battery - type: PredictedBattery
startingCharge: 0 startingCharge: 0
- type: entity - type: entity
@ -403,5 +410,5 @@
state: o2 state: o2
shader: unshaded shader: unshaded
visible: false visible: false
- type: Battery - type: PredictedBattery
startingCharge: 0 startingCharge: 0

View File

@ -48,7 +48,7 @@
- type: MultiHandedItem - type: MultiHandedItem
- type: ToggleCellDraw - type: ToggleCellDraw
- type: PowerCellDraw - type: PowerCellDraw
useRate: 100 useCharge: 100
- type: entity - type: entity
id: DefibrillatorEmpty id: DefibrillatorEmpty
@ -86,7 +86,7 @@
size: Normal size: Normal
- type: ToggleCellDraw - type: ToggleCellDraw
- type: PowerCellDraw - type: PowerCellDraw
useRate: 100 useCharge: 100
- type: Defibrillator - type: Defibrillator
zapHeal: zapHeal:
types: types:

Some files were not shown because too many files have changed in this diff Show More