]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Predict powercells, chargers and PowerCellDraw (#41379)
authorslarticodefast <161409025+slarticodefast@users.noreply.github.com>
Mon, 24 Nov 2025 16:52:11 +0000 (17:52 +0100)
committerGitHub <noreply@github.com>
Mon, 24 Nov 2025 16:52:11 +0000 (16:52 +0000)
* 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

116 files changed:
Content.Client/Power/EntitySystems/ChargerSystem.cs [deleted file]
Content.Client/PowerCell/PowerCellSystem.cs [deleted file]
Content.Client/PowerCell/PowerCellVisualLayers.cs [new file with mode: 0644]
Content.Client/PowerCell/PowerCellVisualsComponent.cs [deleted file]
Content.Client/PowerCell/PowerChargerVisualizerComponent.cs
Content.Client/PowerCell/PowerChargerVisualizerSystem.cs
Content.Client/Weapons/Ranged/Systems/GunSystem.Battery.cs
Content.Client/Weapons/Ranged/Systems/GunSystem.cs
Content.Server/Access/Systems/IdCardSystem.cs
Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs
Content.Server/Construction/Completions/BuildMech.cs
Content.Server/Holosign/HolosignSystem.cs
Content.Server/Kitchen/Components/MicrowaveComponent.cs
Content.Server/Light/EntitySystems/HandheldLightSystem.cs
Content.Server/Mech/Systems/MechSystem.cs
Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs
Content.Server/Medical/DefibrillatorSystem.cs
Content.Server/Medical/HealthAnalyzerSystem.cs
Content.Server/Ninja/Systems/BatteryDrainerSystem.cs
Content.Server/Ninja/Systems/ItemCreatorSystem.cs
Content.Server/Ninja/Systems/NinjaSuitSystem.cs
Content.Server/Ninja/Systems/SpaceNinjaSystem.cs
Content.Server/Ninja/Systems/StunProviderSystem.cs
Content.Server/Nuke/NukeSystem.cs
Content.Server/PAI/PAISystem.cs
Content.Server/Pinpointer/StationMapSystem.cs
Content.Server/Power/Components/ActiveChargerComponent.cs [deleted file]
Content.Server/Power/Components/ExaminableBatteryComponent.cs [deleted file]
Content.Server/Power/EntitySystems/BatterySystem.API.cs
Content.Server/Power/EntitySystems/BatterySystem.cs
Content.Server/Power/EntitySystems/ChargerSystem.cs [deleted file]
Content.Server/Power/EntitySystems/PowerReceiverSystem.cs
Content.Server/Power/EntitySystems/RiggableSystem.cs
Content.Server/Power/SetBatteryPercentCommand.cs
Content.Server/PowerCell/PowerCellSystem.Draw.cs [deleted file]
Content.Server/PowerCell/PowerCellSystem.cs [deleted file]
Content.Server/Radio/EntitySystems/JammerSystem.cs
Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs
Content.Server/Silicons/Borgs/BorgSystem.Ui.cs
Content.Server/Silicons/Borgs/BorgSystem.cs
Content.Server/Speech/EntitySystems/DamagedSiliconAccentSystem.cs
Content.Server/Stunnable/Systems/StunbatonSystem.cs
Content.Server/Weapons/Misc/TetherGunSystem.cs
Content.Server/Weapons/Ranged/Systems/GunSystem.Battery.cs [deleted file]
Content.Server/Xenoarchaeology/Artifact/XAE/XAEChargeBatterySystem.cs
Content.Shared/Kitchen/BeingMicrowavedEvent.cs [new file with mode: 0644]
Content.Shared/Mech/EntitySystems/SharedMechSystem.cs
Content.Shared/Power/ChargeEvents.cs
Content.Shared/Power/Components/BatteryComponent.cs
Content.Shared/Power/Components/BatterySelfRechargerComponent.cs
Content.Shared/Power/Components/ChargerComponent.cs
Content.Shared/Power/Components/ExaminableBatteryComponent.cs [new file with mode: 0644]
Content.Shared/Power/Components/InsideChargerComponent.cs [new file with mode: 0644]
Content.Shared/Power/Components/PredictedBatteryComponent.cs [new file with mode: 0644]
Content.Shared/Power/Components/PredictedBatterySelfRechargerComponent.cs [new file with mode: 0644]
Content.Shared/Power/Components/PredictedBatteryVisualsComponent.cs [new file with mode: 0644]
Content.Shared/Power/EntitySystems/ChargerSystem.cs [new file with mode: 0644]
Content.Shared/Power/EntitySystems/PredictedBatterySystem.API.cs [new file with mode: 0644]
Content.Shared/Power/EntitySystems/PredictedBatterySystem.cs [new file with mode: 0644]
Content.Shared/Power/EntitySystems/SharedBatterySystem.cs
Content.Shared/Power/EntitySystems/SharedChargerSystem.cs [deleted file]
Content.Shared/Power/EntitySystems/SharedPowerReceiverSystem.cs
Content.Shared/Power/SharedPowerItemCharger.cs [deleted file]
Content.Shared/PowerCell/Components/PowerCellComponent.cs
Content.Shared/PowerCell/Components/PowerCellDrawComponent.cs [new file with mode: 0644]
Content.Shared/PowerCell/Components/PowerCellSlotComponent.cs
Content.Shared/PowerCell/PowerCellDrawComponent.cs [deleted file]
Content.Shared/PowerCell/PowerCellEvents.cs [moved from Content.Shared/PowerCell/PowerCellSlotEmptyEvent.cs with 53% similarity]
Content.Shared/PowerCell/PowerCellSystem.API.cs [new file with mode: 0644]
Content.Shared/PowerCell/PowerCellSystem.Draw.cs [new file with mode: 0644]
Content.Shared/PowerCell/PowerCellSystem.Relay.cs [new file with mode: 0644]
Content.Shared/PowerCell/PowerCellSystem.cs [new file with mode: 0644]
Content.Shared/PowerCell/SharedPowerCellSystem.cs [deleted file]
Content.Shared/PowerCell/ToggleCellDrawSystem.cs
Content.Shared/Silicons/Borgs/Components/BorgChassisComponent.cs
Content.Shared/Storage/Components/EntityStorageComponent.cs
Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs
Content.Shared/UserInterface/ActivatableUIRequiresPowerCellComponent.cs
Content.Shared/UserInterface/ActivatableUISystem.Power.cs
Content.Shared/Weapons/Ranged/Components/BatteryAmmoProviderComponent.cs
Content.Shared/Weapons/Ranged/Components/HitscanBatteryAmmoProviderComponent.cs [deleted file]
Content.Shared/Weapons/Ranged/Components/ProjectileBatteryAmmoProviderComponent.cs [deleted file]
Content.Shared/Weapons/Ranged/Events/UpdateClientAmmoEvent.cs [deleted file]
Content.Shared/Weapons/Ranged/Systems/BatteryWeaponFireModesSystem.cs
Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Battery.cs
Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs
Resources/Maps/Shuttles/emergency_meta.yml
Resources/Prototypes/Catalog/Bounties/bounties.yml
Resources/Prototypes/Entities/Clothing/Hands/gloves.yml
Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml
Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml
Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml
Resources/Prototypes/Entities/Mobs/NPCs/lavaland.yml
Resources/Prototypes/Entities/Mobs/NPCs/living_light.yml
Resources/Prototypes/Entities/Mobs/NPCs/miscellaneous.yml
Resources/Prototypes/Entities/Objects/Devices/base_handheld.yml
Resources/Prototypes/Entities/Objects/Fun/spectral_locator.yml
Resources/Prototypes/Entities/Objects/Power/portable_recharger.yml
Resources/Prototypes/Entities/Objects/Power/powercells.yml
Resources/Prototypes/Entities/Objects/Specific/Medical/defib.yml
Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml
Resources/Prototypes/Entities/Objects/Specific/Research/anomaly.yml
Resources/Prototypes/Entities/Objects/Specific/Salvage/scanner.yml
Resources/Prototypes/Entities/Objects/Tools/handheld_mass_scanner.yml
Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml
Resources/Prototypes/Entities/Objects/Weapons/Guns/LMGs/lmgs.yml
Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml
Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml
Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_base.yml
Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_energy.yml
Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml
Resources/Prototypes/Entities/Objects/Weapons/Melee/stunprod.yml
Resources/Prototypes/Entities/Objects/Weapons/security.yml
Resources/Prototypes/Entities/Structures/Power/chargers.yml
Resources/Prototypes/Entities/Structures/Shuttles/cannons.yml
Resources/Prototypes/tags.yml

diff --git a/Content.Client/Power/EntitySystems/ChargerSystem.cs b/Content.Client/Power/EntitySystems/ChargerSystem.cs
deleted file mode 100644 (file)
index efadde3..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-using Content.Shared.Power.EntitySystems;
-
-namespace Content.Client.Power.EntitySystems;
-
-public sealed class ChargerSystem : SharedChargerSystem;
diff --git a/Content.Client/PowerCell/PowerCellSystem.cs b/Content.Client/PowerCell/PowerCellSystem.cs
deleted file mode 100644 (file)
index 8d9dd5e..0000000
+++ /dev/null
@@ -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,
-    }
-}
diff --git a/Content.Client/PowerCell/PowerCellVisualLayers.cs b/Content.Client/PowerCell/PowerCellVisualLayers.cs
new file mode 100644 (file)
index 0000000..f0caf76
--- /dev/null
@@ -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,
+}
diff --git a/Content.Client/PowerCell/PowerCellVisualsComponent.cs b/Content.Client/PowerCell/PowerCellVisualsComponent.cs
deleted file mode 100644 (file)
index 37dfd78..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-namespace Content.Client.PowerCell;
-
-[RegisterComponent]
-public sealed partial class PowerCellVisualsComponent : Component {}
index d96830b5f8f2fba5cd1ed1a3c4d0ac55b8ff10a6..cac503560d2c7e056ea1b61d526c81f8351bea21 100644 (file)
@@ -1,4 +1,4 @@
-using Content.Shared.Power;
+using Content.Shared.Power.Components;
 
 namespace Content.Client.PowerCell;
 
@@ -9,15 +9,13 @@ public sealed partial class PowerChargerVisualsComponent : Component
     /// <summary>
     /// The base sprite state used if the power cell charger does not contain a power cell.
     /// </summary>
-    [DataField("emptyState")]
-    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public string EmptyState = "empty";
 
     /// <summary>
     /// The base sprite state used if the power cell charger contains a power cell.
     /// </summary>
-    [DataField("occupiedState")]
-    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public string OccupiedState = "full";
 
     /// <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.Charged"/> Maps to the state used when the charger contains a fully charged power cell.
     /// </summary>
-    [DataField("lightStates")]
-    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public Dictionary<CellChargerStatus, string> LightStates = new()
     {
         [CellChargerStatus.Off] = "light-off",
index c76e25b7af94ba17cacb5f990eb2bfa2aea9840f..bb0a35d0b35fb9a72a342ef0268aa216ec4c0ce8 100644 (file)
@@ -1,4 +1,4 @@
-using Content.Shared.Power;
+using Content.Shared.Power.Components;
 using Robust.Client.GameObjects;
 
 namespace Content.Client.PowerCell;
index 122244e7f22d6d4647c59b77961de58116645218..b8b3d2ad87c4ec0ab248cb932b397469d0e0a720 100644 (file)
@@ -7,23 +7,20 @@ public sealed partial class GunSystem
     protected override void InitializeBattery()
     {
         base.InitializeBattery();
-        // Hitscan
-        SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
-        SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
 
-        // Projectile
-        SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
-        SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
+        SubscribeLocalEvent<BatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
+        SubscribeLocalEvent<BatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
     }
 
-    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();
     }
index adef067b60d5a1bc4184ae7f7e6011d5ca0ce425..d3dfd50cbf7377d81195299208c4c6ee58e14672 100644 (file)
@@ -80,7 +80,6 @@ public sealed partial class GunSystem : SharedGunSystem
         base.Initialize();
         UpdatesOutsidePrediction = true;
         SubscribeLocalEvent<AmmoCounterComponent, ItemStatusCollectMessage>(OnAmmoCounterCollect);
-        SubscribeLocalEvent<AmmoCounterComponent, UpdateClientAmmoEvent>(OnUpdateClientAmmo);
         SubscribeAllEvent<MuzzleFlashEvent>(OnMuzzleFlash);
 
         // Plays animated effects on the client.
@@ -90,10 +89,6 @@ public sealed partial class GunSystem : SharedGunSystem
         InitializeSpentAmmo();
     }
 
-    private void OnUpdateClientAmmo(EntityUid uid, AmmoCounterComponent ammoComp, ref UpdateClientAmmoEvent args)
-    {
-        UpdateAmmoCount(uid, ammoComp);
-    }
 
     private void OnMuzzleFlash(MuzzleFlashEvent args)
     {
@@ -158,6 +153,8 @@ public sealed partial class GunSystem : SharedGunSystem
 
     public override void Update(float frameTime)
     {
+        base.Update(frameTime);
+
         if (!Timing.IsFirstTimePredicted)
             return;
 
index f317e88f0fc0954831e1238f6767ad0971b28d80..db39491741af02ed218e429b7611200bd7a932b2 100644 (file)
@@ -8,6 +8,7 @@ using Content.Shared.Access.Components;
 using Content.Shared.Access.Systems;
 using Content.Shared.Chat;
 using Content.Shared.Database;
+using Content.Shared.Kitchen;
 using Content.Shared.Popups;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
index 4f96a25a5c616a260ada6a501769f26b2da5d206..ef102cb38a6b59c87a05ec2e63c1643619e67243 100644 (file)
@@ -25,6 +25,7 @@ using Content.Shared.Hands.Components;
 using Content.Shared.Inventory;
 using Content.Shared.PDA;
 using Content.Shared.Power.Components;
+using Content.Shared.Power.EntitySystems;
 using Content.Shared.Stacks;
 using Content.Shared.Station.Components;
 using Content.Shared.Verbs;
@@ -52,6 +53,7 @@ public sealed partial class AdminVerbSystem
     [Dependency] private readonly StationJobsSystem _stationJobsSystem = default!;
     [Dependency] private readonly JointSystem _jointSystem = default!;
     [Dependency] private readonly BatterySystem _batterySystem = default!;
+    [Dependency] private readonly PredictedBatterySystem _predictedBatterySystem = default!;
     [Dependency] private readonly MetaDataSystem _metaSystem = default!;
     [Dependency] private readonly GunSystem _gun = default!;
 
@@ -160,6 +162,57 @@ public sealed partial class AdminVerbSystem
             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))
         {
             Verb refillBattery = new()
index c0b5921db9264e36d9c9f9c3f416a4e694df9ec5..5f1e40c3477b41d7c5f986d85bdf28aefdb8bb03 100644 (file)
@@ -48,7 +48,7 @@ public sealed partial class BuildMech : IGraphAction
 
         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.");
             return;
index beb5e909c0be3822f8aacd68b0b20b5e0137198f..7d01ffb9752224e3fd6e67abb7efe980416d4542 100644 (file)
@@ -1,8 +1,7 @@
 using Content.Shared.Examine;
 using Content.Shared.Coordinates.Helpers;
-using Content.Server.PowerCell;
+using Content.Shared.PowerCell;
 using Content.Shared.Interaction;
-using Content.Shared.Power.Components;
 using Content.Shared.Storage;
 
 namespace Content.Server.Holosign;
@@ -23,9 +22,8 @@ public sealed class HolosignSystem : EntitySystem
     {
         // 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.
-        _powerCell.TryGetBatteryFromSlot(uid, out var battery);
-        var charges = UsesRemaining(component, battery);
-        var maxCharges = MaxUses(component, battery);
+        var charges = _powerCell.GetRemainingUses(uid, component.ChargeUse);
+        var maxCharges = _powerCell.GetMaxUses(uid, component.ChargeUse);
 
         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
         var holoUid = Spawn(component.SignProto, args.ClickLocation.SnapToGrid(EntityManager));
         var xform = Transform(holoUid);
+        // TODO: Just make the prototype anchored
         if (!xform.Anchored)
             _transform.AnchorEntity(holoUid, xform); // anchor to prevent any tempering with (don't know what could even interact with it)
 
         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);
-    }
 }
index 5337d80fd17bf69a58a72c53b409628fd16d21a6..85478b83cd7fcb2993f7cbeef1bdc59315fbdbbc 100644 (file)
@@ -111,16 +111,4 @@ namespace Content.Server.Kitchen.Components
         [DataField, ViewVariables(VVAccess.ReadWrite)]
         public bool CanMicrowaveIdsSafely = true;
     }
-
-    public sealed class BeingMicrowavedEvent : HandledEntityEventArgs
-    {
-        public EntityUid Microwave;
-        public EntityUid? User;
-
-        public BeingMicrowavedEvent(EntityUid microwave, EntityUid? user)
-        {
-            Microwave = microwave;
-            User = user;
-        }
-    }
 }
index 7167aa496367bbfd53ec1fab99f54c26483ee9f4..a7f5801f32a41dd4b3c768a75cae9b9e7705f997 100644 (file)
@@ -1,12 +1,12 @@
 using Content.Server.Actions;
 using Content.Server.Popups;
-using Content.Server.Power.EntitySystems;
-using Content.Server.PowerCell;
 using Content.Shared.Actions;
 using Content.Shared.Examine;
 using Content.Shared.Interaction;
 using Content.Shared.Light;
 using Content.Shared.Light.Components;
+using Content.Shared.Power.EntitySystems;
+using Content.Shared.PowerCell;
 using Content.Shared.Rounding;
 using Content.Shared.Toggleable;
 using JetBrains.Annotations;
@@ -25,7 +25,7 @@ namespace Content.Server.Light.EntitySystems
         [Dependency] private readonly ActionContainerSystem _actionContainer = default!;
         [Dependency] private readonly PopupSystem _popup = 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 SharedAudioSystem _audio = 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
             // 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;
 
-            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 (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)
@@ -153,6 +155,8 @@ namespace Content.Server.Light.EntitySystems
             _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)
         {
             var toRemove = new RemQueue<Entity<HandheldLightComponent>>();
@@ -199,8 +203,7 @@ namespace Content.Server.Light.EntitySystems
                 return false;
             }
 
-            if (!_powerCell.TryGetBatteryFromSlot(uid, out var battery) &&
-                !TryComp(uid, out battery))
+            if (!_powerCell.TryGetBatteryFromSlot(uid.Owner, out var battery))
             {
                 _audio.PlayPvs(_audio.ResolveSound(component.TurnOnFailSound), uid);
                 _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.
             // Let's just say you need a whole second of charge before you can turn it on.
             // Simple enough.
-            if (component.Wattage > battery.CurrentCharge)
+            if (component.Wattage > _battery.GetCharge(battery.Value.AsNullable()))
             {
                 _audio.PlayPvs(_audio.ResolveSound(component.TurnOnFailSound), uid);
                 _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)
         {
             var component = uid.Comp;
-            if (!_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery, null) &&
-                !TryComp(uid, out battery))
+            if (!_powerCell.TryGetBatteryFromSlot(uid.Owner, out var battery))
             {
                 TurnOff(uid, false);
                 return;
             }
 
-            if (batteryUid == null)
-                return;
-
             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)
             {
                 _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);
             }
 
-            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);
 
             UpdateLevel(uid);
index 89297a6e86278dfc1b7a99a37091d7204a947829..80169cb2ed9b5cadbc2aa0d5a1d4ef2d2b479bf7 100644 (file)
@@ -2,7 +2,6 @@ using System.Linq;
 using Content.Server.Atmos.EntitySystems;
 using Content.Server.Body.Systems;
 using Content.Server.Mech.Components;
-using Content.Server.Power.EntitySystems;
 using Content.Shared.ActionBlocker;
 using Content.Shared.Damage.Systems;
 using Content.Shared.DoAfter;
@@ -14,6 +13,7 @@ using Content.Shared.Mech.EntitySystems;
 using Content.Shared.Movement.Events;
 using Content.Shared.Popups;
 using Content.Shared.Power.Components;
+using Content.Shared.Power.EntitySystems;
 using Content.Shared.Tools;
 using Content.Shared.Tools.Components;
 using Content.Shared.Tools.Systems;
@@ -33,7 +33,7 @@ public sealed partial class MechSystem : SharedMechSystem
 {
     [Dependency] private readonly ActionBlockerSystem _actionBlocker = 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 DamageableSystem _damageable = 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)
             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);
             _actionBlocker.UpdateCanMove(uid);
@@ -109,10 +109,10 @@ public sealed partial class MechSystem : SharedMechSystem
 
     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;
 
-        component.Energy = battery.CurrentCharge;
+        component.Energy = _battery.GetCharge((args.Entity, battery));
         component.MaxEnergy = battery.MaxCharge;
 
         Dirty(uid, component);
@@ -337,21 +337,23 @@ public sealed partial class MechSystem : SharedMechSystem
         if (battery == null)
             return false;
 
-        if (!TryComp<BatteryComponent>(battery, out var batteryComp))
+        if (!TryComp<PredictedBatteryComponent>(battery, out var batteryComp))
             return false;
 
-        _battery.SetCharge((battery.Value, batteryComp), batteryComp.CurrentCharge + delta.Float());
-        if (batteryComp.CurrentCharge != component.Energy) //if there's a discrepency, we have to resync them
+        _battery.SetCharge((battery.Value, batteryComp), _battery.GetCharge((battery.Value, batteryComp)) + delta.Float());
+        // 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}");
-            component.Energy = batteryComp.CurrentCharge;
+            Log.Debug($"Battery charge was not equal to mech charge. Battery {charge}. Mech {component.Energy}");
+            component.Energy = charge;
             Dirty(uid, component);
         }
         _actionBlocker.UpdateCanMove(uid);
         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))
             return;
@@ -360,7 +362,7 @@ public sealed partial class MechSystem : SharedMechSystem
             return;
 
         _container.Insert(toInsert, component.BatterySlot);
-        component.Energy = battery.CurrentCharge;
+        component.Energy = _battery.GetCharge((toInsert, battery));
         component.MaxEnergy = battery.MaxCharge;
 
         _actionBlocker.UpdateCanMove(uid);
index 0e1d27a3c55621d701bab91e6b3bcdce69da36de..92c5f59b15797f0c2d324357909096d32da9f04e 100644 (file)
@@ -1,7 +1,7 @@
 using System.Linq;
 using Content.Server.DeviceNetwork;
 using Content.Server.DeviceNetwork.Systems;
-using Content.Server.PowerCell;
+using Content.Shared.PowerCell;
 using Content.Shared.DeviceNetwork;
 using Content.Shared.DeviceNetwork.Events;
 using Content.Shared.Medical.CrewMonitoring;
index 14ee155d2a4b5b249c4c7d23d593f5920faebdde..719bd9946ca76ddd9064a6158d1d28c3be8f6e95 100644 (file)
@@ -5,7 +5,7 @@ using Content.Server.Electrocution;
 using Content.Server.EUI;
 using Content.Server.Ghost;
 using Content.Server.Popups;
-using Content.Server.PowerCell;
+using Content.Shared.PowerCell;
 using Content.Shared.Traits.Assorted;
 using Content.Shared.Chat;
 using Content.Shared.Damage.Components;
index f022dff5e352ccb76cd4fd9de47b9c3854e07f66..bc6285c1d216c4be5bc4d29de289a3a40d111cad 100644 (file)
@@ -1,5 +1,4 @@
 using Content.Server.Medical.Components;
-using Content.Server.PowerCell;
 using Content.Shared.Body.Components;
 using Content.Shared.Chemistry.EntitySystems;
 using Content.Shared.Damage.Components;
@@ -12,6 +11,7 @@ using Content.Shared.Item.ItemToggle.Components;
 using Content.Shared.MedicalScanner;
 using Content.Shared.Mobs.Components;
 using Content.Shared.Popups;
+using Content.Shared.PowerCell;
 using Content.Shared.Temperature.Components;
 using Content.Shared.Traits.Assorted;
 using Robust.Server.GameObjects;
@@ -81,7 +81,7 @@ public sealed class HealthAnalyzerSystem : EntitySystem
     /// </summary>
     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;
 
         _audio.PlayPvs(uid.Comp.ScanningBeginSound, uid);
@@ -101,7 +101,7 @@ public sealed class HealthAnalyzerSystem : EntitySystem
 
     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;
 
         if (!uid.Comp.Silent)
index 7b9d229d73cb5a2ed6a2c731de6324a095c88ef2..f6d9d7bec744048ff72e4a573cffa3690e0ecf2e 100644 (file)
@@ -7,6 +7,7 @@ using Content.Shared.Ninja.Components;
 using Content.Shared.Ninja.Systems;
 using Content.Shared.Popups;
 using Content.Shared.Power.Components;
+using Content.Shared.Power.EntitySystems;
 using Robust.Shared.Audio.Systems;
 
 namespace Content.Server.Ninja.Systems;
@@ -17,6 +18,7 @@ namespace Content.Server.Ninja.Systems;
 public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
 {
     [Dependency] private readonly BatterySystem _battery = default!;
+    [Dependency] private readonly PredictedBatterySystem _predictedBattery = default!;
     [Dependency] private readonly SharedAudioSystem _audio = default!;
     [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
     [Dependency] private readonly SharedPopupSystem _popup = default!;
@@ -37,7 +39,7 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
     {
         var (uid, comp) = ent;
         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;
 
         // 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);
 
-        if (ent.Comp.BatteryUid is not {} battery || _battery.IsFull(battery))
+        if (ent.Comp.BatteryUid is not { } battery || _battery.IsFull(battery))
             args.Cancel();
     }
 
@@ -78,7 +80,7 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
     protected override bool TryDrainPower(Entity<BatteryDrainerComponent> ent, EntityUid target)
     {
         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;
 
         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 required = battery.MaxCharge - battery.CurrentCharge;
+        var required = battery.MaxCharge - _predictedBattery.GetCharge((comp.BatteryUid.Value, battery));
         // higher tier storages can charge more
         var maxDrained = pnb.MaxSupply * comp.DrainTime;
         var input = Math.Min(Math.Min(available, required / comp.DrainEfficiency), maxDrained);
@@ -99,13 +101,15 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
             return false;
 
         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
         Spawn("EffectSparks", Transform(target).Coordinates);
         _audio.PlayPvs(comp.SparkSound, target);
         _popup.PopupEntity(Loc.GetString("battery-drainer-success", ("battery", target)), uid, uid);
 
         // repeat the doafter until battery is full
-        return !_battery.IsFull((comp.BatteryUid.Value, battery));
+        return !_predictedBattery.IsFull((comp.BatteryUid.Value, battery));
     }
 }
index d7a7be995db276c2b2c19c2bfecbd9cadf999f2e..227fdea789999b8fad5b56936c03c04228ea3627 100644 (file)
@@ -1,15 +1,15 @@
 using Content.Server.Ninja.Events;
-using Content.Server.Power.EntitySystems;
 using Content.Shared.Hands.EntitySystems;
 using Content.Shared.Ninja.Components;
 using Content.Shared.Ninja.Systems;
 using Content.Shared.Popups;
+using Content.Shared.Power.EntitySystems;
 
 namespace Content.Server.Ninja.Systems;
 
 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 SharedPopupSystem _popup = default!;
 
@@ -24,7 +24,7 @@ public sealed class ItemCreatorSystem : SharedItemCreatorSystem
     private void OnCreateItem(Entity<ItemCreatorComponent> ent, ref CreateItemEvent args)
     {
         var (uid, comp) = ent;
-        if (comp.Battery is not {} battery)
+        if (comp.Battery is not { } battery)
             return;
 
         args.Handled = true;
index 399d94e8f77dba3e79abd6b4d6f810ccf652e71c..8ebd56ea7c0e90647c25d3e3025db5225c404e60 100644 (file)
@@ -1,11 +1,10 @@
 using Content.Server.Ninja.Events;
-using Content.Server.Power.Components;
-using Content.Server.PowerCell;
 using Content.Shared.Emp;
 using Content.Shared.Hands.EntitySystems;
 using Content.Shared.Ninja.Components;
 using Content.Shared.Ninja.Systems;
 using Content.Shared.Power.Components;
+using Content.Shared.PowerCell;
 using Content.Shared.PowerCell.Components;
 using Robust.Shared.Containers;
 
@@ -13,6 +12,7 @@ namespace Content.Server.Ninja.Systems;
 
 /// <summary>
 /// Handles power cell upgrading and actions.
+/// TODO: Move all of this to shared and predict it
 /// </summary>
 public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
 {
@@ -51,8 +51,6 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
         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)
     {
         // this is for handling battery upgrading, not stopping actions from being added
@@ -61,10 +59,10 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
             return;
 
         // 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;
 
-        if (!TryComp<BatteryComponent>(args.EntityUid, out var inserting))
+        if (!TryComp<PredictedBatteryComponent>(args.EntityUid, out var inserting))
         {
             args.Cancel();
             return;
@@ -73,7 +71,7 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
         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
-        if (GetCellScore(args.EntityUid, inserting) <= GetCellScore(batteryUid.Value, battery))
+        if (GetCellScore(args.EntityUid, inserting) <= GetCellScore(battery.Value, battery.Value))
         {
             args.Cancel();
             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.
-    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,
         // 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;
     }
@@ -136,7 +134,6 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
         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)
     {
         var (uid, comp) = ent;
index fd7f908738d46b82128207ffbad99d44b35faa87..f2fc9abfccb5c1af67f4c6d1430910ad50d3d0df 100644 (file)
@@ -2,8 +2,6 @@ using Content.Server.Communications;
 using Content.Server.CriminalRecords.Systems;
 using Content.Server.Objectives.Components;
 using Content.Server.Objectives.Systems;
-using Content.Server.Power.EntitySystems;
-using Content.Server.PowerCell;
 using Content.Server.Research.Systems;
 using Content.Shared.Alert;
 using Content.Shared.Doors.Components;
@@ -12,6 +10,8 @@ using Content.Shared.Mind;
 using Content.Shared.Ninja.Components;
 using Content.Shared.Ninja.Systems;
 using Content.Shared.Power.Components;
+using Content.Shared.Power.EntitySystems;
+using Content.Shared.PowerCell;
 using Content.Shared.Popups;
 using Content.Shared.Rounding;
 using System.Diagnostics.CodeAnalysis;
@@ -24,7 +24,7 @@ namespace Content.Server.Ninja.Systems;
 public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
 {
     [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 PowerCellSystem _powerCell = default!;
     [Dependency] private readonly SharedMindSystem _mind = default!;
@@ -39,6 +39,8 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
         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)
     {
         var query = EntityQueryEnumerator<SpaceNinjaComponent>();
@@ -62,7 +64,7 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
         return newCount - oldCount;
     }
 
-    // TODO: can probably copy paste borg code here
+    // TODO: Generic charge indicator that is combined with borg code.
     /// <summary>
     /// Update the alert for the ninja's suit power indicator.
     /// </summary>
@@ -75,10 +77,10 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
             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);
-            _alerts.ShowAlert(uid, comp.SuitPowerAlert, (short) severity);
+            var severity = ContentHelpers.RoundToLevels(MathF.Max(0f, _battery.GetCharge((batteryUid.Value, batteryComp))), batteryComp.MaxCharge, 8);
+            _alerts.ShowAlert(uid, comp.SuitPowerAlert, (short)severity);
         }
         else
         {
@@ -89,17 +91,19 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
     /// <summary>
     /// Get the battery component in a ninja's suit, if it's worn.
     /// </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)
             && 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;
         }
 
-        uid = null;
-        battery = null;
+        batteryUid = null;
+        batteryComp = null;
         return false;
     }
 
index 98df8a039a04b2948322dba71c62ee51fd206cf0..49d7ab5e85c05c5acbe3d5ba21d322fc23c32443 100644 (file)
@@ -1,10 +1,10 @@
 using Content.Server.Ninja.Events;
-using Content.Server.Power.EntitySystems;
 using Content.Shared.Damage.Systems;
 using Content.Shared.Interaction;
 using Content.Shared.Ninja.Components;
 using Content.Shared.Ninja.Systems;
 using Content.Shared.Popups;
+using Content.Shared.Power.EntitySystems;
 using Content.Shared.Stunnable;
 using Content.Shared.Timing;
 using Content.Shared.Whitelist;
@@ -17,7 +17,7 @@ namespace Content.Server.Ninja.Systems;
 /// </summary>
 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 EntityWhitelistSystem _whitelist = default!;
     [Dependency] private readonly SharedAudioSystem _audio = default!;
index f110734a604bc73655ca61d3d120ccc6a30312ad..3788d2b2ab57268d439336ab452015cd35f357a2 100644 (file)
@@ -2,7 +2,6 @@ using Content.Server.AlertLevel;
 using Content.Server.Audio;
 using Content.Server.Chat.Systems;
 using Content.Server.Explosion.EntitySystems;
-using Content.Server.Kitchen.Components;
 using Content.Server.Pinpointer;
 using Content.Server.Popups;
 using Content.Server.Station.Systems;
@@ -11,6 +10,7 @@ using Content.Shared.Containers.ItemSlots;
 using Content.Shared.Coordinates.Helpers;
 using Content.Shared.DoAfter;
 using Content.Shared.Examine;
+using Content.Shared.Kitchen;
 using Content.Shared.Maps;
 using Content.Shared.Nuke;
 using Content.Shared.Popups;
index 7fd58f15728cb110888115bf6f8b667da3cb251c..a23defdceae61721820b8ae43560491f0c800025 100644 (file)
@@ -1,9 +1,9 @@
 using Content.Server.Ghost.Roles;
 using Content.Server.Ghost.Roles.Components;
 using Content.Server.Instruments;
-using Content.Server.Kitchen.Components;
 using Content.Shared.Interaction.Events;
 using Content.Shared.Mind.Components;
+using Content.Shared.Kitchen;
 using Content.Shared.PAI;
 using Content.Shared.Popups;
 using Content.Shared.Instruments;
index b0b3141fb0ee80a5cf4c368fcc20e156cc29cb94..c8e5b226172ff461d717f6767ff50cb1d30455b3 100644 (file)
@@ -1,4 +1,4 @@
-using Content.Server.PowerCell;
+using Content.Shared.PowerCell;
 using Content.Shared.Pinpointer;
 using Robust.Server.GameObjects;
 using Robust.Shared.Player;
diff --git a/Content.Server/Power/Components/ActiveChargerComponent.cs b/Content.Server/Power/Components/ActiveChargerComponent.cs
deleted file mode 100644 (file)
index f3d863c..0000000
+++ /dev/null
@@ -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
-    {
-    }
-}
diff --git a/Content.Server/Power/Components/ExaminableBatteryComponent.cs b/Content.Server/Power/Components/ExaminableBatteryComponent.cs
deleted file mode 100644 (file)
index 4cf4abd..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Content.Server.Power.Components
-{
-    [RegisterComponent]
-    public sealed partial class ExaminableBatteryComponent : Component
-    {}
-}
index 6ddf1f4bbc198ed921e6faf5dd18238808bedf05..5683e7c13349a32746d7f1f112923d29b176b7b2 100644 (file)
@@ -1,8 +1,14 @@
 using Content.Shared.Power;
 using Content.Shared.Power.Components;
+using Content.Shared.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 override float ChangeCharge(Entity<BatteryComponent?> ent, float amount)
@@ -12,6 +18,10 @@ public sealed partial class BatterySystem
 
         var newValue = Math.Clamp(ent.Comp.CurrentCharge + amount, 0, ent.Comp.MaxCharge);
         var delta = newValue - ent.Comp.CurrentCharge;
+
+        if (delta == 0f)
+            return delta;
+
         ent.Comp.CurrentCharge = newValue;
 
         TrySetChargeCooldown(ent.Owner);
@@ -23,8 +33,8 @@ public sealed partial class BatterySystem
 
     public override float UseCharge(Entity<BatteryComponent?> ent, float amount)
     {
-        if (amount <= 0 || !Resolve(ent, ref ent.Comp) || ent.Comp.CurrentCharge == 0)
-            return 0;
+        if (amount <= 0f || !Resolve(ent, ref ent.Comp) || ent.Comp.CurrentCharge == 0)
+            return 0f;
 
         return ChangeCharge(ent, -amount);
     }
@@ -69,6 +79,45 @@ public sealed partial class BatterySystem
         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)
     {
         if (!Resolve(ent, ref ent.Comp, false))
index 4b6a52c0f117169f585a2f3efd56d1d08af1ddc5..1a88672bc935894dbca7a2442d31c1a3f1ece1d0 100644 (file)
@@ -11,6 +11,11 @@ using Robust.Shared.Timing;
 
 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]
 public sealed partial class BatterySystem : SharedBatterySystem
 {
@@ -20,7 +25,8 @@ public sealed partial class BatterySystem : SharedBatterySystem
     {
         base.Initialize();
 
-        SubscribeLocalEvent<ExaminableBatteryComponent, ExaminedEvent>(OnExamine);
+        SubscribeLocalEvent<BatteryComponent, ComponentInit>(OnInit);
+        SubscribeLocalEvent<BatteryComponent, ExaminedEvent>(OnExamine);
         SubscribeLocalEvent<BatteryComponent, RejuvenateEvent>(OnBatteryRejuvenate);
         SubscribeLocalEvent<PowerNetworkBatteryComponent, RejuvenateEvent>(OnNetBatteryRejuvenate);
         SubscribeLocalEvent<BatteryComponent, PriceCalculationEvent>(CalculateBatteryPrice);
@@ -31,27 +37,31 @@ public sealed partial class BatterySystem : SharedBatterySystem
         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)
     {
         ent.Comp.NetworkBattery.CurrentStorage = ent.Comp.NetworkBattery.Capacity;
     }
-
     private void OnBatteryRejuvenate(Entity<BatteryComponent> ent, ref RejuvenateEvent args)
     {
         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)
             return;
 
-        if (!TryComp<BatteryComponent>(ent, out var battery))
+        if (!HasComp<ExaminableBatteryComponent>(ent))
             return;
 
         var chargePercentRounded = 0;
-        if (battery.MaxCharge != 0)
-            chargePercentRounded = (int)(100 * battery.CurrentCharge / battery.MaxCharge);
+        if (ent.Comp.MaxCharge != 0)
+            chargePercentRounded = (int)(100 * ent.Comp.CurrentCharge / ent.Comp.MaxCharge);
+
         args.PushMarkup(
             Loc.GetString(
                 "examinable-battery-component-examine-detail",
diff --git a/Content.Server/Power/EntitySystems/ChargerSystem.cs b/Content.Server/Power/EntitySystems/ChargerSystem.cs
deleted file mode 100644 (file)
index d523de6..0000000
+++ /dev/null
@@ -1,259 +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;
-
-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!;
-
-    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);
-    }
-
-    private 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))
-        {
-            // or by checking for a power cell slot on the inserted entity
-            return _powerCell.TryGetBatteryFromSlot(uid, out batteryUid, out component);
-        }
-        batteryUid = uid;
-        return true;
-    }
-}
index f3405486e6754c7d397a0e914534f8ef7254a65c..f5ecb118dc57a86b587fcb9f93740d9675497b68 100644 (file)
@@ -161,11 +161,6 @@ namespace Content.Server.Power.EntitySystems
             return !_recQuery.Resolve(uid, ref receiver, false) || receiver.Powered;
         }
 
-        public void SetLoad(ApcPowerReceiverComponent comp, float load)
-        {
-            comp.Load = load;
-        }
-
         public override bool ResolveApc(EntityUid entity, [NotNullWhen(true)] ref SharedApcPowerReceiverComponent? component)
         {
             if (component != null)
index 0f8b32865b3a98e6d1cb0a0a0d1b569a68fc2e05..3972cf3f864d84cf7ed5dfb17dc6ad767138b035 100644 (file)
@@ -1,10 +1,12 @@
 using Content.Server.Administration.Logs;
 using Content.Server.Explosion.EntitySystems;
-using Content.Server.Kitchen.Components;
 using Content.Server.Power.Components;
 using Content.Shared.Chemistry.EntitySystems;
 using Content.Shared.Database;
+using Content.Shared.Kitchen;
+using Content.Shared.Power;
 using Content.Shared.Power.Components;
+using Content.Shared.Power.EntitySystems;
 using Content.Shared.Rejuvenate;
 
 namespace Content.Server.Power.EntitySystems;
@@ -16,6 +18,7 @@ public sealed class RiggableSystem : EntitySystem
 {
     [Dependency] private readonly ExplosionSystem _explosionSystem = default!;
     [Dependency] private readonly IAdminLogManager _adminLogger = default!;
+    [Dependency] private readonly PredictedBatterySystem _predictedBattery = default!;
 
     public override void Initialize()
     {
@@ -23,6 +26,8 @@ public sealed class RiggableSystem : EntitySystem
         SubscribeLocalEvent<RiggableComponent, RejuvenateEvent>(OnRejuvenate);
         SubscribeLocalEvent<RiggableComponent, BeingMicrowavedEvent>(OnMicrowaved);
         SubscribeLocalEvent<RiggableComponent, SolutionContainerChangedEvent>(OnSolutionChanged);
+        SubscribeLocalEvent<RiggableComponent, ChargeChangedEvent>(OnChargeChanged);
+        SubscribeLocalEvent<RiggableComponent, PredictedBatteryChargeChangedEvent>(OnChargeChanged);
     }
 
     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 (batteryComponent.CurrentCharge == 0)
+            if (batteryComponent.CurrentCharge == 0f)
                 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.Owner, batteryComponent, args.User);
+            Explode(entity, charge);
+            args.Handled = true;
+        }
     }
 
     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)
+    {
+        var radius = MathF.Min(5, MathF.Sqrt(charge) / 9);
+
+        _explosionSystem.TriggerExplosive(uid, radius: radius, user: cause);
+        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 (!Resolve(uid, ref battery))
+        if (!ent.Comp.IsRigged)
             return;
 
-        var radius = MathF.Min(5, MathF.Sqrt(battery.CurrentCharge) / 9);
+        if (TryComp<PredictedBatteryComponent>(ent, out var predictedBatteryComponent))
+        {
+            var charge = _predictedBattery.GetCharge((ent.Owner, predictedBatteryComponent));
+            if (charge == 0f)
+                return;
 
-        _explosionSystem.TriggerExplosive(uid, radius: radius, user:cause);
-        QueueDel(uid);
+            Explode(ent, charge);
+        }
     }
 }
index a6123382fd9cec6b4c00fc18cd92915bcd7ab7f7..bd48e6cd977cf766aee62bd4c7d663eb36aac2e1 100644 (file)
@@ -2,6 +2,7 @@ using Content.Server.Administration;
 using Content.Server.Power.EntitySystems;
 using Content.Shared.Administration;
 using Content.Shared.Power.Components;
+using Content.Shared.Power.EntitySystems;
 using Robust.Shared.Console;
 
 namespace Content.Server.Power
@@ -10,6 +11,7 @@ namespace Content.Server.Power
     public sealed class SetBatteryPercentCommand : LocalizedEntityCommands
     {
         [Dependency] private readonly BatterySystem _batterySystem = default!;
+        [Dependency] private readonly PredictedBatterySystem _predictedBatterySystem = default!;
 
         public override string Command => "setbatterypercent";
 
@@ -35,12 +37,15 @@ namespace Content.Server.Power
                 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)));
                 return;
             }
-            _batterySystem.SetCharge((id.Value, battery), battery.MaxCharge * percent / 100);
             // Don't acknowledge b/c people WILL forall this
         }
     }
diff --git a/Content.Server/PowerCell/PowerCellSystem.Draw.cs b/Content.Server/PowerCell/PowerCellSystem.Draw.cs
deleted file mode 100644 (file)
index d0dafb1..0000000
+++ /dev/null
@@ -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);
-        }
-    }
-}
diff --git a/Content.Server/PowerCell/PowerCellSystem.cs b/Content.Server/PowerCell/PowerCellSystem.cs
deleted file mode 100644 (file)
index f358e1b..0000000
+++ /dev/null
@@ -1,257 +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.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);
-
-        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 battery);
-        OnBatteryExamined(uid, battery, args);
-    }
-
-    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);
-    }
-}
index 3ec2e9c38c2c09887a82d6825b6fa9c8ca39681f..750010c9ccf7bd5c48343525d8ed3bffaf9ccd99 100644 (file)
@@ -1,8 +1,7 @@
-using Content.Server.Power.EntitySystems;
-using Content.Server.PowerCell;
 using Content.Shared.DeviceNetwork.Components;
 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.Components;
 using Content.Shared.DeviceNetwork.Systems;
@@ -12,7 +11,7 @@ namespace Content.Server.Radio.EntitySystems;
 public sealed class JammerSystem : SharedJammerSystem
 {
     [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 SharedDeviceNetworkJammerSystem _jammer = default!;
 
@@ -25,6 +24,8 @@ public sealed class JammerSystem : SharedJammerSystem
         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)
     {
         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))
         {
 
-            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);
                     RemComp<ActiveRadioJammerComponent>(uid);
@@ -42,7 +43,7 @@ public sealed class JammerSystem : SharedJammerSystem
                 }
                 else
                 {
-                    var percentCharged = battery.CurrentCharge / battery.MaxCharge;
+                    var percentCharged = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge;
                     var chargeLevel = percentCharged switch
                     {
                         > 0.50f => RadioJammerChargeLevel.High,
@@ -64,7 +65,7 @@ public sealed class JammerSystem : SharedJammerSystem
 
         var activated = !HasComp<ActiveRadioJammerComponent>(ent) &&
             _powerCell.TryGetBatteryFromSlot(ent.Owner, out var battery) &&
-            battery.CurrentCharge > GetCurrentWattage(ent);
+            _battery.GetCharge(battery.Value.AsNullable()) > GetCurrentWattage(ent);
         if (activated)
         {
             ChangeLEDState(ent.Owner, true);
index 8dd074e6a01a591b2413b531b520a28e036dae54..1237919b2032b77f0cc5847f506543da59352a50 100644 (file)
@@ -27,10 +27,8 @@ public sealed partial class BorgSystem
         SubscribeLocalEvent<BorgTransponderComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
     }
 
-    public override void Update(float frameTime)
+    public void UpdateTransponder(float frameTime)
     {
-        base.Update(frameTime);
-
         var now = _timing.CurTime;
         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))
@@ -43,7 +41,7 @@ public sealed partial class BorgSystem
 
             var charge = 0f;
             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);
 
index 58cd7135af9221d7cf2dfdb0ce1f34e53f9c9248..d718916abfccd556bc888dd3587353d4dfd2e5c1 100644 (file)
@@ -91,6 +91,43 @@ public sealed partial class BorgSystem
         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)
     {
         if (!Resolve(uid, ref component))
@@ -101,10 +138,26 @@ public sealed partial class BorgSystem
         if (_powerCell.TryGetBatteryFromSlot(uid, out var battery))
         {
             hasBattery = true;
-            chargePercent = battery.CurrentCharge / battery.MaxCharge;
+            chargePercent = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge;
         }
 
         var state = new BorgBuiState(chargePercent, hasBattery);
         _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);
+        }
+    }
 }
index 6ae61acc57b8129d90ad1974bcd76c0655c6b50b..24fde24f33e357b9ba69f8757d941c7ca142b024 100644 (file)
@@ -5,7 +5,6 @@ using Content.Server.Administration.Logs;
 using Content.Server.Administration.Managers;
 using Content.Server.DeviceNetwork.Systems;
 using Content.Server.Hands.Systems;
-using Content.Server.PowerCell;
 using Content.Shared.Alert;
 using Content.Shared.Body.Events;
 using Content.Shared.Database;
@@ -18,6 +17,8 @@ using Content.Shared.Mobs;
 using Content.Shared.Mobs.Systems;
 using Content.Shared.Movement.Systems;
 using Content.Shared.Pointing;
+using Content.Shared.Power;
+using Content.Shared.Power.EntitySystems;
 using Content.Shared.PowerCell;
 using Content.Shared.PowerCell.Components;
 using Content.Shared.Roles;
@@ -61,6 +62,7 @@ public sealed partial class BorgSystem : SharedBorgSystem
     [Dependency] private readonly SharedContainerSystem _container = default!;
     [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
     [Dependency] private readonly ISharedPlayerManager _player = default!;
+    [Dependency] private readonly PredictedBatterySystem _battery = default!;
 
     public static readonly ProtoId<JobPrototype> BorgJobId = "Borg";
 
@@ -76,6 +78,7 @@ public sealed partial class BorgSystem : SharedBorgSystem
         SubscribeLocalEvent<BorgChassisComponent, MobStateChangedEvent>(OnMobStateChanged);
         SubscribeLocalEvent<BorgChassisComponent, BeingGibbedEvent>(OnBeingGibbed);
         SubscribeLocalEvent<BorgChassisComponent, PowerCellChangedEvent>(OnPowerCellChanged);
+        SubscribeLocalEvent<BorgChassisComponent, PredictedBatteryChargeChangedEvent>(OnBatteryChargeChanged);
         SubscribeLocalEvent<BorgChassisComponent, PowerCellSlotEmptyEvent>(OnPowerCellSlotEmpty);
         SubscribeLocalEvent<BorgChassisComponent, GetCharactedDeadIcEvent>(OnGetDeadIC);
         SubscribeLocalEvent<BorgChassisComponent, GetCharacterUnrevivableIcEvent>(OnGetUnrevivableIC);
@@ -209,17 +212,14 @@ public sealed partial class BorgSystem : SharedBorgSystem
         _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));
-
-        // if we aren't drawing and suddenly get enough power to draw again, reeanble.
-        if (_powerCell.HasDrawCharge(uid))
-        {
-            Toggle.TryActivate(uid);
-        }
+        UpdateBattery(ent);
+    }
 
-        UpdateUI(uid, component);
+    private void OnBatteryChargeChanged(Entity<BorgChassisComponent> ent, ref PredictedBatteryChargeChangedEvent args)
+    {
+        UpdateBattery(ent);
     }
 
     private void OnPowerCellSlotEmpty(EntityUid uid, BorgChassisComponent component, ref PowerCellSlotEmptyEvent args)
@@ -286,28 +286,6 @@ public sealed partial class BorgSystem : SharedBorgSystem
         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)
     {
         ents = null;
@@ -357,4 +335,12 @@ public sealed partial class BorgSystem : SharedBorgSystem
 
         return true;
     }
+
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        UpdateTransponder(frameTime);
+        UpdateBattery(frameTime);
+    }
 }
index a5e30346bd2a1a13b5b11b735e175ca98e6c10c8..99cd0ca9e22f0832313bb2f171a3893b6d0a0d6d 100644 (file)
@@ -1,9 +1,10 @@
 using System.Text;
 using Content.Server.Destructible;
-using Content.Server.PowerCell;
 using Content.Shared.Speech.Components;
 using Content.Shared.Damage.Components;
 using Content.Shared.FixedPoint;
+using Content.Shared.Power.EntitySystems;
+using Content.Shared.PowerCell;
 using Content.Shared.Speech;
 using Robust.Shared.Random;
 
@@ -12,6 +13,7 @@ namespace Content.Server.Speech.EntitySystems;
 public sealed class DamagedSiliconAccentSystem : EntitySystem
 {
     [Dependency] private readonly IRobustRandom _random = default!;
+    [Dependency] private readonly PredictedBatterySystem _battery = default!;
     [Dependency] private readonly PowerCellSystem _powerCell = default!;
     [Dependency] private readonly DestructibleSystem _destructibleSystem = default!;
 
@@ -34,7 +36,7 @@ public sealed class DamagedSiliconAccentSystem : EntitySystem
             }
             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);
             // Corrupt due to low power (drops characters on longer messages)
index a22a4c09fab540a7cd1abb65efd51b66bf393dce..b1bae0127c45455eed4fa7c9a4535c274824c65c 100644 (file)
@@ -1,6 +1,6 @@
 using Content.Server.Power.Components;
-using Content.Server.Power.EntitySystems;
 using Content.Server.Power.Events;
+using Content.Server.Power.EntitySystems;
 using Content.Shared.Chemistry.EntitySystems;
 using Content.Shared.Damage.Events;
 using Content.Shared.Examine;
@@ -9,6 +9,7 @@ using Content.Shared.Item.ItemToggle.Components;
 using Content.Shared.Popups;
 using Content.Shared.Power;
 using Content.Shared.Power.Components;
+using Content.Shared.Power.EntitySystems;
 using Content.Shared.Stunnable;
 
 namespace Content.Server.Stunnable.Systems
@@ -17,7 +18,7 @@ namespace Content.Server.Stunnable.Systems
     {
         [Dependency] private readonly RiggableSystem _riggableSystem = 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!;
 
         public override void Initialize()
@@ -27,13 +28,13 @@ namespace Content.Server.Stunnable.Systems
             SubscribeLocalEvent<StunbatonComponent, ExaminedEvent>(OnExamined);
             SubscribeLocalEvent<StunbatonComponent, SolutionContainerChangedEvent>(OnSolutionChange);
             SubscribeLocalEvent<StunbatonComponent, StaminaDamageOnHitAttemptEvent>(OnStaminaHitAttempt);
-            SubscribeLocalEvent<StunbatonComponent, ChargeChangedEvent>(OnChargeChanged);
+            SubscribeLocalEvent<StunbatonComponent, PredictedBatteryChargeChangedEvent>(OnChargeChanged);
         }
 
         private void OnStaminaHitAttempt(Entity<StunbatonComponent> entity, ref StaminaDamageOnHitAttemptEvent args)
         {
             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;
             }
@@ -46,9 +47,9 @@ namespace Content.Server.Stunnable.Systems
             : Loc.GetString("comp-stunbaton-examined-off");
             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)));
             }
         }
@@ -57,7 +58,7 @@ namespace Content.Server.Stunnable.Systems
         {
             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;
                 if (args.User != null)
@@ -69,7 +70,7 @@ namespace Content.Server.Stunnable.Systems
 
             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.
             if (!TryComp<RiggableComponent>(entity, out var riggable) ||
-                !TryComp<BatteryComponent>(entity, out var battery))
+                !TryComp<PredictedBatteryComponent>(entity, out var battery))
                 return;
 
             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)
         {
             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) &&
-                battery.CurrentCharge < entity.Comp.EnergyPerUse)
+            if (TryComp<PredictedBatteryComponent>(entity.Owner, out var battery) &&
+                _battery.GetCharge((entity.Owner, battery)) < entity.Comp.EnergyPerUse)
             {
                 _itemToggle.TryDeactivate(entity.Owner, predicted: false);
             }
index 2bf53d46f4bdc24f0f43cab3248c016fa8214fd2..d1984448c5d05ac3054567c30ae54b66c655e1b9 100644 (file)
@@ -1,4 +1,3 @@
-using Content.Server.PowerCell;
 using Content.Shared.Item.ItemToggle;
 using Content.Shared.PowerCell;
 using Content.Shared.Weapons.Misc;
diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.Battery.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.Battery.cs
deleted file mode 100644 (file)
index c1e442c..0000000
+++ /dev/null
@@ -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);
-    }
-}
index 21952912d73844672641cfa80e53dcd59a826836..c3f4926a127ebee05020948314fd12fdf5351037 100644 (file)
@@ -1,6 +1,7 @@
 using Content.Server.Power.EntitySystems;
 using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
 using Content.Shared.Power.Components;
+using Content.Shared.Power.EntitySystems;
 using Content.Shared.Xenoarchaeology.Artifact;
 using Content.Shared.Xenoarchaeology.Artifact.XAE;
 
@@ -12,20 +13,29 @@ namespace Content.Server.Xenoarchaeology.Artifact.XAE;
 public sealed class XAEChargeBatterySystem : BaseXAESystem<XAEChargeBatteryComponent>
 {
     [Dependency] private readonly BatterySystem _battery = default!;
+    [Dependency] private readonly PredictedBatterySystem _predictedBattery = default!;
     [Dependency] private readonly EntityLookupSystem _lookup = default!;
 
     /// <summary> Pre-allocated and re-used collection.</summary>
     private readonly HashSet<Entity<BatteryComponent>> _batteryEntities = new();
+    private readonly HashSet<Entity<PredictedBatteryComponent>> _pBatteryEntities = new();
 
     /// <inheritdoc />
     protected override void OnActivated(Entity<XAEChargeBatteryComponent> ent, ref XenoArtifactNodeActivatedEvent args)
     {
-        var chargeBatteryComponent = ent.Comp;
         _batteryEntities.Clear();
-        _lookup.GetEntitiesInRange(args.Coordinates, chargeBatteryComponent.Radius, _batteryEntities);
+        _pBatteryEntities.Clear();
+
+        _lookup.GetEntitiesInRange(args.Coordinates, ent.Comp.Radius, _batteryEntities);
         foreach (var battery in _batteryEntities)
         {
             _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);
+        }
     }
 }
diff --git a/Content.Shared/Kitchen/BeingMicrowavedEvent.cs b/Content.Shared/Kitchen/BeingMicrowavedEvent.cs
new file mode 100644 (file)
index 0000000..c053eda
--- /dev/null
@@ -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;
+}
index c461b588b1b6e546d62f4f3846fee7f6cf7427e5..f2ba8c69f5e3a184dcae1241cdab85ae1a32b425 100644 (file)
@@ -283,6 +283,7 @@ public abstract partial class SharedMechSystem : EntitySystem
 
     /// <summary>
     /// 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>
     /// <param name="uid">The mech itself</param>
     /// <param name="delta">The change in energy</param>
index cc9dc49a7c829e5ebfc9f534d26f4a7cb76b0204..f84b255f12fc455ec02cdc2e3a2e6a5988e1a914 100644 (file)
@@ -5,14 +5,44 @@ namespace Content.Shared.Power;
 
 /// <summary>
 /// Raised when a battery's charge or capacity changes (capacity affects relative charge percentage).
+/// Only raised for entities with <see cref="BatteryComponent"/>.
 /// </summary>
 [ByRefEvent]
 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>
 /// Event that supports multiple battery types.
 /// 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.
 /// </summary>
 [ByRefEvent]
@@ -25,7 +55,7 @@ public record struct GetChargeEvent
 /// <summary>
 /// Method event that supports multiple battery types.
 /// 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.
 /// </summary>
 [ByRefEvent]
index 6a65405115d26e69d3869553e158c42f3af78c78..396896a6c5b9123f52868a8acbe3d5a21b1e6209 100644 (file)
@@ -5,6 +5,8 @@ namespace Content.Shared.Power.Components;
 
 /// <summary>
 /// 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>
 [RegisterComponent]
 [Virtual]
index 38819803824094dcfe37ab15860b2b0887d27301..7a5665ae82d3a2cf11b01156185d096e97b2d990 100644 (file)
@@ -5,6 +5,7 @@ namespace Content.Shared.Power.Components;
 /// <summary>
 /// Self-recharging battery.
 /// To be used in combination with <see cref="BatteryComponent"/>.
+/// For <see cref="PredictedBatteryComponent"/> use <see cref="PredictedBatterySelfRechargerComponent"/> instead.
 /// </summary>
 [RegisterComponent, AutoGenerateComponentPause]
 public sealed partial class BatterySelfRechargerComponent : Component
@@ -16,7 +17,7 @@ public sealed partial class BatterySelfRechargerComponent : Component
     public bool AutoRecharge = true;
 
     /// <summary>
-    /// At what rate does the entity automatically recharge?
+    /// At what rate does the entity automatically recharge? In watts.
     /// </summary>
     [DataField]
     public float AutoRechargeRate;
index a3f2f8f424351fdb19009a248060dbeb684d642f..930ff059f35d9e0110d2639867bd854f4f537c6b 100644 (file)
@@ -1,20 +1,25 @@
 using Content.Shared.Whitelist;
 using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
 
 namespace Content.Shared.Power.Components;
 
-[RegisterComponent, NetworkedComponent]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
 public sealed partial class ChargerComponent : Component
 {
-    [ViewVariables]
-    public CellChargerStatus Status;
-
     /// <summary>
-    /// The charge rate of the charger, in watts
+    /// The charge rate of the charger, in watts.
     /// </summary>
-    [DataField]
+    [DataField, AutoNetworkedField]
     public float ChargeRate = 20.0f;
 
+    /// <summary>
+    /// 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>
+    [DataField, AutoNetworkedField]
+    public float PassiveDraw = 1f;
+
     /// <summary>
     /// The container ID that is holds the entities being charged.
     /// </summary>
@@ -24,13 +29,29 @@ public sealed partial class ChargerComponent : Component
     /// <summary>
     /// A whitelist for what entities can be charged by this Charger.
     /// </summary>
-    [DataField]
+    [DataField, AutoNetworkedField]
     public EntityWhitelist? Whitelist;
 
     /// <summary>
     /// Indicates whether the charger is portable and thus subject to EMP effects
     /// and bypasses checks for transform, anchored, and ApcPowerReceiverComponent.
     /// </summary>
-    [DataField]
+    [DataField, AutoNetworkedField]
     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,
+}
diff --git a/Content.Shared/Power/Components/ExaminableBatteryComponent.cs b/Content.Shared/Power/Components/ExaminableBatteryComponent.cs
new file mode 100644 (file)
index 0000000..59d0b87
--- /dev/null
@@ -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;
diff --git a/Content.Shared/Power/Components/InsideChargerComponent.cs b/Content.Shared/Power/Components/InsideChargerComponent.cs
new file mode 100644 (file)
index 0000000..64e5702
--- /dev/null
@@ -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;
diff --git a/Content.Shared/Power/Components/PredictedBatteryComponent.cs b/Content.Shared/Power/Components/PredictedBatteryComponent.cs
new file mode 100644 (file)
index 0000000..0db5324
--- /dev/null
@@ -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,
+}
+
diff --git a/Content.Shared/Power/Components/PredictedBatterySelfRechargerComponent.cs b/Content.Shared/Power/Components/PredictedBatterySelfRechargerComponent.cs
new file mode 100644 (file)
index 0000000..449a4e1
--- /dev/null
@@ -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);
+}
diff --git a/Content.Shared/Power/Components/PredictedBatteryVisualsComponent.cs b/Content.Shared/Power/Components/PredictedBatteryVisualsComponent.cs
new file mode 100644 (file)
index 0000000..f7ea933
--- /dev/null
@@ -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,
+}
diff --git a/Content.Shared/Power/EntitySystems/ChargerSystem.cs b/Content.Shared/Power/EntitySystems/ChargerSystem.cs
new file mode 100644 (file)
index 0000000..54dac61
--- /dev/null
@@ -0,0 +1,270 @@
+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));
+    }
+
+    private 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;
+
+        battery = null;
+        return false;
+    }
+
+    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;
+    }
+}
diff --git a/Content.Shared/Power/EntitySystems/PredictedBatterySystem.API.cs b/Content.Shared/Power/EntitySystems/PredictedBatterySystem.API.cs
new file mode 100644 (file)
index 0000000..c06b458
--- /dev/null
@@ -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;
+    }
+}
diff --git a/Content.Shared/Power/EntitySystems/PredictedBatterySystem.cs b/Content.Shared/Power/EntitySystems/PredictedBatterySystem.cs
new file mode 100644 (file)
index 0000000..760d727
--- /dev/null
@@ -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);
+    }
+}
index d067a685d496e7d67c40fdaec60d240003eaac1a..317dcb129ec5faa099c69fa07a18948bdb212bd5 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Shared.Emp;
 using Content.Shared.Power.Components;
+using JetBrains.Annotations;
 
 namespace Content.Shared.Power.EntitySystems;
 
@@ -21,19 +22,23 @@ public abstract class SharedBatterySystem : EntitySystem
     }
 
     /// <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.
     /// </summary>
     /// <returns>The actually changed amount.</returns>
+    [PublicAPI]
     public virtual float ChangeCharge(Entity<BatteryComponent?> ent, float amount)
     {
         return 0f;
     }
 
     /// <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>
     /// <returns>The actually changed amount.</returns>
+    [PublicAPI]
     public virtual float UseCharge(Entity<BatteryComponent?> ent, float amount)
     {
         return 0f;
@@ -41,9 +46,11 @@ public abstract class SharedBatterySystem : EntitySystem
 
     /// <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 virtual bool TryUseCharge(Entity<BatteryComponent?> ent, float amount)
     {
         return false;
@@ -52,21 +59,25 @@ public abstract class SharedBatterySystem : EntitySystem
     /// <summary>
     /// Sets the battery's charge.
     /// </summary>
+    [PublicAPI]
     public virtual void SetCharge(Entity<BatteryComponent?> ent, float value) { }
 
     /// <summary>
     /// Sets the battery's maximum charge.
     /// </summary>
+    [PublicAPI]
     public virtual void SetMaxCharge(Entity<BatteryComponent?> ent, float value) { }
 
     /// <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 virtual void TrySetChargeCooldown(Entity<BatterySelfRechargerComponent?> ent) { }
 
     /// <summary>
     /// Puts the entity's self recharge on cooldown for the specified time.
     /// </summary>
+    [PublicAPI]
     public virtual void SetChargeCooldown(Entity<BatterySelfRechargerComponent?> ent, TimeSpan cooldown) { }
 }
diff --git a/Content.Shared/Power/EntitySystems/SharedChargerSystem.cs b/Content.Shared/Power/EntitySystems/SharedChargerSystem.cs
deleted file mode 100644 (file)
index a150436..0000000
+++ /dev/null
@@ -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;
-    }
-}
index 4a66a6ea97c4c44839d33f414b00e05442d77f53..c3ab0ce26dd4d7df421475031adfeb1efd8574de 100644 (file)
@@ -92,8 +92,19 @@ public abstract class SharedPowerReceiverSystem : EntitySystem
         // NOOP on server because client has 0 idea of load so we can't raise it properly in shared.
     }
 
-       /// <summary>
-       /// Checks if entity is APC-powered device, and if it have power.
+    /// <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>
+    /// Checks if entity is APC-powered device, and if it have power.
     /// </summary>
     public bool IsPowered(Entity<SharedApcPowerReceiverComponent?> entity)
     {
diff --git a/Content.Shared/Power/SharedPowerItemCharger.cs b/Content.Shared/Power/SharedPowerItemCharger.cs
deleted file mode 100644 (file)
index ec51a04..0000000
+++ /dev/null
@@ -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,
-    }
-}
index 3d4f0472f8cbed936da6d5e7c29f5151624134dc..fdada109588e9202e42e639e76978a437f15ae43 100644 (file)
@@ -1,26 +1,11 @@
+using Content.Shared.Power.Components;
 using Robust.Shared.GameStates;
-using Robust.Shared.Serialization;
 
-namespace Content.Shared.PowerCell;
+namespace Content.Shared.PowerCell.Components;
 
 /// <summary>
-///     This component enables power-cell related interactions (e.g., entity white-lists, cell sizes, examine, rigging).
-///     The actual power functionality is provided by the server-side BatteryComponent.
+/// This component enables power-cell related interactions (e.g. EntityWhitelists, cell sizes, examine, rigging).
+/// The actual power functionality is provided by the <see cref="PredictedBatteryComponent"/>.
 /// </summary>
-[NetworkedComponent]
-[RegisterComponent]
-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
-}
+[RegisterComponent, NetworkedComponent]
+public sealed partial class PowerCellComponent : Component;
diff --git a/Content.Shared/PowerCell/Components/PowerCellDrawComponent.cs b/Content.Shared/PowerCell/Components/PowerCellDrawComponent.cs
new file mode 100644 (file)
index 0000000..d09c3ed
--- /dev/null
@@ -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;
+}
index 5f21b397a759ce7b82c5e8d0a578e0c949d66fec..b0aad8956967d644555ec67a95a7ef4c9ffed7cc 100644 (file)
@@ -1,8 +1,9 @@
 using Content.Shared.Containers.ItemSlots;
+using Robust.Shared.GameStates;
 
 namespace Content.Shared.PowerCell.Components;
 
-[RegisterComponent]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
 public sealed partial class PowerCellSlotComponent : Component
 {
     /// <summary>
@@ -10,29 +11,17 @@ public sealed partial class PowerCellSlotComponent : Component
     /// </summary>
     /// <remarks>
     /// 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>
-    [DataField("cellSlotId", required: true)]
+    [DataField(required: true)]
     public string CellSlotId = string.Empty;
 
     /// <summary>
     /// Can this entity be inserted directly into a charging station? If false, you need to manually remove the power
     /// cell and recharge it separately.
     /// </summary>
-    [DataField("fitsInCharger")]
+    [DataField, AutoNetworkedField]
     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;
-    }
-}
diff --git a/Content.Shared/PowerCell/PowerCellDrawComponent.cs b/Content.Shared/PowerCell/PowerCellDrawComponent.cs
deleted file mode 100644 (file)
index cecf23d..0000000
+++ /dev/null
@@ -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);
-}
similarity index 53%
rename from Content.Shared/PowerCell/PowerCellSlotEmptyEvent.cs
rename to Content.Shared/PowerCell/PowerCellEvents.cs
index e4075175aef7e4f5ad07537cfde981d6f34bab71..0d3af9b969f4b54c0727d8cc0fee5fb762e193b8 100644 (file)
@@ -5,3 +5,9 @@ namespace Content.Shared.PowerCell;
 /// </summary>
 [ByRefEvent]
 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);
diff --git a/Content.Shared/PowerCell/PowerCellSystem.API.cs b/Content.Shared/PowerCell/PowerCellSystem.API.cs
new file mode 100644 (file)
index 0000000..98b24ed
--- /dev/null
@@ -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);
+    }
+}
diff --git a/Content.Shared/PowerCell/PowerCellSystem.Draw.cs b/Content.Shared/PowerCell/PowerCellSystem.Draw.cs
new file mode 100644 (file)
index 0000000..8790ec9
--- /dev/null
@@ -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);
+    }
+}
diff --git a/Content.Shared/PowerCell/PowerCellSystem.Relay.cs b/Content.Shared/PowerCell/PowerCellSystem.Relay.cs
new file mode 100644 (file)
index 0000000..ca4bc2a
--- /dev/null
@@ -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);
+    }
+}
diff --git a/Content.Shared/PowerCell/PowerCellSystem.cs b/Content.Shared/PowerCell/PowerCellSystem.cs
new file mode 100644 (file)
index 0000000..dfc2dfd
--- /dev/null
@@ -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);
+    }
+}
diff --git a/Content.Shared/PowerCell/SharedPowerCellSystem.cs b/Content.Shared/PowerCell/SharedPowerCellSystem.cs
deleted file mode 100644 (file)
index a32687c..0000000
+++ /dev/null
@@ -1,118 +0,0 @@
-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!;
-
-    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;
-        // 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);
-}
index 14d91d2f5fb8f0ae59d1e7ba0e92ee2c0ccaf948..9c50a8aa601637b3f3085b2622da4524b37c481e 100644 (file)
@@ -10,7 +10,7 @@ namespace Content.Shared.PowerCell;
 public sealed class ToggleCellDrawSystem : EntitySystem
 {
     [Dependency] private readonly ItemToggleSystem _toggle = default!;
-    [Dependency] private readonly SharedPowerCellSystem _cell = default!;
+    [Dependency] private readonly PowerCellSystem _cell = default!;
 
     public override void Initialize()
     {
@@ -29,8 +29,8 @@ public sealed class ToggleCellDrawSystem : EntitySystem
 
     private void OnActivateAttempt(Entity<ToggleCellDrawComponent> ent, ref ItemToggleActivateAttemptEvent args)
     {
-        if (!_cell.HasDrawCharge(ent, user: args.User)
-            || !_cell.HasActivatableCharge(ent, user: args.User))
+        if (!_cell.HasDrawCharge(ent.Owner, user: args.User, predicted: true)
+            || !_cell.HasActivatableCharge(ent.Owner, user: args.User, predicted: true))
             args.Cancelled = true;
     }
 
index f562ddefdd0741ba9058ad2f8130dff36e366344..98a5b205e9252a415ee4ef41801576984c1c8494 100644 (file)
@@ -4,6 +4,7 @@ using Robust.Shared.Containers;
 using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
 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
 /// for borg logic.
 /// </summary>
-[RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem)), AutoGenerateComponentState]
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem))]
+[AutoGenerateComponentState, AutoGenerateComponentPause]
 public sealed partial class BorgChassisComponent : Component
 {
     #region Brain
@@ -79,6 +81,14 @@ public sealed partial class BorgChassisComponent : Component
     [DataField]
     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>
     /// If the entity can open own UI.
     /// </summary>
index 628cc85252c7e104d9f15d31455541dd56c099b5..009083b2d86274b3e12879b31993fb8937885e9b 100644 (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.
 /// </summary>
 [ByRefEvent]
-public record struct InsertIntoEntityStorageAttemptEvent(EntityUid ItemToInsert, bool Cancelled = false);
+public record struct InsertIntoEntityStorageAttemptEvent(BaseContainer Container, EntityUid ItemToInsert, bool Cancelled = false);
 
 /// <summary>
 /// Raised on the entity storage whenever checking if an entity can be inserted into it.
 /// </summary>
 [ByRefEvent]
-public record struct EntityStorageInsertedIntoAttemptEvent(EntityUid ItemToInsert, bool Cancelled = false);
+public record struct EntityStorageInsertedIntoAttemptEvent(BaseContainer Container, EntityUid ItemToInsert, bool Cancelled = false);
 
 /// <summary>
 /// Raised on the Container's owner whenever an entity storage tries to dump its
index 066cd2d88615b9704a75904c92406fc562a7c103..dd6f1a223e8afe01e3b3395c677f4eed7a7acab1 100644 (file)
@@ -341,14 +341,14 @@ public abstract class SharedEntityStorageSystem : EntitySystem
             return false;
 
         // 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);
 
         if (attemptEvent.Cancelled)
             return false;
 
         // 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);
 
         if (containerAttemptEvent.Cancelled)
index aa9e561e076f7f0677c6e52cd091fa1a01118782..39b9a01955c08c110e256a440cce4c9d3753284c 100644 (file)
@@ -1,13 +1,10 @@
-using Content.Shared.PowerCell;
+using Content.Shared.PowerCell.Components;
 using Robust.Shared.GameStates;
 
 namespace Content.Shared.UserInterface;
 
 /// <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>
 [RegisterComponent, NetworkedComponent]
-public sealed partial class ActivatableUIRequiresPowerCellComponent : Component
-{
-
-}
+public sealed partial class ActivatableUIRequiresPowerCellComponent : Component;
index e494253c8321b0c0607e87af563d793b61f55c86..3e52e3ed36f030dcf9788044773c4ab1be78003f 100644 (file)
@@ -1,27 +1,29 @@
 using Content.Shared.Item.ItemToggle;
 using Content.Shared.Item.ItemToggle.Components;
 using Content.Shared.PowerCell;
-using Robust.Shared.Containers;
+using Content.Shared.Power;
+using Content.Shared.Power.Components;
 
 namespace Content.Shared.UserInterface;
 
 public sealed partial class ActivatableUISystem
 {
     [Dependency] private readonly ItemToggleSystem _toggle = default!;
-    [Dependency] private readonly SharedPowerCellSystem _cell = default!;
+    [Dependency] private readonly PowerCellSystem _cell = default!;
 
     private void InitializePower()
     {
-        SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, ActivatableUIOpenAttemptEvent>(OnBatteryOpenAttempt);
+        SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, ItemToggledEvent>(OnToggled);
         SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, BoundUIOpenedEvent>(OnBatteryOpened);
         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)
     {
         // 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;
 
         if (activatable.Key == null)
@@ -55,35 +57,25 @@ public sealed partial class ActivatableUISystem
             _toggle.TryDeactivate(uid);
     }
 
-    /// <summary>
-    /// 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)
+    private void OnBatteryStateChanged(Entity<ActivatableUIRequiresPowerCellComponent> ent, ref PredictedBatteryStateChangedEvent args)
     {
-        if (!Resolve(uid, ref component, ref draw, ref active, false))
-            return;
-
-        if (active.Key == null)
-        {
-            Log.Error($"Encountered null key in activatable ui on entity {ToPrettyString(uid)}");
-            return;
-        }
-
-        if (_cell.HasActivatableCharge(uid))
+        // Deactivate when empty.
+        if (args.NewState != BatteryState.Empty)
             return;
 
-        _uiSystem.CloseUi(uid, active.Key);
+        var activatable = Comp<ActivatableUIComponent>(ent);
+        if (activatable.Key != null)
+            _uiSystem.CloseUi(ent.Owner, activatable.Key);
     }
 
     private void OnBatteryOpenAttempt(EntityUid uid, ActivatableUIRequiresPowerCellComponent component, ActivatableUIOpenAttemptEvent args)
     {
-        if (!TryComp<PowerCellDrawComponent>(uid, out var draw))
+        if (args.Cancelled)
             return;
 
         // Check if we have the appropriate drawrate / userate to even open it.
-        if (args.Cancelled ||
-            !_cell.HasActivatableCharge(uid, draw, user: args.User) ||
-            !_cell.HasDrawCharge(uid, draw, user: args.User))
+        if (!_cell.HasActivatableCharge(uid, user: args.User, predicted: true) ||
+            !_cell.HasDrawCharge(uid, user: args.User, predicted: true))
         {
             args.Cancel();
         }
index 605e169c38dd7b65946c74d8654db1dceb7e38af..3378d82b44741c657d40e05b59bd40372c5e3984 100644 (file)
@@ -1,18 +1,70 @@
+using Content.Shared.Power.Components;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
 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>
-    /// How much battery it costs to fire once.
+    /// The projectile or hitscan entity to spawn when firing.
+    /// </summary>
+    [DataField("proto", required: true)]
+    public EntProtoId Prototype;
+
+    /// <summary>
+    /// How much charge it costs to fire once, in watts.
     /// </summary>
-    [DataField("fireCost"), ViewVariables(VVAccess.ReadWrite)]
+    [DataField, AutoNetworkedField]
     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;
 
-    [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;
 }
diff --git a/Content.Shared/Weapons/Ranged/Components/HitscanBatteryAmmoProviderComponent.cs b/Content.Shared/Weapons/Ranged/Components/HitscanBatteryAmmoProviderComponent.cs
deleted file mode 100644 (file)
index cdbf514..0000000
+++ /dev/null
@@ -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;
-}
diff --git a/Content.Shared/Weapons/Ranged/Components/ProjectileBatteryAmmoProviderComponent.cs b/Content.Shared/Weapons/Ranged/Components/ProjectileBatteryAmmoProviderComponent.cs
deleted file mode 100644 (file)
index d0de436..0000000
+++ /dev/null
@@ -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!;
-}
diff --git a/Content.Shared/Weapons/Ranged/Events/UpdateClientAmmoEvent.cs b/Content.Shared/Weapons/Ranged/Events/UpdateClientAmmoEvent.cs
deleted file mode 100644 (file)
index 57f7318..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-namespace Content.Shared.Weapons.Ranged.Events;
-
-[ByRefEvent]
-public readonly record struct UpdateClientAmmoEvent();
\ No newline at end of file
index 974bfa1783694eaecd3ae55eb2b7a270089da8d6..81f4ad3213383da098d9c4d567da7836fceca90c 100644 (file)
@@ -17,6 +17,7 @@ public sealed class BatteryWeaponFireModesSystem : EntitySystem
     [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
     [Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!;
     [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
+    [Dependency] private readonly SharedGunSystem _gun = default!;
 
     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);
         }
 
-        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.
-            var OldFireCost = projectileBatteryAmmoProviderComponent.FireCost;
-            projectileBatteryAmmoProviderComponent.Prototype = fireMode.Prototype;
-            projectileBatteryAmmoProviderComponent.FireCost = fireMode.FireCost;
+            batteryAmmoProviderComponent.Prototype = fireMode.Prototype;
+            batteryAmmoProviderComponent.FireCost = fireMode.FireCost;
 
-            float FireCostDiff = (float)fireMode.FireCost / (float)OldFireCost;
-            projectileBatteryAmmoProviderComponent.Shots = (int)Math.Round(projectileBatteryAmmoProviderComponent.Shots / FireCostDiff);
-            projectileBatteryAmmoProviderComponent.Capacity = (int)Math.Round(projectileBatteryAmmoProviderComponent.Capacity / FireCostDiff);
+            Dirty(uid, batteryAmmoProviderComponent);
 
-            Dirty(uid, projectileBatteryAmmoProviderComponent);
-
-            var updateClientAmmoEvent = new UpdateClientAmmoEvent();
-            RaiseLocalEvent(uid, ref updateClientAmmoEvent);
+            _gun.UpdateShots((uid, batteryAmmoProviderComponent));
         }
     }
 }
index 663f5f1faa2924dea266f603d8f7a9a601292a72..24cfe350518865a8dfeda60ff1f90d91f7be4d6b 100644 (file)
@@ -2,13 +2,13 @@ using Content.Shared.Damage;
 using Content.Shared.Damage.Events;
 using Content.Shared.Examine;
 using Content.Shared.Projectiles;
+using Content.Shared.Power;
+using Content.Shared.PowerCell;
 using Content.Shared.Weapons.Hitscan.Components;
 using Content.Shared.Weapons.Ranged.Components;
 using Content.Shared.Weapons.Ranged.Events;
-using Robust.Shared.GameStates;
 using Robust.Shared.Map;
 using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization;
 
 namespace Content.Shared.Weapons.Ranged.Systems;
 
@@ -16,160 +16,202 @@ public abstract partial class SharedGunSystem
 {
     protected virtual void InitializeBattery()
     {
-        // Trying to dump comp references hence the below
-        // Hitscan
-        SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, ComponentGetState>(OnBatteryGetState);
-        SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, ComponentHandleState>(OnBatteryHandleState);
-        SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, TakeAmmoEvent>(OnBatteryTakeAmmo);
-        SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, GetAmmoCountEvent>(OnBatteryAmmoCount);
-        SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, ExaminedEvent>(OnBatteryExamine);
-        SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, DamageExamineEvent>(OnBatteryDamageExamine);
-
-        // 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);
+        SubscribeLocalEvent<BatteryAmmoProviderComponent, ComponentStartup>(OnBatteryStartup);
+        SubscribeLocalEvent<BatteryAmmoProviderComponent, AfterAutoHandleStateEvent>(OnAfterAutoHandleState);
+        SubscribeLocalEvent<BatteryAmmoProviderComponent, TakeAmmoEvent>(OnBatteryTakeAmmo);
+        SubscribeLocalEvent<BatteryAmmoProviderComponent, GetAmmoCountEvent>(OnBatteryAmmoCount);
+        SubscribeLocalEvent<BatteryAmmoProviderComponent, ExaminedEvent>(OnBatteryExamine);
+        SubscribeLocalEvent<BatteryAmmoProviderComponent, DamageExamineEvent>(OnBatteryDamageExamine);
+        SubscribeLocalEvent<BatteryAmmoProviderComponent, PowerCellChangedEvent>(OnPowerCellChanged);
+        SubscribeLocalEvent<BatteryAmmoProviderComponent, PredictedBatteryChargeChangedEvent>(OnPredictedChargeChanged);
+        SubscribeLocalEvent<BatteryAmmoProviderComponent, ChargeChangedEvent>(OnChargeChanged);
     }
 
-    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)
-            return;
-
-        component.Shots = state.Shots;
-        component.Capacity = state.MaxShots;
-        component.FireCost = state.FireCost;
-        UpdateAmmoCount(uid, prediction: false);
+        args.PushMarkup(Loc.GetString("gun-battery-examine", ("color", AmmoExamineColor), ("count", ent.Comp.Shots)));
     }
 
-    private void OnBatteryGetState(EntityUid uid, BatteryAmmoProviderComponent component, ref ComponentGetState args)
+    private void OnBatteryDamageExamine(Entity<BatteryAmmoProviderComponent> ent, ref DamageExamineEvent args)
     {
-        args.State = new BatteryAmmoProviderComponentState()
+        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,
-            MaxShots = component.Capacity,
-            FireCost = component.FireCost,
-        };
-    }
+            if (!projectileComp.Damage.Empty)
+            {
+                damageType = Loc.GetString("damage-projectile");
+                damageSpec = projectileComp.Damage * Damageable.UniversalProjectileDamageModifier;
+            }
+        }
+        else if (proto.TryGetComponent<HitscanBasicDamageComponent>(out var hitscanComp, Factory))
+        {
+            if (!hitscanComp.Damage.Empty)
+            {
+                damageType = Loc.GetString("damage-hitscan");
+                damageSpec = hitscanComp.Damage * Damageable.UniversalHitscanDamageModifier;
+            }
+        }
+        if (damageSpec == null)
+            return;
 
-    private void OnBatteryExamine(EntityUid uid, BatteryAmmoProviderComponent component, ExaminedEvent args)
-    {
-        args.PushMarkup(Loc.GetString("gun-battery-examine", ("color", AmmoExamineColor), ("count", component.Shots)));
+        _damageExamine.AddDamageExamine(args.Message, Damageable.ApplyUniversalAllModifiers(damageSpec), damageType);
     }
 
-    private void OnBatteryDamageExamine<T>(Entity<T> entity, ref DamageExamineEvent args) where T : BatteryAmmoProviderComponent
+    private void OnBatteryTakeAmmo(Entity<BatteryAmmoProviderComponent> ent, ref TakeAmmoEvent args)
     {
-        var damageSpec = GetDamage(entity.Comp);
+        var shots = Math.Min(args.Shots, ent.Comp.Shots);
 
-        if (damageSpec == null)
+        if (shots == 0)
             return;
 
-        var damageType = entity.Comp switch
+        for (var i = 0; i < shots; i++)
         {
-            HitscanBatteryAmmoProviderComponent => Loc.GetString("damage-hitscan"),
-            ProjectileBatteryAmmoProviderComponent => Loc.GetString("damage-projectile"),
-            _ => throw new ArgumentOutOfRangeException(),
-        };
+            args.Ammo.Add(GetShootable(ent, args.Coordinates));
+        }
 
-        _damageExamine.AddDamageExamine(args.Message, Damageable.ApplyUniversalAllModifiers(damageSpec), damageType);
+        TakeCharge(ent, shots);
     }
 
-    private DamageSpecifier? GetDamage(BatteryAmmoProviderComponent component)
+    private void OnBatteryAmmoCount(Entity<BatteryAmmoProviderComponent> ent, ref GetAmmoCountEvent args)
     {
-        if (component is ProjectileBatteryAmmoProviderComponent battery)
-        {
-            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;
-        }
+        args.Count = ent.Comp.Shots;
+        args.Capacity = ent.Comp.Capacity;
+    }
 
-        if (component is HitscanBatteryAmmoProviderComponent hitscan)
-        {
-            var dmg = ProtoManager.Index(hitscan.HitscanEntityProto);
-            if (!dmg.TryGetComponent<HitscanBasicDamageComponent>(out var basicDamageComp, Factory))
-                return null;
+    /// <summary>
+    /// Use up the required amount of battery charge for firing.
+    /// </summary>
+    public void TakeCharge(Entity<BatteryAmmoProviderComponent> ent, int shots = 1)
+    {
+        // Take charge from either the BatteryComponent, PredictedBatteryComponent or PowerCellSlotComponent.
+        var ev = new ChangeChargeEvent(-ent.Comp.FireCost * shots);
+        RaiseLocalEvent(ent, ref ev);
+        // UpdateShots is already called by the resulting PredictedBatteryChargeChangedEvent or ChargeChangedEvent
+    }
 
-            return basicDamageComp.Damage * Damageable.UniversalHitscanDamageModifier;
-        }
+    private (EntityUid? Entity, IShootable) GetShootable(BatteryAmmoProviderComponent component, EntityCoordinates coordinates)
+    {
 
-        return null;
+        var ent = Spawn(component.Prototype, coordinates);
+        return (ent, EnsureShootable(ent));
     }
 
-    private void OnBatteryTakeAmmo(EntityUid uid, BatteryAmmoProviderComponent component, TakeAmmoEvent args)
+    public void UpdateShots(Entity<BatteryAmmoProviderComponent> ent)
     {
-        var shots = Math.Min(args.Shots, component.Shots);
+        var oldShots = ent.Comp.Shots;
+        var oldCapacity = ent.Comp.Capacity;
+        (var newShots, var newCapacity) = GetShots(ent);
 
-        // Don't dirty if it's an empty fire.
-        if (shots == 0)
+        // Only dirty if necessary.
+        if (oldShots == newShots && oldCapacity == newCapacity)
             return;
 
-        for (var i = 0; i < shots; i++)
-        {
-            args.Ammo.Add(GetShootable(component, args.Coordinates));
-            component.Shots--;
-        }
+        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);
 
-        TakeCharge((uid, component));
-        UpdateBatteryAppearance(uid, component);
-        Dirty(uid, component);
+        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);
     }
 
-    private void OnBatteryAmmoCount(EntityUid uid, BatteryAmmoProviderComponent component, ref GetAmmoCountEvent args)
+    // For server side changes the client's ammo counter needs to be updated as well.
+    private void OnAfterAutoHandleState(Entity<BatteryAmmoProviderComponent> ent, ref AfterAutoHandleStateEvent args)
     {
-        args.Count = component.Shots;
-        args.Capacity = component.Capacity;
+        UpdateAmmoCount(ent); // Need to have prediction set to true because the state is applied repeatedly while prediction is running.
     }
 
-    /// <summary>
-    /// Update the battery (server-only) whenever fired.
-    /// </summary>
-    protected virtual void TakeCharge(Entity<BatteryAmmoProviderComponent> entity)
+    // For when a power cell gets inserted or removed.
+    private void OnPowerCellChanged(Entity<BatteryAmmoProviderComponent> ent, ref PowerCellChangedEvent args)
     {
-        UpdateAmmoCount(entity, prediction: false);
+        UpdateShots(ent);
     }
 
-    protected void UpdateBatteryAppearance(EntityUid uid, BatteryAmmoProviderComponent component)
+    // 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)
     {
-        if (!TryComp<AppearanceComponent>(uid, out var appearance))
-            return;
+        // 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);
+    }
 
-        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);
+    // 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 (EntityUid? Entity, IShootable) GetShootable(BatteryAmmoProviderComponent component, EntityCoordinates coordinates)
+    private void UpdateNextUpdate(Entity<BatteryAmmoProviderComponent> ent, float currentCharge, float maxCharge, float currentChargeRate)
     {
-        switch (component)
+        // 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)
         {
-            case ProjectileBatteryAmmoProviderComponent proj:
-                var ent = Spawn(proj.Prototype, coordinates);
-                return (ent, EnsureShootable(ent));
-            case HitscanBatteryAmmoProviderComponent hitscan:
-                var hitscanEnt = Spawn(hitscan.HitscanEntityProto);
-                return (hitscanEnt, EnsureShootable(hitscanEnt));
-            default:
-                throw new ArgumentOutOfRangeException();
+            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);
     }
 
-    [Serializable, NetSerializable]
-    private sealed class BatteryAmmoProviderComponentState : ComponentState
+    /// <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)
     {
-        public int Shots;
-        public int MaxShots;
-        public float FireCost;
+        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.
+        }
     }
 }
index ed8849a41ac1a9f6634b029a6cff705ffb24415e..e9170d45dfda0c13ec148ef5fab91d6224ce3220 100644 (file)
@@ -649,6 +649,11 @@ public abstract partial class SharedGunSystem : EntitySystem
         RaiseLocalEvent(uid, ref ammoEv);
         return ammoEv.Capacity;
     }
+
+    public override void Update(float frameTime)
+    {
+        UpdateBattery(frameTime);
+    }
 }
 
 /// <summary>
index faf82ea99701d1b3ca9e1bac91285335408b86ea..83cdab338e3e2f0227ff343595bfc0a448400de3 100644 (file)
@@ -2593,9 +2593,6 @@ entities:
     - type: Transform
       pos: 6.5,-7.5
       parent: 1
-    - type: PowerCellDraw
-      canUse: True
-      canDraw: True
     - type: Physics
       canCollide: False
     - type: ContainerContainer
index 60fa5147b8700de9e13c417f604df0e849273141..9ec49d629998b39092237b84a21c8e2a53904bf2 100644 (file)
     amount: 6
     whitelist:
       components:
-      - HitscanBatteryAmmoProvider
+      - BatteryAmmoProvider
     blacklist:
       components:
       - PowerCell
index 05dda23f0a2785c3ab3647c01908d8d155ac4817..5dfc94a1f771c4ffc16e4af6a74fe392e5c6ae75 100644 (file)
     damage: 35
     sound: /Audio/Weapons/egloves.ogg
   - type: LandAtCursor # it deals stamina damage when thrown
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 1000
     startingCharge: 1000
   - type: GuideHelp
index 3b85ce94c9ee2b0a3a626d4ca4486cb5a30d6856..2d320c868c415d2efc95bf166412f5cb43e34f68 100644 (file)
         startValue: 0.1
         endValue: 2.0
         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.
     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.
 
 - type: entity
index 341404af2b2d78e39f5647b0afb325d57e8db1e5..1c23d18a97d46ed895fe53fff554c721065df57f 100644 (file)
         startValue: 0.1
         endValue: 2.0
         isLooped: true
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 600
     startingCharge: 600
-  - type: BatterySelfRecharger
+  - type: PredictedBatterySelfRecharger
     autoRechargeRate: 2
   - type: Item
     size: Normal
index 65cc71aaa86db7f4fd9ef0fabacfdc1423baa17c..1cb00ef7608ae44ba90c96049d1614cba7dbb9c9 100644 (file)
     - type: HTN
       rootTask:
         task: SimpleRangedHostileCompound
-    - type: HitscanBatteryAmmoProvider
+    - type: BatteryAmmoProvider
       proto: RedLaser
       fireCost: 62.5
-    - type: Battery
+    - type: PredictedBattery
       maxCharge: 1000
       startingCharge: 1000
-    - type: BatterySelfRecharger
+    - type: PredictedBatterySelfRecharger
       autoRechargeRate: 40
     - type: Gun
       fireRate: 0.75
index 949fc76efe9dfea37c2ab85df99bf403dcb62cd5..4a205c9cb026b1d937b3a9f83b15d1877f59ccee 100644 (file)
   - type: MovementSpeedModifier
     baseWalkSpeed: 5
     baseSprintSpeed: 7
-  - type: ProjectileBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: WatcherBolt
     fireCost: 50
-  - type: BatterySelfRecharger
+  - type: PredictedBatterySelfRecharger
     autoRechargeRate: 50
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 1000
     startingCharge: 1000
   - type: Gun
     radius: 1
     energy: 3
     color: orangered
-  - type: ProjectileBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: WatcherBoltMagmawing
     fireCost: 50
 
index 1531e348f72c932edbe50b27a89e706d85bbc6d9..13eeb2b372423fa332a07818912078f51d3216e1 100644 (file)
     damage:
       types:
         Heat: 5
-  - type: HitscanBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: RedLaser
     fireCost: 140
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 1000
     startingCharge: 1000
-  - type: BatterySelfRecharger
+  - type: PredictedBatterySelfRecharger
     autoRechargeRate: 50
   - type: Gun
     fireRate: 0.3
index 173c5ad165421ada4c257830318968a150e73592..00aa20db18720394552f8b0644978f26a57ce831 100644 (file)
     - type: Tag
       tags:
         - FootstepSound
-    - type: HitscanBatteryAmmoProvider
+    - type: BatteryAmmoProvider
       proto: RedLightLaser
       fireCost: 50
-    - type: BatterySelfRecharger
+    - type: PredictedBatterySelfRecharger
       autoRechargeRate: 50
-    - type: Battery
+    - type: PredictedBattery
       maxCharge: 1000
       startingCharge: 1000
     - type: Gun
index c377519ddb4437aa608cbc54ec3ae1fb499c2775..e131e7b7326293836332d7f35907dda96ccba1fb 100644 (file)
@@ -8,5 +8,5 @@
     onUse: false # above component does the toggling
   - type: PowerCellDraw
     drawRate: 0
-    useRate: 20
+    useCharge: 20
   - type: ToggleCellDraw
index 194fd4d23306fd9404e9316c9e003d512f740e67..329d278bc8f95b076eaa5a4f1b08e0042b0956a2 100644 (file)
@@ -43,7 +43,7 @@
   components:
   - type: PowerCellDraw
     drawRate: 1
-    useRate: 0
+    useCharge: 0
   - type: ToggleCellDraw
 
 - type: entity
index 3c553fc81241b57a91d9b054ea4f67943e946594..2a29951f1fc1829229de99b4bfe2dd02680f591c 100644 (file)
@@ -8,7 +8,12 @@
     size: Huge
   - type: Sprite
     sprite: Objects/Power/portable_recharger.rsi
-    state: charging
+    layers:
+    - map: ["enum.PowerChargerVisualLayers.Base"]
+      state: charging
+    - map: ["enum.PowerChargerVisualLayers.Light"]
+      state: charging-unlit
+      shader: unshaded
   - type: Clothing
     equippedPrefix: charging
     quickEquip: false
@@ -18,9 +23,6 @@
     slotId: charger_slot
     portable: true
   - type: PowerChargerVisuals
-  - type: ApcPowerReceiver
-    needsPower: false
-    powerLoad: 0
   - type: StaticPrice
     price: 500
   - type: Tag
@@ -34,5 +36,4 @@
         ejectOnInteract: true
         whitelist:
           components:
-          - HitscanBatteryAmmoProvider
-          - ProjectileBatteryAmmoProvider
+          - BatteryAmmoProvider
index 4ce96419bf37f8f605fba2f86af8556abc51329c..a896e3a6e2be664538147f46df0aec0a444a7559 100644 (file)
@@ -5,7 +5,7 @@
   components:
   - type: Item
     storedRotation: -90
-  - type: Battery
+  - type: PredictedBattery
     pricePerJoule: 0.15
   - type: PowerCell
   - type: Explosive
   - type: Tag
     tags:
       - PowerCell
-  - type: Appearance
-  - type: PowerCellVisuals
   - 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
   name: potato battery
@@ -43,7 +50,7 @@
   - type: Sprite
     layers:
     - state: potato
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 70
     startingCharge: 70
   - type: Tag
@@ -67,7 +74,7 @@
     - map: [ "enum.PowerCellVisualLayers.Unshaded" ]
       state: o2
       shader: unshaded
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 360
     startingCharge: 360
   - type: Tag
@@ -87,7 +94,7 @@
         state: o2
         shader: unshaded
         visible: false
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 360
     startingCharge: 0
 
   name: small-capacity nuclear power cell
   description: A self rechargeable power cell, designed for fast recharge rate at the expense of capacity.
   components:
-  - type: BatterySelfRecharger
+  - type: PredictedBatterySelfRecharger
     autoRechargeRate: 36 # 10 seconds to recharge
     autoRechargePauseTime: 30
 
     - map: [ "enum.PowerCellVisualLayers.Unshaded" ]
       state: o2
       shader: unshaded
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 720
     startingCharge: 720
 
         state: o2
         shader: unshaded
         visible: false
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 720
     startingCharge: 0
 
     - map: [ "enum.PowerCellVisualLayers.Unshaded" ]
       state: o2
       shader: unshaded
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 1080
     startingCharge: 1080
 
         state: o2
         shader: unshaded
         visible: false
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 1080
     startingCharge: 0
 
     - map: [ "enum.PowerCellVisualLayers.Unshaded" ]
       state: o2
       shader: unshaded
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 1800
     startingCharge: 1800
 
         state: o2
         shader: unshaded
         visible: false
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 1800
     startingCharge: 0
 
     - map: [ "enum.PowerCellVisualLayers.Unshaded" ]
       state: o2
       shader: unshaded
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 720
     startingCharge: 720
-  - type: BatterySelfRecharger
+  - type: PredictedBatterySelfRecharger
     autoRechargeRate: 12 # takes 1 minute to charge itself back to full
 
 - type: entity
       state: o2
       shader: unshaded
       visible: false
-  - type: Battery
+  - type: PredictedBattery
     startingCharge: 0
 
 - type: entity
       - map: [ "enum.PowerCellVisualLayers.Unshaded" ]
         state: o2
         shader: unshaded
-    - type: Battery
+    - type: PredictedBattery
       maxCharge: 1200
       startingCharge: 1200
-    - type: BatterySelfRecharger
+    - type: PredictedBatterySelfRecharger
       autoRechargeRate: 40
 
 # Power cage (big heavy power cell for big devices)
     - map: [ "enum.PowerCellVisualLayers.Unshaded" ]
       state: o2
       shader: unshaded
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 1400
     startingCharge: 1400
 
     - map: [ "enum.PowerCellVisualLayers.Unshaded" ]
       state: o2
       shader: unshaded
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 2700
     startingCharge: 2700
 
     - map: [ "enum.PowerCellVisualLayers.Unshaded" ]
       state: o2
       shader: unshaded
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 6200
     startingCharge: 6200
 
       state: o2
       shader: unshaded
       visible: false
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 1400
     startingCharge: 0
 
       state: o2
       shader: unshaded
       visible: false
-  - type: Battery
+  - type: PredictedBattery
     startingCharge: 0
 
 - type: entity
       state: o2
       shader: unshaded
       visible: false
-  - type: Battery
+  - type: PredictedBattery
     startingCharge: 0
index 894454b05cba37e2439163e77c269356516f170d..86372c1769afdc7d799f39323ecee6fd8477722c 100644 (file)
@@ -48,7 +48,7 @@
   - type: MultiHandedItem
   - type: ToggleCellDraw
   - type: PowerCellDraw
-    useRate: 100
+    useCharge: 100
 
 - type: entity
   id: DefibrillatorEmpty
@@ -86,7 +86,7 @@
     size: Normal
   - type: ToggleCellDraw
   - type: PowerCellDraw
-    useRate: 100
+    useCharge: 100
   - type: Defibrillator
     zapHeal:
       types:
index 0f2259f7e9fe79776b67173e51415fa58174e5c0..9fe4d377d7e27078b3c47158d90c693b5bca1778 100644 (file)
   - type: Appearance
   - type: GenericVisualizer
     visuals:
-      enum.PowerCellSlotVisuals.Enabled:
+      enum.BatteryVisuals.State:
         enum.PowerDeviceVisualLayers.Powered:
-          True: { visible: true }
-          False: { visible: false }
+          Full: { visible: true }
+          Neither: { visible: true }
+          Empty: { visible: false }
   - type: GuideHelp
     guides:
     - MedicalDoctor
@@ -47,6 +48,7 @@
   parent: [ HandheldHealthAnalyzerUnpowered, PowerCellSlotSmallItem]
   suffix: ""
   components:
+  - type: PredictedBatteryVisuals
   - type: PowerCellDraw
     drawRate: 1.2 #Calculated for 5 minutes on a small cell
   - type: ToggleCellDraw
     slots:
       cell_slot:
         name: power-cell-slot-component-slot-name-default
+  - type: Sprite
+    layers:
+    - state: icon
+    - state: analyzer
+      shader: unshaded
+      visible: false
+      map: [ "enum.PowerDeviceVisualLayers.Powered" ]
index e9b365de9a18201a828d6756e32565e1cc6e8a22..b2eae73ddf8d48eb1ed5ea78871e8f979a8c74cf 100644 (file)
   components:
   - type: PowerCellDraw
     drawRate: 1
-    useRate: 0
+    useCharge: 0
   - type: ToggleCellDraw
 
 - type: entity
   components:
   - type: PowerCellDraw
     drawRate: 1
-    useRate: 0
+    useCharge: 0
   - type: ToggleCellDraw
 
 - type: entity
index 7c2d76b3b882ac229a3b774cde8ab49de8a1a61c..6081e2e27769698ba8f54b06e996e1742317fbb6 100644 (file)
@@ -47,7 +47,7 @@
   - type: ToggleCellDraw
   - type: PowerCellDraw
     drawRate: 2.4 # ~5 minutes on a medium power cell.
-    useRate: 0
+    useCharge: 0
 
 - type: entity
   id: MineralScannerEmpty
@@ -91,7 +91,7 @@
   - type: ToggleCellDraw
   - type: PowerCellDraw
     drawRate: 1.2 # ~10 minutes on a medium power cell.
-    useRate: 0
+    useCharge: 0
 
 - type: entity
   id: AdvancedMineralScannerEmpty
index 4dc2e65b77fd5a482fbcedc489dae2e25e3f4e97..086901b145712929a9fdff7e0397fce840903709 100644 (file)
     maxRange: 256
     followEntity: true
   - type: Appearance
+  - type: PredictedBatteryVisuals
   - type: GenericVisualizer
     visuals:
-      enum.PowerCellSlotVisuals.Enabled:
+      enum.BatteryVisuals.State:
         enum.PowerDeviceVisualLayers.Powered:
-          True: { visible: true }
-          False: { visible: false }
+          Full: { visible: true }
+          Neither: { visible: true }
+          Empty: { visible: false }
   - type: PowerCellDraw
     drawRate: 1.5
   - type: ToggleCellDraw
index 10181b54e70a3cec43632a31ce2d68a6c28156a9..5134b2d71189d8a0c747e5a274404ca6162af81f 100644 (file)
@@ -20,7 +20,7 @@
     - SemiAuto
     soundGunshot:
       path: /Audio/Weapons/Guns/Gunshots/laser.ogg
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 1000
     startingCharge: 1000
   - type: StaticPrice
     selectedMode: SemiAuto
     availableModes:
     - SemiAuto
-  - type: HitscanBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: RedLaser
     fireCost: 62.5
   - type: StaticPrice
     price: 420
+  - type: Tag
+    tags:
+    - LaserWeapon
 
 - type: entity
   id: BaseWeaponPowerCell
@@ -74,7 +77,7 @@
     - SemiAuto
     soundGunshot:
       path: /Audio/Weapons/Guns/Gunshots/laser.ogg
-  - type: HitscanBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: RedLightLaser
     fireCost: 50
   - type: ItemSlots
     magState: mag
     steps: 5
     zeroVisible: true
+  - type: Tag
+    tags:
+    - Sidearm
+    - LaserWeapon
 
 - type: entity
   name: retro laser blaster
       shader: unshaded
   - type: Item
     storedOffset: 0,-5
-  - type: HitscanBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: RedMediumLaser
     fireCost: 62.5
   - type: MagazineVisuals
     steps: 5
     zeroVisible: true
   - type: Appearance
+  - type: Tag
+    tags:
+    - Sidearm
+    - LaserWeapon
 
 - type: entity
   name: makeshift laser pistol
   - type: Appearance
   - type: Clothing
     sprite: Objects/Weapons/Guns/Battery/makeshift.rsi
-  - type: HitscanBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: RedLaser
     fireCost: 62.5
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 500
     startingCharge: 500
+  - type: Tag
+    tags:
+    - Sidearm
+    - LaserWeapon
 
 - type: entity
   name: tesla gun
       path: /Audio/Effects/Lightning/lightningshock.ogg
       params:
         variation: 0.2
-  - type: ProjectileBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: TeslaGunBullet
     fireCost: 300
   - type: MagazineVisuals
   components:
   - type: StaticPrice
     price: 300
-  - type: HitscanBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: RedLaserPractice
     fireCost: 62.5
   - type: PacifismAllowedGun
     - SemiAuto
     soundGunshot:
       path: /Audio/Weapons/Guns/Gunshots/laser3.ogg
-  - type: HitscanBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: Pulse
     fireCost: 200
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 2000
     startingCharge: 2000
+  - type: Tag
+    tags:
+    - Sidearm
+    - LaserWeapon
 
 - type: entity
   name: pulse carbine
       - FullAuto
     soundGunshot:
       path: /Audio/Weapons/Guns/Gunshots/laser3.ogg
-  - type: HitscanBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: Pulse
     fireCost: 200
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 5000
     startingCharge: 5000
 
     fireRate: 1.5
     soundGunshot:
       path: /Audio/Weapons/Guns/Gunshots/laser3.ogg
-  - type: HitscanBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: Pulse
     fireCost: 100
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 40000
     startingCharge: 40000
 
     fireRate: 1.5
     soundGunshot:
       path: /Audio/Weapons/Guns/Gunshots/laser_cannon.ogg
-  - type: HitscanBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: RedHeavyLaser
     fireCost: 100
   - type: Tag
         path: /Audio/Weapons/emitter.ogg
         params:
           pitch: 2
-    - type: ProjectileBatteryAmmoProvider
+    - type: BatteryAmmoProvider
       proto: AntiParticlesProjectile
       fireCost: 500
-    - type: Battery
+    - type: PredictedBattery
       maxCharge: 10000
       startingCharge: 10000
 
   - type: Gun
     soundGunshot:
       path: /Audio/Weapons/Guns/Gunshots/laser3.ogg
-  - type: HitscanBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: XrayLaser
     fireCost: 100
   - type: MagazineVisuals
     projectileSpeed: 35 # any higher and this causes issues in lag
     soundGunshot:
       path: /Audio/Weapons/Guns/Gunshots/taser2.ogg
-  - type: ProjectileBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: BulletDisablerPractice
     fireCost: 62.5
   - type: Tag
     slots:
       - suitStorage
       - Belt
-  - type: ProjectileBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: BulletDisabler
     fireCost: 62.5
   - type: GuideHelp
       - FullAuto
     soundGunshot:
       path: /Audio/Weapons/Guns/Gunshots/taser2.ogg
-  - type: ProjectileBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: BulletDisablerSmg
     fireCost: 25
   - type: MagazineVisuals
     fireRate: 0.5
     soundGunshot:
       path: /Audio/Weapons/Guns/Gunshots/taser.ogg
-  - type: ProjectileBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: BulletTaser
     fireCost: 200
   - type: MagazineVisuals
       path: /Audio/Effects/tesla_collapse.ogg # The wrath of god...
       params:
         volume: -6
-  - type: ProjectileBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: BulletTaserSuper
     fireCost: 200
 
   - type: Gun
     soundGunshot:
       path: /Audio/Weapons/Guns/Gunshots/laser_cannon.ogg
-  - type: HitscanBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: RedMediumLaser
     fireCost: 100
-  - type: BatterySelfRecharger
+  - type: PredictedBatterySelfRecharger
     autoRechargeRate: 40
   - type: MagazineVisuals
     magState: mag
     - HighRiskItem
     - Sidearm
     - WeaponAntiqueLaser
+    - LaserWeapon
   - type: StaticPrice
     price: 750
   - type: StealTarget
   - type: Gun
     soundGunshot:
       path: /Audio/Weapons/Guns/Gunshots/laser_cannon.ogg
-  - type: HitscanBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: RedMediumLaser
     fireCost: 100
-  - type: BatterySelfRecharger
+  - type: PredictedBatterySelfRecharger
     autoRechargeRate: 30
   - type: MagazineVisuals
     magState: mag
   - type: Appearance
   - type: StaticPrice
     price: 63
+  - type: Tag
+    tags:
+    - Sidearm
+    - LaserWeapon
 
 - type: entity
   name: C.H.I.M.P. handcannon
     fireRate: 1.5
     soundGunshot:
       path: /Audio/Weapons/Guns/Gunshots/taser2.ogg
-  - type: ProjectileBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: AnomalousParticleDeltaStrong
     fireCost: 100
   - type: BatteryWeaponFireModes
     fireRate: 1
     soundGunshot:
       path: /Audio/Weapons/Guns/Gunshots/laser_clown.ogg
-  - type: HitscanBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: RedMediumLaser
     fireCost: 100
-  - type: BatterySelfRecharger
+  - type: PredictedBatterySelfRecharger
     autoRechargeRate: 40
   - type: StaticPrice
     price: 750
+  - type: Tag
+    tags:
+    - Sidearm
+    - LaserWeapon
 
 - type: entity
   name: energy shotgun
     fireRate: 2
     soundGunshot:
       path: /Audio/Weapons/Guns/Gunshots/laser_cannon.ogg
-  - type: ProjectileBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: BulletLaserSpreadNarrow
     fireCost: 80
   - type: BatteryWeaponFireModes
     sprite: Objects/Weapons/Guns/Battery/inhands_64x.rsi
     heldPrefix: energy
   - type: GunRequiresWield #remove when inaccuracy on spreads is fixed
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 480
     startingCharge: 480
 
   - type: Gun
     soundGunshot:
       path: /Audio/Weapons/Guns/Gunshots/laser_cannon.ogg
-  - type: ProjectileBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: BulletLaserMagnum
     fireCost: 150
   - type: BatteryWeaponFireModes
     - proto: BulletDisabler
       fireCost: 62.5
       pacifismAllowedMode: true
-  - type: BatterySelfRecharger
+  - type: PredictedBatterySelfRecharger
     autoRechargeRate: 48
     autoRechargePauseTime: 10
 
     fireRate: 1
     soundGunshot:
       path: /Audio/Weapons/Guns/Gunshots/taser2.ogg
-  - type: ProjectileBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: BoltTempgunCold
     fireCost: 100
   - type: BatteryWeaponFireModes
       fireCost: 100
     - proto: BoltTempgunHot
       fireCost: 100
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 1000
     startingCharge: 1000
   - type: StaticPrice
   parent: [WeaponAdvancedLaser, BaseXenoborgContraband]
   id: XenoborgHeavyLaserGun
   components:
-  - type: HitscanBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: RedHeavyLaser
index be57d5f0f91b597d28081e3f66aebdbb127a295b..acf29c9e1a322245491103ad69ff18dff6311b26 100644 (file)
     - type: ContainerContainer
       containers:
         ballistic-ammo: !type:Container
-    - type: ProjectileBatteryAmmoProvider
+    - type: BatteryAmmoProvider
       proto: CartridgeLightRifle
       fireCost: 100
-    - type: Battery
+    - type: PredictedBattery
       maxCharge: 10000
       startingCharge: 10000
-    - type: BatterySelfRecharger
+    - type: PredictedBatterySelfRecharger
       autoRechargeRate: 25
     - type: AmmoCounter
index ca40d1940d19310f7cc6b50ec98b109f8cb615e6..1a1095d981af79bc0b83a7e4ca5ff15738d36179 100644 (file)
   - type: ContainerContainer
     containers:
       ballistic-ammo: !type:Container
-  - type: ProjectileBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: BulletPistol
     fireCost: 100
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 1000
     startingCharge: 1000
-  - type: BatterySelfRecharger
+  - type: PredictedBatterySelfRecharger
     autoRechargeRate: 25
   - type: AmmoCounter
 
index 4ca2e0c0f526944526d067e703c57b5189e3109b..e567d06c03d231386f20aad22c8a9b4100678b77 100644 (file)
   - type: ContainerContainer
     containers:
       ballistic-ammo: !type:Container
-  - type: ProjectileBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: CartridgePistol
     fireCost: 100
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 3000
     startingCharge: 3000
-  - type: BatterySelfRecharger
+  - type: PredictedBatterySelfRecharger
     autoRechargeRate: 25
   - type: AmmoCounter
 
index f60297d2235491fac01d9927334a9e883e3245c9..dc4be40c8248fc1b3cd6b1b5b5f5f52ae83b1274 100644 (file)
     fireRate: 1.5
     soundGunshot:
       path: /Audio/Weapons/Guns/Gunshots/taser2.ogg
-  - type: ProjectileBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: BulletEnergyTurretLaser
     fireCost: 100
   - type: Battery
     rootTask:
       task: EnergyTurretCompound
   - type: StaticPrice
-    price: 200
\ No newline at end of file
+    price: 200
index 33d5fd6e970a52fcdf36f092ab1a8a76c0323be1..01a0569ad5a65358ed64fcb8ff0282b367351c64 100644 (file)
@@ -6,7 +6,7 @@
   description: A high-tech autonomous weapons system designed to keep unauthorized personnel out of sensitive areas.
   components:
   - type: Anchorable
-    flags: 
+    flags:
     - Anchorable
   - type: Rotatable
   - type: Fixtures
@@ -77,7 +77,7 @@
   - type: NpcFactionMember
     factions:
     - AllHostile
-  - type: ProjectileBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: BulletEnergyTurretDisabler
     fireCost: 100
   - type: BatteryWeaponFireModes
index 6b8c4eebe1ffd3402a47b8e6cc5e111e1f69fe11..26520c2fd543c7492e60c89d2b18063a956c67f3 100644 (file)
     soundEmpty:
       path: /Audio/Items/hiss.ogg
     clumsyProof: true
-  - type: ProjectileBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: FoodPieBananaCream
     fireCost: 30
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 90
     startingCharge: 90
-  - type: BatterySelfRecharger
+  - type: PredictedBatterySelfRecharger
     autoRechargeRate: 1
   - type: AmmoCounter
   - type: Item
index 295a7821907b4d8e23e0847a0c88782970fd6975..27857e24a12e717d0e4d0b27dee58c90d6068c6e 100644 (file)
@@ -42,7 +42,7 @@
     damage: 35
     sound: /Audio/Weapons/egloves.ogg
   - type: LandAtCursor # it deals stamina damage when thrown
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 360
     startingCharge: 360
   - type: UseDelay
index 2f6ac834ba46f3506d2eb02f5be337cc8559a4b9..217286a5c527f7911551c7d4503b1c66a80a1be6 100644 (file)
@@ -47,7 +47,7 @@
     damage: 35
     sound: /Audio/Weapons/egloves.ogg
   - type: LandAtCursor # it deals stamina damage when thrown
-  - type: Battery
+  - type: PredictedBattery
     maxCharge: 1000
     startingCharge: 1000
   - type: UseDelay
index 0bbe95efb5d4a97fb2585f91a4375b3ab04029c2..907c2ddbd45834c65010407f2b1af5fe37dfdbe9 100644 (file)
@@ -13,6 +13,8 @@
   - type: Appearance
   - type: Charger
     slotId: charger_slot
+  - type: ApcPowerReceiver
+    powerLoad: 1 # same as draw given in ChargerComponent.PassiveDraw; increased when it is actually charging something
   - type: Anchorable
     delay: 1
   - type: Destructible
         ejectOnInteract: true
         whitelist:
           components:
-          - HitscanBatteryAmmoProvider
-          - ProjectileBatteryAmmoProvider
+          - BatteryAmmoProvider
           - Stunbaton
           - PowerCell
         blacklist:
         ejectOnInteract: true
         whitelist:
           components:
-          - HitscanBatteryAmmoProvider
-          - ProjectileBatteryAmmoProvider
+          - BatteryAmmoProvider
           - Stunbaton
           - PowerCell
         blacklist:
         ejectOnInteract: true
         whitelist:
           components:
-          - HitscanBatteryAmmoProvider
-          - ProjectileBatteryAmmoProvider
+          - BatteryAmmoProvider
           - Stunbaton
         blacklist:
           tags:
index 92e86f322338a49c0882a83d80d429408f006c71..516b90ed274ebfd724120288c35398d9e80b50b7 100644 (file)
@@ -92,7 +92,7 @@
           tags:
             - PowerCell
             - PowerCellSmall
-  - type: HitscanBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: RedLightLaser
     fireCost: 50
 
         whitelist:
           tags:
             - PowerCage
-  - type: HitscanBatteryAmmoProvider
+  - type: BatteryAmmoProvider
     proto: RedShuttleLaser
     fireCost: 150
 
index 274d9447b4a36d626b15094a9da3af6bdcaa47e9..34dfe443ad6e3359a3eaaabb6eaa85dd6cf943e1 100644 (file)
 
 ## L ##
 
+- type: Tag
+  id: LaserWeapon # Used for a cargo bounty. Added to all hitscan weapons that shoot lasers.
+
 - type: Tag # Used on DungeonRoom prototypes.
   id: LavaBrig # PrefabDunGen whitelist on LavaBrig DungeonConfig.