From e2ff167062b5909343608ef6c1b0176f6ba2bce7 Mon Sep 17 00:00:00 2001 From: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Date: Mon, 24 Nov 2025 17:52:11 +0100 Subject: [PATCH] Predict powercells, chargers and PowerCellDraw (#41379) * cleanup * fix fixtures * prediction * fix test * review * fix svalinn visuals * fix chargers * fix portable recharger and its unlit visuals * fix borgs * oomba review * fix examination prediction --- .../Power/EntitySystems/ChargerSystem.cs | 5 - Content.Client/PowerCell/PowerCellSystem.cs | 67 ----- .../PowerCell/PowerCellVisualLayers.cs | 11 + .../PowerCell/PowerCellVisualsComponent.cs | 4 - .../PowerChargerVisualizerComponent.cs | 11 +- .../PowerCell/PowerChargerVisualizerSystem.cs | 2 +- .../Ranged/Systems/GunSystem.Battery.cs | 17 +- .../Weapons/Ranged/Systems/GunSystem.cs | 7 +- Content.Server/Access/Systems/IdCardSystem.cs | 1 + .../Systems/AdminVerbSystem.Tools.cs | 53 ++++ .../Construction/Completions/BuildMech.cs | 2 +- Content.Server/Holosign/HolosignSystem.cs | 25 +- .../Kitchen/Components/MicrowaveComponent.cs | 12 - .../EntitySystems/HandheldLightSystem.cs | 31 +- Content.Server/Mech/Systems/MechSystem.cs | 26 +- .../CrewMonitoringConsoleSystem.cs | 2 +- Content.Server/Medical/DefibrillatorSystem.cs | 2 +- .../Medical/HealthAnalyzerSystem.cs | 6 +- .../Ninja/Systems/BatteryDrainerSystem.cs | 16 +- .../Ninja/Systems/ItemCreatorSystem.cs | 6 +- .../Ninja/Systems/NinjaSuitSystem.cs | 17 +- .../Ninja/Systems/SpaceNinjaSystem.cs | 26 +- .../Ninja/Systems/StunProviderSystem.cs | 4 +- Content.Server/Nuke/NukeSystem.cs | 2 +- Content.Server/PAI/PAISystem.cs | 2 +- Content.Server/Pinpointer/StationMapSystem.cs | 2 +- .../Components/ActiveChargerComponent.cs | 10 - .../Components/ExaminableBatteryComponent.cs | 6 - .../Power/EntitySystems/BatterySystem.API.cs | 53 +++- .../Power/EntitySystems/BatterySystem.cs | 22 +- .../Power/EntitySystems/ChargerSystem.cs | 259 ---------------- .../EntitySystems/PowerReceiverSystem.cs | 5 - .../Power/EntitySystems/RiggableSystem.cs | 61 +++- .../Power/SetBatteryPercentCommand.cs | 9 +- .../PowerCell/PowerCellSystem.Draw.cs | 72 ----- Content.Server/PowerCell/PowerCellSystem.cs | 257 ---------------- .../Radio/EntitySystems/JammerSystem.cs | 17 +- .../Silicons/Borgs/BorgSystem.Transponder.cs | 6 +- .../Silicons/Borgs/BorgSystem.Ui.cs | 55 +++- Content.Server/Silicons/Borgs/BorgSystem.cs | 50 ++-- .../DamagedSiliconAccentSystem.cs | 6 +- .../Stunnable/Systems/StunbatonSystem.cs | 28 +- .../Weapons/Misc/TetherGunSystem.cs | 1 - .../Ranged/Systems/GunSystem.Battery.cs | 75 ----- .../Artifact/XAE/XAEChargeBatterySystem.cs | 14 +- .../Kitchen/BeingMicrowavedEvent.cs | 10 + .../Mech/EntitySystems/SharedMechSystem.cs | 1 + Content.Shared/Power/ChargeEvents.cs | 34 ++- .../Power/Components/BatteryComponent.cs | 2 + .../BatterySelfRechargerComponent.cs | 3 +- .../Power/Components/ChargerComponent.cs | 37 ++- .../Components/ExaminableBatteryComponent.cs | 10 + .../Components/InsideChargerComponent.cs | 10 + .../Components/PredictedBatteryComponent.cs | 94 ++++++ .../PredictedBatterySelfRechargerComponent.cs | 33 +++ .../PredictedBatteryVisualsComponent.cs | 51 ++++ .../Power/EntitySystems/ChargerSystem.cs | 270 +++++++++++++++++ .../PredictedBatterySystem.API.cs | 278 ++++++++++++++++++ .../EntitySystems/PredictedBatterySystem.cs | 182 ++++++++++++ .../EntitySystems/SharedBatterySystem.cs | 15 +- .../EntitySystems/SharedChargerSystem.cs | 20 -- .../SharedPowerReceiverSystem.cs | 15 +- .../Power/SharedPowerItemCharger.cs | 20 -- .../Components/PowerCellComponent.cs | 27 +- .../Components/PowerCellDrawComponent.cs | 35 +++ .../Components/PowerCellSlotComponent.cs | 21 +- .../PowerCell/PowerCellDrawComponent.cs | 68 ----- ...llSlotEmptyEvent.cs => PowerCellEvents.cs} | 6 + .../PowerCell/PowerCellSystem.API.cs | 145 +++++++++ .../PowerCell/PowerCellSystem.Draw.cs | 76 +++++ .../PowerCell/PowerCellSystem.Relay.cs | 40 +++ Content.Shared/PowerCell/PowerCellSystem.cs | 154 ++++++++++ .../PowerCell/SharedPowerCellSystem.cs | 118 -------- .../PowerCell/ToggleCellDrawSystem.cs | 6 +- .../Borgs/Components/BorgChassisComponent.cs | 12 +- .../Components/EntityStorageComponent.cs | 4 +- .../SharedEntityStorageSystem.cs | 4 +- ...ActivatableUIRequiresPowerCellComponent.cs | 9 +- .../ActivatableUISystem.Power.cs | 40 +-- .../BatteryAmmoProviderComponent.cs | 64 +++- .../HitscanBatteryAmmoProviderComponent.cs | 11 - .../ProjectileBatteryAmmoProviderComponent.cs | 12 - .../Ranged/Events/UpdateClientAmmoEvent.cs | 4 - .../Systems/BatteryWeaponFireModesSystem.cs | 18 +- .../Ranged/Systems/SharedGunSystem.Battery.cs | 262 ++++++++++------- .../Weapons/Ranged/Systems/SharedGunSystem.cs | 5 + Resources/Maps/Shuttles/emergency_meta.yml | 3 - .../Prototypes/Catalog/Bounties/bounties.yml | 2 +- .../Entities/Clothing/Hands/gloves.yml | 2 +- .../Clothing/Head/base_clothinghead.yml | 4 +- .../Entities/Clothing/OuterClothing/armor.yml | 4 +- .../Entities/Mobs/NPCs/behonker.yml | 6 +- .../Entities/Mobs/NPCs/lavaland.yml | 8 +- .../Entities/Mobs/NPCs/living_light.yml | 6 +- .../Entities/Mobs/NPCs/miscellaneous.yml | 6 +- .../Objects/Devices/base_handheld.yml | 2 +- .../Entities/Objects/Fun/spectral_locator.yml | 2 +- .../Objects/Power/portable_recharger.yml | 13 +- .../Entities/Objects/Power/powercells.yml | 55 ++-- .../Objects/Specific/Medical/defib.yml | 4 +- .../Specific/Medical/healthanalyzer.yml | 15 +- .../Objects/Specific/Research/anomaly.yml | 4 +- .../Objects/Specific/Salvage/scanner.yml | 4 +- .../Objects/Tools/handheld_mass_scanner.yml | 8 +- .../Weapons/Guns/Battery/battery_guns.yml | 102 ++++--- .../Objects/Weapons/Guns/LMGs/lmgs.yml | 6 +- .../Objects/Weapons/Guns/Pistols/pistols.yml | 6 +- .../Objects/Weapons/Guns/SMGs/smgs.yml | 6 +- .../Weapons/Guns/Turrets/turrets_base.yml | 4 +- .../Weapons/Guns/Turrets/turrets_energy.yml | 4 +- .../Objects/Weapons/Guns/pneumatic_cannon.yml | 6 +- .../Objects/Weapons/Melee/stunprod.yml | 2 +- .../Entities/Objects/Weapons/security.yml | 2 +- .../Entities/Structures/Power/chargers.yml | 11 +- .../Entities/Structures/Shuttles/cannons.yml | 4 +- Resources/Prototypes/tags.yml | 3 + 116 files changed, 2327 insertions(+), 1568 deletions(-) delete mode 100644 Content.Client/Power/EntitySystems/ChargerSystem.cs delete mode 100644 Content.Client/PowerCell/PowerCellSystem.cs create mode 100644 Content.Client/PowerCell/PowerCellVisualLayers.cs delete mode 100644 Content.Client/PowerCell/PowerCellVisualsComponent.cs delete mode 100644 Content.Server/Power/Components/ActiveChargerComponent.cs delete mode 100644 Content.Server/Power/Components/ExaminableBatteryComponent.cs delete mode 100644 Content.Server/Power/EntitySystems/ChargerSystem.cs delete mode 100644 Content.Server/PowerCell/PowerCellSystem.Draw.cs delete mode 100644 Content.Server/PowerCell/PowerCellSystem.cs delete mode 100644 Content.Server/Weapons/Ranged/Systems/GunSystem.Battery.cs create mode 100644 Content.Shared/Kitchen/BeingMicrowavedEvent.cs create mode 100644 Content.Shared/Power/Components/ExaminableBatteryComponent.cs create mode 100644 Content.Shared/Power/Components/InsideChargerComponent.cs create mode 100644 Content.Shared/Power/Components/PredictedBatteryComponent.cs create mode 100644 Content.Shared/Power/Components/PredictedBatterySelfRechargerComponent.cs create mode 100644 Content.Shared/Power/Components/PredictedBatteryVisualsComponent.cs create mode 100644 Content.Shared/Power/EntitySystems/ChargerSystem.cs create mode 100644 Content.Shared/Power/EntitySystems/PredictedBatterySystem.API.cs create mode 100644 Content.Shared/Power/EntitySystems/PredictedBatterySystem.cs delete mode 100644 Content.Shared/Power/EntitySystems/SharedChargerSystem.cs delete mode 100644 Content.Shared/Power/SharedPowerItemCharger.cs create mode 100644 Content.Shared/PowerCell/Components/PowerCellDrawComponent.cs delete mode 100644 Content.Shared/PowerCell/PowerCellDrawComponent.cs rename Content.Shared/PowerCell/{PowerCellSlotEmptyEvent.cs => PowerCellEvents.cs} (53%) create mode 100644 Content.Shared/PowerCell/PowerCellSystem.API.cs create mode 100644 Content.Shared/PowerCell/PowerCellSystem.Draw.cs create mode 100644 Content.Shared/PowerCell/PowerCellSystem.Relay.cs create mode 100644 Content.Shared/PowerCell/PowerCellSystem.cs delete mode 100644 Content.Shared/PowerCell/SharedPowerCellSystem.cs delete mode 100644 Content.Shared/Weapons/Ranged/Components/HitscanBatteryAmmoProviderComponent.cs delete mode 100644 Content.Shared/Weapons/Ranged/Components/ProjectileBatteryAmmoProviderComponent.cs delete mode 100644 Content.Shared/Weapons/Ranged/Events/UpdateClientAmmoEvent.cs diff --git a/Content.Client/Power/EntitySystems/ChargerSystem.cs b/Content.Client/Power/EntitySystems/ChargerSystem.cs deleted file mode 100644 index efadde30e0..0000000000 --- a/Content.Client/Power/EntitySystems/ChargerSystem.cs +++ /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 index 8d9dd5ebdd..0000000000 --- a/Content.Client/PowerCell/PowerCellSystem.cs +++ /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(OnPowerCellVisualsChange); - } - - /// - 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; - } - - /// - 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(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 index 0000000000..f0caf76a79 --- /dev/null +++ b/Content.Client/PowerCell/PowerCellVisualLayers.cs @@ -0,0 +1,11 @@ +namespace Content.Client.PowerCell; + +/// +/// Sprite layers for power cells. +/// For use with the generic visualizer. +/// +public enum PowerCellVisualLayers : byte +{ + Base, + Unshaded, +} diff --git a/Content.Client/PowerCell/PowerCellVisualsComponent.cs b/Content.Client/PowerCell/PowerCellVisualsComponent.cs deleted file mode 100644 index 37dfd78839..0000000000 --- a/Content.Client/PowerCell/PowerCellVisualsComponent.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Content.Client.PowerCell; - -[RegisterComponent] -public sealed partial class PowerCellVisualsComponent : Component {} diff --git a/Content.Client/PowerCell/PowerChargerVisualizerComponent.cs b/Content.Client/PowerCell/PowerChargerVisualizerComponent.cs index d96830b5f8..cac503560d 100644 --- a/Content.Client/PowerCell/PowerChargerVisualizerComponent.cs +++ b/Content.Client/PowerCell/PowerChargerVisualizerComponent.cs @@ -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 /// /// The base sprite state used if the power cell charger does not contain a power cell. /// - [DataField("emptyState")] - [ViewVariables(VVAccess.ReadWrite)] + [DataField] public string EmptyState = "empty"; /// /// The base sprite state used if the power cell charger contains a power cell. /// - [DataField("occupiedState")] - [ViewVariables(VVAccess.ReadWrite)] + [DataField] public string OccupiedState = "full"; /// @@ -27,8 +25,7 @@ public sealed partial class PowerChargerVisualsComponent : Component /// Maps to the state used when the charger is charging a power cell. /// Maps to the state used when the charger contains a fully charged power cell. /// - [DataField("lightStates")] - [ViewVariables(VVAccess.ReadWrite)] + [DataField] public Dictionary LightStates = new() { [CellChargerStatus.Off] = "light-off", diff --git a/Content.Client/PowerCell/PowerChargerVisualizerSystem.cs b/Content.Client/PowerCell/PowerChargerVisualizerSystem.cs index c76e25b7af..bb0a35d0b3 100644 --- a/Content.Client/PowerCell/PowerChargerVisualizerSystem.cs +++ b/Content.Client/PowerCell/PowerChargerVisualizerSystem.cs @@ -1,4 +1,4 @@ -using Content.Shared.Power; +using Content.Shared.Power.Components; using Robust.Client.GameObjects; namespace Content.Client.PowerCell; diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.Battery.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.Battery.cs index 122244e7f2..b8b3d2ad87 100644 --- a/Content.Client/Weapons/Ranged/Systems/GunSystem.Battery.cs +++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.Battery.cs @@ -7,23 +7,20 @@ public sealed partial class GunSystem protected override void InitializeBattery() { base.InitializeBattery(); - // Hitscan - SubscribeLocalEvent(OnControl); - SubscribeLocalEvent(OnAmmoCountUpdate); - // Projectile - SubscribeLocalEvent(OnControl); - SubscribeLocalEvent(OnAmmoCountUpdate); + SubscribeLocalEvent(OnAmmoCountUpdate); + SubscribeLocalEvent(OnControl); } - private void OnAmmoCountUpdate(EntityUid uid, BatteryAmmoProviderComponent component, UpdateAmmoCounterEvent args) + private void OnAmmoCountUpdate(Entity 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 ent, ref AmmoCounterControlEvent args) { args.Control = new BoxesStatusControl(); } diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs index adef067b60..d3dfd50cbf 100644 --- a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs +++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs @@ -80,7 +80,6 @@ public sealed partial class GunSystem : SharedGunSystem base.Initialize(); UpdatesOutsidePrediction = true; SubscribeLocalEvent(OnAmmoCounterCollect); - SubscribeLocalEvent(OnUpdateClientAmmo); SubscribeAllEvent(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; diff --git a/Content.Server/Access/Systems/IdCardSystem.cs b/Content.Server/Access/Systems/IdCardSystem.cs index f317e88f0f..db39491741 100644 --- a/Content.Server/Access/Systems/IdCardSystem.cs +++ b/Content.Server/Access/Systems/IdCardSystem.cs @@ -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; diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs index 4f96a25a5c..ef102cb38a 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs @@ -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(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(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(args.Target, out var battery)) { Verb refillBattery = new() diff --git a/Content.Server/Construction/Completions/BuildMech.cs b/Content.Server/Construction/Completions/BuildMech.cs index c0b5921db9..5f1e40c347 100644 --- a/Content.Server/Construction/Completions/BuildMech.cs +++ b/Content.Server/Construction/Completions/BuildMech.cs @@ -48,7 +48,7 @@ public sealed partial class BuildMech : IGraphAction var cell = container.ContainedEntities[0]; - if (!entityManager.TryGetComponent(cell, out var batteryComponent)) + if (!entityManager.TryGetComponent(cell, out var batteryComponent)) { Logger.Warning($"Mech construct entity {uid} had an invalid entity in container \"{Container}\"! Aborting build mech action."); return; diff --git a/Content.Server/Holosign/HolosignSystem.cs b/Content.Server/Holosign/HolosignSystem.cs index beb5e909c0..7d01ffb975 100644 --- a/Content.Server/Holosign/HolosignSystem.cs +++ b/Content.Server/Holosign/HolosignSystem.cs @@ -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); - } } diff --git a/Content.Server/Kitchen/Components/MicrowaveComponent.cs b/Content.Server/Kitchen/Components/MicrowaveComponent.cs index 5337d80fd1..85478b83cd 100644 --- a/Content.Server/Kitchen/Components/MicrowaveComponent.cs +++ b/Content.Server/Kitchen/Components/MicrowaveComponent.cs @@ -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; - } - } } diff --git a/Content.Server/Light/EntitySystems/HandheldLightSystem.cs b/Content.Server/Light/EntitySystems/HandheldLightSystem.cs index 7167aa4963..a7f5801f32 100644 --- a/Content.Server/Light/EntitySystems/HandheldLightSystem.cs +++ b/Content.Server/Light/EntitySystems/HandheldLightSystem.cs @@ -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 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>(); @@ -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 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(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); diff --git a/Content.Server/Mech/Systems/MechSystem.cs b/Content.Server/Mech/Systems/MechSystem.cs index 89297a6e86..80169cb2ed 100644 --- a/Content.Server/Mech/Systems/MechSystem.cs +++ b/Content.Server/Mech/Systems/MechSystem.cs @@ -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(uid, out var panel) && !panel.Open) return; - if (component.BatterySlot.ContainedEntity == null && TryComp(args.Used, out var battery)) + if (component.BatterySlot.ContainedEntity == null && TryComp(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(args.Entity, out var battery)) + if (args.Container != component.BatterySlot || !TryComp(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(battery, out var batteryComp)) + if (!TryComp(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); diff --git a/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs b/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs index 0e1d27a3c5..92c5f59b15 100644 --- a/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs +++ b/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs @@ -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; diff --git a/Content.Server/Medical/DefibrillatorSystem.cs b/Content.Server/Medical/DefibrillatorSystem.cs index 14ee155d2a..719bd9946c 100644 --- a/Content.Server/Medical/DefibrillatorSystem.cs +++ b/Content.Server/Medical/DefibrillatorSystem.cs @@ -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; diff --git a/Content.Server/Medical/HealthAnalyzerSystem.cs b/Content.Server/Medical/HealthAnalyzerSystem.cs index f022dff5e3..bc6285c1d2 100644 --- a/Content.Server/Medical/HealthAnalyzerSystem.cs +++ b/Content.Server/Medical/HealthAnalyzerSystem.cs @@ -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 /// private void OnAfterInteract(Entity uid, ref AfterInteractEvent args) { - if (args.Target == null || !args.CanReach || !HasComp(args.Target) || !_cell.HasDrawCharge(uid, user: args.User)) + if (args.Target == null || !args.CanReach || !HasComp(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 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) diff --git a/Content.Server/Ninja/Systems/BatteryDrainerSystem.cs b/Content.Server/Ninja/Systems/BatteryDrainerSystem.cs index 7b9d229d73..f6d9d7bec7 100644 --- a/Content.Server/Ninja/Systems/BatteryDrainerSystem.cs +++ b/Content.Server/Ninja/Systems/BatteryDrainerSystem.cs @@ -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(target)) + if (args.Handled || comp.BatteryUid is not { } battery || !HasComp(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 ent, EntityUid target) { var (uid, comp) = ent; - if (comp.BatteryUid == null || !TryComp(comp.BatteryUid.Value, out var battery)) + if (comp.BatteryUid == null || !TryComp(comp.BatteryUid.Value, out var battery)) return false; if (!TryComp(target, out var targetBattery) || !TryComp(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)); } } diff --git a/Content.Server/Ninja/Systems/ItemCreatorSystem.cs b/Content.Server/Ninja/Systems/ItemCreatorSystem.cs index d7a7be995d..227fdea789 100644 --- a/Content.Server/Ninja/Systems/ItemCreatorSystem.cs +++ b/Content.Server/Ninja/Systems/ItemCreatorSystem.cs @@ -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 ent, ref CreateItemEvent args) { var (uid, comp) = ent; - if (comp.Battery is not {} battery) + if (comp.Battery is not { } battery) return; args.Handled = true; diff --git a/Content.Server/Ninja/Systems/NinjaSuitSystem.cs b/Content.Server/Ninja/Systems/NinjaSuitSystem.cs index 399d94e8f7..8ebd56ea7c 100644 --- a/Content.Server/Ninja/Systems/NinjaSuitSystem.cs +++ b/Content.Server/Ninja/Systems/NinjaSuitSystem.cs @@ -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; /// /// Handles power cell upgrading and actions. +/// TODO: Move all of this to shared and predict it /// 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(args.EntityUid, out var inserting)) + if (!TryComp(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(uid, out var selfcomp) && selfcomp.AutoRecharge) + if (TryComp(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 ent, ref NinjaEmpEvent args) { var (uid, comp) = ent; diff --git a/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs b/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs index fd7f908738..f2fc9abfcc 100644 --- a/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs +++ b/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs @@ -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(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(); @@ -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. /// /// Update the alert for the ninja's suit power indicator. /// @@ -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 /// /// Get the battery component in a ninja's suit, if it's worn. /// - 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(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; } diff --git a/Content.Server/Ninja/Systems/StunProviderSystem.cs b/Content.Server/Ninja/Systems/StunProviderSystem.cs index 98df8a039a..49d7ab5e85 100644 --- a/Content.Server/Ninja/Systems/StunProviderSystem.cs +++ b/Content.Server/Ninja/Systems/StunProviderSystem.cs @@ -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; /// 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!; diff --git a/Content.Server/Nuke/NukeSystem.cs b/Content.Server/Nuke/NukeSystem.cs index f110734a60..3788d2b2ab 100644 --- a/Content.Server/Nuke/NukeSystem.cs +++ b/Content.Server/Nuke/NukeSystem.cs @@ -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; diff --git a/Content.Server/PAI/PAISystem.cs b/Content.Server/PAI/PAISystem.cs index 7fd58f1572..a23defdcea 100644 --- a/Content.Server/PAI/PAISystem.cs +++ b/Content.Server/PAI/PAISystem.cs @@ -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; diff --git a/Content.Server/Pinpointer/StationMapSystem.cs b/Content.Server/Pinpointer/StationMapSystem.cs index b0b3141fb0..c8e5b22617 100644 --- a/Content.Server/Pinpointer/StationMapSystem.cs +++ b/Content.Server/Pinpointer/StationMapSystem.cs @@ -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 index f3d863c9e4..0000000000 --- a/Content.Server/Power/Components/ActiveChargerComponent.cs +++ /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 index 4cf4abd3cf..0000000000 --- a/Content.Server/Power/Components/ExaminableBatteryComponent.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Content.Server.Power.Components -{ - [RegisterComponent] - public sealed partial class ExaminableBatteryComponent : Component - {} -} diff --git a/Content.Server/Power/EntitySystems/BatterySystem.API.cs b/Content.Server/Power/EntitySystems/BatterySystem.API.cs index 6ddf1f4bbc..5683e7c133 100644 --- a/Content.Server/Power/EntitySystems/BatterySystem.API.cs +++ b/Content.Server/Power/EntitySystems/BatterySystem.API.cs @@ -1,8 +1,14 @@ using Content.Shared.Power; using Content.Shared.Power.Components; +using Content.Shared.Power.EntitySystems; namespace Content.Server.Power.EntitySystems; +/// +/// Responsible for . +/// Unpredicted equivalent of . +/// If you make changes to this make sure to keep the two consistent. +/// public sealed partial class BatterySystem { public override float ChangeCharge(Entity 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 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); } + /// + /// Gets the battery's current charge. + /// + public float GetCharge(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return 0f; + + return ent.Comp.CurrentCharge; + } + + /// + /// Gets number of remaining uses for the given charge cost. + /// + public int GetRemainingUses(Entity ent, float cost) + { + if (cost <= 0) + return 0; + + if (!Resolve(ent, ref ent.Comp)) + return 0; + + return (int)(ent.Comp.CurrentCharge / cost); + } + + /// + /// Gets number of maximum uses at full charge for the given charge cost. + /// + public int GetMaxUses(Entity 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 ent) { if (!Resolve(ent, ref ent.Comp, false)) diff --git a/Content.Server/Power/EntitySystems/BatterySystem.cs b/Content.Server/Power/EntitySystems/BatterySystem.cs index 4b6a52c0f1..1a88672bc9 100644 --- a/Content.Server/Power/EntitySystems/BatterySystem.cs +++ b/Content.Server/Power/EntitySystems/BatterySystem.cs @@ -11,6 +11,11 @@ using Robust.Shared.Timing; namespace Content.Server.Power.EntitySystems; +/// +/// Responsible for . +/// Unpredicted equivalent of . +/// If you make changes to this make sure to keep the two consistent. +/// [UsedImplicitly] public sealed partial class BatterySystem : SharedBatterySystem { @@ -20,7 +25,8 @@ public sealed partial class BatterySystem : SharedBatterySystem { base.Initialize(); - SubscribeLocalEvent(OnExamine); + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnExamine); SubscribeLocalEvent(OnBatteryRejuvenate); SubscribeLocalEvent(OnNetBatteryRejuvenate); SubscribeLocalEvent(CalculateBatteryPrice); @@ -31,27 +37,31 @@ public sealed partial class BatterySystem : SharedBatterySystem SubscribeLocalEvent(PostSync); } + private void OnInit(Entity ent, ref ComponentInit args) + { + DebugTools.Assert(!HasComp(ent), $"{ent} has both BatteryComponent and PredictedBatteryComponent"); + } private void OnNetBatteryRejuvenate(Entity ent, ref RejuvenateEvent args) { ent.Comp.NetworkBattery.CurrentStorage = ent.Comp.NetworkBattery.Capacity; } - private void OnBatteryRejuvenate(Entity ent, ref RejuvenateEvent args) { SetCharge(ent.AsNullable(), ent.Comp.MaxCharge); } - private void OnExamine(Entity ent, ref ExaminedEvent args) + private void OnExamine(Entity ent, ref ExaminedEvent args) { if (!args.IsInDetailsRange) return; - if (!TryComp(ent, out var battery)) + if (!HasComp(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 index d523de65db..0000000000 --- a/Content.Server/Power/EntitySystems/ChargerSystem.cs +++ /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(OnStartup); - SubscribeLocalEvent(OnPowerChanged); - SubscribeLocalEvent(OnInserted); - SubscribeLocalEvent(OnRemoved); - SubscribeLocalEvent(OnInsertAttempt); - SubscribeLocalEvent(OnEntityStorageInsertAttempt); - SubscribeLocalEvent(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(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(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(); - 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); - } - - /// - /// Verify that the entity being inserted is actually rechargeable. - /// - private void OnInsertAttempt(EntityUid uid, ChargerComponent component, ContainerIsInsertingAttemptEvent args) - { - if (!component.Initialized) - return; - - if (args.Container.ID != component.SlotId) - return; - - if (!TryComp(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(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(uid); - } - else - { - RemComp(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(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; - } -} diff --git a/Content.Server/Power/EntitySystems/PowerReceiverSystem.cs b/Content.Server/Power/EntitySystems/PowerReceiverSystem.cs index f3405486e6..f5ecb118dc 100644 --- a/Content.Server/Power/EntitySystems/PowerReceiverSystem.cs +++ b/Content.Server/Power/EntitySystems/PowerReceiverSystem.cs @@ -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) diff --git a/Content.Server/Power/EntitySystems/RiggableSystem.cs b/Content.Server/Power/EntitySystems/RiggableSystem.cs index 0f8b32865b..3972cf3f86 100644 --- a/Content.Server/Power/EntitySystems/RiggableSystem.cs +++ b/Content.Server/Power/EntitySystems/RiggableSystem.cs @@ -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(OnRejuvenate); SubscribeLocalEvent(OnMicrowaved); SubscribeLocalEvent(OnSolutionChanged); + SubscribeLocalEvent(OnChargeChanged); + SubscribeLocalEvent(OnChargeChanged); } private void OnRejuvenate(Entity entity, ref RejuvenateEvent args) @@ -34,14 +39,22 @@ public sealed class RiggableSystem : EntitySystem { if (TryComp(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(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 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 ent, ref ChargeChangedEvent args) + { + if (!ent.Comp.IsRigged) + return; + + if (TryComp(ent, out var batteryComponent)) + { + if (batteryComponent.CurrentCharge == 0f) + return; + + Explode(ent, batteryComponent.CurrentCharge); + } + } + + // predicted batteries + private void OnChargeChanged(Entity 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(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); + } } } diff --git a/Content.Server/Power/SetBatteryPercentCommand.cs b/Content.Server/Power/SetBatteryPercentCommand.cs index a6123382fd..bd48e6cd97 100644 --- a/Content.Server/Power/SetBatteryPercentCommand.cs +++ b/Content.Server/Power/SetBatteryPercentCommand.cs @@ -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(id, out var battery)) + if (EntityManager.TryGetComponent(id, out var battery)) + _batterySystem.SetCharge((id.Value, battery), battery.MaxCharge * percent / 100); + else if (EntityManager.TryGetComponent(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 index d0dafb1ef6..0000000000 --- a/Content.Server/PowerCell/PowerCellSystem.Draw.cs +++ /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(); - - 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 index f358e1bc34..0000000000 --- a/Content.Server/PowerCell/PowerCellSystem.cs +++ /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; - -/// -/// Handles Power cells -/// -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(OnChargeChanged); - SubscribeLocalEvent(OnCellExamined); - - SubscribeLocalEvent(OnDrawChargeChanged); - SubscribeLocalEvent(OnDrawCellChanged); - - SubscribeLocalEvent(OnCellSlotExamined); - // funny - SubscribeLocalEvent(OnSlotMicrowaved); - - SubscribeLocalEvent(OnGetCharge); - SubscribeLocalEvent(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(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 - /// - 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); - } - - /// - /// Tries to use the for this entity. - /// - /// Popup to this user with the relevant detail if specified. - 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; - } - - /// - 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 - - /// - /// Returns whether the entity has a slotted battery and charge for the requested action. - /// - /// Popup to this user with the relevant detail if specified. - 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; - } - - /// - /// Tries to use charge from a slotted battery. - /// - 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(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 entity, ref GetChargeEvent args) - { - if (!TryGetBatteryFromSlot(entity, out var batteryUid, out _)) - return; - - RaiseLocalEvent(batteryUid.Value, ref args); - } - - private void OnChangeCharge(Entity entity, ref ChangeChargeEvent args) - { - if (!TryGetBatteryFromSlot(entity, out var batteryUid, out _)) - return; - - RaiseLocalEvent(batteryUid.Value, ref args); - } -} diff --git a/Content.Server/Radio/EntitySystems/JammerSystem.cs b/Content.Server/Radio/EntitySystems/JammerSystem.cs index 3ec2e9c38c..750010c9cc 100644 --- a/Content.Server/Radio/EntitySystems/JammerSystem.cs +++ b/Content.Server/Radio/EntitySystems/JammerSystem.cs @@ -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(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(); @@ -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(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(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); diff --git a/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs b/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs index 8dd074e6a0..1237919b20 100644 --- a/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs +++ b/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs @@ -27,10 +27,8 @@ public sealed partial class BorgSystem SubscribeLocalEvent(OnPacketReceived); } - public override void Update(float frameTime) + public void UpdateTransponder(float frameTime) { - base.Update(frameTime); - var now = _timing.CurTime; var query = EntityQueryEnumerator(); 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); diff --git a/Content.Server/Silicons/Borgs/BorgSystem.Ui.cs b/Content.Server/Silicons/Borgs/BorgSystem.Ui.cs index 58cd7135af..d718916abf 100644 --- a/Content.Server/Silicons/Borgs/BorgSystem.Ui.cs +++ b/Content.Server/Silicons/Borgs/BorgSystem.Ui.cs @@ -91,6 +91,43 @@ public sealed partial class BorgSystem UpdateUI(uid, component); } + public void UpdateBattery(Entity 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 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(); + while (query.MoveNext(out var uid, out var borgChassis)) + { + if (curTime < borgChassis.NextBatteryUpdate) + continue; + + UpdateBattery((uid, borgChassis)); + borgChassis.NextBatteryUpdate = curTime + TimeSpan.FromSeconds(1); + } + } } diff --git a/Content.Server/Silicons/Borgs/BorgSystem.cs b/Content.Server/Silicons/Borgs/BorgSystem.cs index 6ae61acc57..24fde24f33 100644 --- a/Content.Server/Silicons/Borgs/BorgSystem.cs +++ b/Content.Server/Silicons/Borgs/BorgSystem.cs @@ -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 BorgJobId = "Borg"; @@ -76,6 +78,7 @@ public sealed partial class BorgSystem : SharedBorgSystem SubscribeLocalEvent(OnMobStateChanged); SubscribeLocalEvent(OnBeingGibbed); SubscribeLocalEvent(OnPowerCellChanged); + SubscribeLocalEvent(OnBatteryChargeChanged); SubscribeLocalEvent(OnPowerCellSlotEmpty); SubscribeLocalEvent(OnGetDeadIC); SubscribeLocalEvent(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 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 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 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? 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); + } } diff --git a/Content.Server/Speech/EntitySystems/DamagedSiliconAccentSystem.cs b/Content.Server/Speech/EntitySystems/DamagedSiliconAccentSystem.cs index a5e30346bd..99cd0ca9e2 100644 --- a/Content.Server/Speech/EntitySystems/DamagedSiliconAccentSystem.cs +++ b/Content.Server/Speech/EntitySystems/DamagedSiliconAccentSystem.cs @@ -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) diff --git a/Content.Server/Stunnable/Systems/StunbatonSystem.cs b/Content.Server/Stunnable/Systems/StunbatonSystem.cs index a22a4c09fa..b1bae0127c 100644 --- a/Content.Server/Stunnable/Systems/StunbatonSystem.cs +++ b/Content.Server/Stunnable/Systems/StunbatonSystem.cs @@ -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(OnExamined); SubscribeLocalEvent(OnSolutionChange); SubscribeLocalEvent(OnStaminaHitAttempt); - SubscribeLocalEvent(OnChargeChanged); + SubscribeLocalEvent(OnChargeChanged); } private void OnStaminaHitAttempt(Entity entity, ref StaminaDamageOnHitAttemptEvent args) { if (!_itemToggle.IsActivated(entity.Owner) || - !TryComp(entity.Owner, out var battery) || !_battery.TryUseCharge((entity.Owner, battery), entity.Comp.EnergyPerUse)) + !TryComp(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(entity.Owner, out var battery)) + if (TryComp(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(entity, out var battery) || battery.CurrentCharge < entity.Comp.EnergyPerUse) + if (!TryComp(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(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(entity, out var riggable) || - !TryComp(entity, out var battery)) + !TryComp(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 entity, ref ChargeChangedEvent args) + private void OnChargeChanged(Entity entity, ref PredictedBatteryChargeChangedEvent args) { - if (TryComp(entity.Owner, out var battery) && - battery.CurrentCharge < entity.Comp.EnergyPerUse) + if (TryComp(entity.Owner, out var battery) && + _battery.GetCharge((entity.Owner, battery)) < entity.Comp.EnergyPerUse) { _itemToggle.TryDeactivate(entity.Owner, predicted: false); } diff --git a/Content.Server/Weapons/Misc/TetherGunSystem.cs b/Content.Server/Weapons/Misc/TetherGunSystem.cs index 2bf53d46f4..d1984448c5 100644 --- a/Content.Server/Weapons/Misc/TetherGunSystem.cs +++ b/Content.Server/Weapons/Misc/TetherGunSystem.cs @@ -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 index c1e442c24b..0000000000 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.Battery.cs +++ /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(OnBatteryStartup); - SubscribeLocalEvent(OnBatteryChargeChange); - SubscribeLocalEvent(OnPowerCellChanged); - - // Projectile - SubscribeLocalEvent(OnBatteryStartup); - SubscribeLocalEvent(OnBatteryChargeChange); - SubscribeLocalEvent(OnPowerCellChanged); - } - - private void OnBatteryStartup(Entity entity, ref ComponentStartup args) where T : BatteryAmmoProviderComponent - { - UpdateShots(entity, entity.Comp); - } - - private void OnBatteryChargeChange(Entity entity, ref ChargeChangedEvent args) where T : BatteryAmmoProviderComponent - { - UpdateShots(entity, entity.Comp, args.Charge, args.MaxCharge); - } - - private void OnPowerCellChanged(Entity 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 entity) - { - // Take charge from either the BatteryComponent or PowerCellSlotComponent. - var ev = new ChangeChargeEvent(-entity.Comp.FireCost); - RaiseLocalEvent(entity, ref ev); - } -} diff --git a/Content.Server/Xenoarchaeology/Artifact/XAE/XAEChargeBatterySystem.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/XAEChargeBatterySystem.cs index 21952912d7..c3f4926a12 100644 --- a/Content.Server/Xenoarchaeology/Artifact/XAE/XAEChargeBatterySystem.cs +++ b/Content.Server/Xenoarchaeology/Artifact/XAE/XAEChargeBatterySystem.cs @@ -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 { [Dependency] private readonly BatterySystem _battery = default!; + [Dependency] private readonly PredictedBatterySystem _predictedBattery = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; /// Pre-allocated and re-used collection. private readonly HashSet> _batteryEntities = new(); + private readonly HashSet> _pBatteryEntities = new(); /// protected override void OnActivated(Entity 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 index 0000000000..c053eda997 --- /dev/null +++ b/Content.Shared/Kitchen/BeingMicrowavedEvent.cs @@ -0,0 +1,10 @@ +namespace Content.Shared.Kitchen; + +/// +/// Raised on an entity when it is inside a microwave and it starts cooking. +/// +public sealed class BeingMicrowavedEvent(EntityUid microwave, EntityUid? user) : HandledEntityEventArgs +{ + public EntityUid Microwave = microwave; + public EntityUid? User = user; +} diff --git a/Content.Shared/Mech/EntitySystems/SharedMechSystem.cs b/Content.Shared/Mech/EntitySystems/SharedMechSystem.cs index c461b588b1..f2ba8c69f5 100644 --- a/Content.Shared/Mech/EntitySystems/SharedMechSystem.cs +++ b/Content.Shared/Mech/EntitySystems/SharedMechSystem.cs @@ -283,6 +283,7 @@ public abstract partial class SharedMechSystem : EntitySystem /// /// Attempts to change the amount of energy in the mech. + /// TODO: Power cells are predicted now, so no need to duplicate the charge level /// /// The mech itself /// The change in energy diff --git a/Content.Shared/Power/ChargeEvents.cs b/Content.Shared/Power/ChargeEvents.cs index cc9dc49a7c..f84b255f12 100644 --- a/Content.Shared/Power/ChargeEvents.cs +++ b/Content.Shared/Power/ChargeEvents.cs @@ -5,14 +5,44 @@ namespace Content.Shared.Power; /// /// Raised when a battery's charge or capacity changes (capacity affects relative charge percentage). +/// Only raised for entities with . /// [ByRefEvent] public readonly record struct ChargeChangedEvent(float Charge, float MaxCharge); +/// +/// Raised when a predicted battery's charge or capacity changes (capacity affects relative charge percentage). +/// Unlike 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 . +/// +[ByRefEvent] +public readonly record struct PredictedBatteryChargeChangedEvent(float CurrentCharge, float CurrentChargeRate, TimeSpan CurrentTime, float MaxCharge); + +/// +/// Raised when a battery changes its state between full, empty, or neither. +/// Used only for . +/// +[ByRefEvent] +public record struct PredictedBatteryStateChangedEvent(BatteryState OldState, BatteryState NewState); + +/// +/// Raised to calculate a predicted battery's recharge rate. +/// Subscribe to this to offset its current charge rate. +/// Used only for . +/// +[ByRefEvent] +public record struct RefreshChargeRateEvent(float MaxCharge) +{ + public readonly float MaxCharge = MaxCharge; + public float NewChargeRate; +} + /// /// Event that supports multiple battery types. /// Raised when it is necessary to get information about battery charges. -/// Works with either or . +/// Works with either , , or . /// If there are multiple batteries then the results will be summed up. /// [ByRefEvent] @@ -25,7 +55,7 @@ public record struct GetChargeEvent /// /// Method event that supports multiple battery types. /// Raised when it is necessary to change the current battery charge by some value. -/// Works with either or . +/// Works with either , , or . /// If there are multiple batteries then they will be changed in order of subscription until the total value was reached. /// [ByRefEvent] diff --git a/Content.Shared/Power/Components/BatteryComponent.cs b/Content.Shared/Power/Components/BatteryComponent.cs index 6a65405115..396896a6c5 100644 --- a/Content.Shared/Power/Components/BatteryComponent.cs +++ b/Content.Shared/Power/Components/BatteryComponent.cs @@ -5,6 +5,8 @@ namespace Content.Shared.Power.Components; /// /// Battery node on the pow3r network. Needs other components to connect to actual networks. +/// Use this for batteries that cannot be predicted. +/// Use otherwise. /// [RegisterComponent] [Virtual] diff --git a/Content.Shared/Power/Components/BatterySelfRechargerComponent.cs b/Content.Shared/Power/Components/BatterySelfRechargerComponent.cs index 3881980382..7a5665ae82 100644 --- a/Content.Shared/Power/Components/BatterySelfRechargerComponent.cs +++ b/Content.Shared/Power/Components/BatterySelfRechargerComponent.cs @@ -5,6 +5,7 @@ namespace Content.Shared.Power.Components; /// /// Self-recharging battery. /// To be used in combination with . +/// For use instead. /// [RegisterComponent, AutoGenerateComponentPause] public sealed partial class BatterySelfRechargerComponent : Component @@ -16,7 +17,7 @@ public sealed partial class BatterySelfRechargerComponent : Component public bool AutoRecharge = true; /// - /// At what rate does the entity automatically recharge? + /// At what rate does the entity automatically recharge? In watts. /// [DataField] public float AutoRechargeRate; diff --git a/Content.Shared/Power/Components/ChargerComponent.cs b/Content.Shared/Power/Components/ChargerComponent.cs index a3f2f8f424..930ff059f3 100644 --- a/Content.Shared/Power/Components/ChargerComponent.cs +++ b/Content.Shared/Power/Components/ChargerComponent.cs @@ -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; - /// - /// The charge rate of the charger, in watts + /// The charge rate of the charger, in watts. /// - [DataField] + [DataField, AutoNetworkedField] public float ChargeRate = 20.0f; + /// + /// 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. + /// + [DataField, AutoNetworkedField] + public float PassiveDraw = 1f; + /// /// The container ID that is holds the entities being charged. /// @@ -24,13 +29,29 @@ public sealed partial class ChargerComponent : Component /// /// A whitelist for what entities can be charged by this Charger. /// - [DataField] + [DataField, AutoNetworkedField] public EntityWhitelist? Whitelist; /// /// Indicates whether the charger is portable and thus subject to EMP effects /// and bypasses checks for transform, anchored, and ApcPowerReceiverComponent. /// - [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 index 0000000000..59d0b8792b --- /dev/null +++ b/Content.Shared/Power/Components/ExaminableBatteryComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Power.Components; + +/// +/// Allows the charge of a battery to be seen by examination. +/// Works with either or . +/// +[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 index 0000000000..64e5702782 --- /dev/null +++ b/Content.Shared/Power/Components/InsideChargerComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Power.Components; + +/// +/// This entity is currently inside the charging slot of an entity with . +/// Added regardless whether or not the charger is powered. +/// +[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 index 0000000000..0db53245eb --- /dev/null +++ b/Content.Shared/Power/Components/PredictedBatteryComponent.cs @@ -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; + +/// +/// Predicted equivalent to . +/// 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. +/// +/// +/// We cannot simply network 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. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +[Access(typeof(PredictedBatterySystem))] +public sealed partial class PredictedBatteryComponent : Component +{ + /// + /// Maximum charge of the battery in joules (ie. watt seconds) + /// + [DataField, AutoNetworkedField, ViewVariables] + [GuidebookData] + public float MaxCharge; + + /// + /// The price per one joule. Default is 1 speso for 10kJ. + /// + [DataField] + public float PricePerJoule = 0.0001f; + + /// + /// Time stamp of the last networked update. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoNetworkedField, AutoPausedField, ViewVariables] + public TimeSpan LastUpdate = TimeSpan.Zero; + + /// + /// The intial charge to be set on map init. + /// + [DataField] + public float StartingCharge; + + /// + /// The charge at the last update in joules (i.e. watt seconds). + /// + [DataField, AutoNetworkedField, ViewVariables] + public float LastCharge; + + /// + /// The current charge rate in watt. + /// + /// + /// Not a datafield as this is only cached and recalculated on component startup. + /// + [ViewVariables, AutoNetworkedField] + public float ChargeRate; + + /// + /// The current charge state of the battery. + /// Used to track state changes for raising . + /// + /// + /// Not a datafield as this is only cached and recalculated in an update loop. + /// + [ViewVariables, AutoNetworkedField] + public BatteryState State = BatteryState.Neither; +} + +/// +/// Charge level status of the battery. +/// +[Serializable, NetSerializable] +public enum BatteryState : byte +{ + /// + /// Full charge. + /// + Full, + /// + /// No charge. + /// + Empty, + /// + /// Neither full nor empty. + /// + Neither, +} + diff --git a/Content.Shared/Power/Components/PredictedBatterySelfRechargerComponent.cs b/Content.Shared/Power/Components/PredictedBatterySelfRechargerComponent.cs new file mode 100644 index 0000000000..449a4e1dc6 --- /dev/null +++ b/Content.Shared/Power/Components/PredictedBatterySelfRechargerComponent.cs @@ -0,0 +1,33 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Power.Components; + +/// +/// Self-recharging battery. +/// To be used in combination with . +/// For use instead. +/// +[RegisterComponent, NetworkedComponent] +[AutoGenerateComponentState, AutoGenerateComponentPause] +public sealed partial class PredictedBatterySelfRechargerComponent : Component +{ + /// + /// At what rate does the entity automatically recharge? In watts. + /// + [DataField, AutoNetworkedField, ViewVariables] + public float AutoRechargeRate; + + /// + /// How long should the entity stop automatically recharging if a charge is used? + /// + [DataField, AutoNetworkedField] + public TimeSpan AutoRechargePauseTime = TimeSpan.Zero; + + /// + /// Do not auto recharge if this timestamp has yet to happen, set for the auto recharge pause system. + /// + [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 index 0000000000..f7ea9338ab --- /dev/null +++ b/Content.Shared/Power/Components/PredictedBatteryVisualsComponent.cs @@ -0,0 +1,51 @@ +using Content.Shared.PowerCell.Components; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Power.Components; + +/// +/// Marker component that makes an entity with update its appearance data for use with visualizers. +/// Also works with an entity with and will relay the state of the inserted powercell. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class PredictedBatteryVisualsComponent : Component; + +/// +/// Keys for the appearance data. +/// +[Serializable, NetSerializable] +public enum BatteryVisuals : byte +{ + /// + /// The current charge state of the battery. + /// Either full, empty, or neither. + /// Uses a . + /// + State, + /// + /// Is the battery currently charging or discharging? + /// Uses a . + /// + Charging, +} + +/// +/// Charge level status of the battery. +/// +[Serializable, NetSerializable] +public enum BatteryChargingState : byte +{ + /// + /// PredictedBatteryComponent.ChargeRate > 0 + /// + Charging, + /// + /// PredictedBatteryComponent.ChargeRate < 0 + /// + Decharging, + /// + /// PredictedBatteryComponent.ChargeRate == 0 + /// + Constant, +} diff --git a/Content.Shared/Power/EntitySystems/ChargerSystem.cs b/Content.Shared/Power/EntitySystems/ChargerSystem.cs new file mode 100644 index 0000000000..54dac61157 --- /dev/null +++ b/Content.Shared/Power/EntitySystems/ChargerSystem.cs @@ -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(OnStartup); + SubscribeLocalEvent(OnPowerChanged); + SubscribeLocalEvent(OnInserted); + SubscribeLocalEvent(OnRemoved); + SubscribeLocalEvent(OnInsertAttempt); + SubscribeLocalEvent(OnEntityStorageInsertAttempt); + SubscribeLocalEvent(OnChargerExamine); + SubscribeLocalEvent(OnEmpPulse); + SubscribeLocalEvent(OnEmpRemoved); + SubscribeLocalEvent(OnRefreshChargeRate); + SubscribeLocalEvent(OnStatusChanged); + } + + private void OnStartup(Entity 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(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 ent, ref PowerChangedEvent args) + { + RefreshAllBatteries(ent); + UpdateStatus(ent); + } + + private void OnInserted(Entity ent, ref EntInsertedIntoContainerMessage args) + { + if (_timing.ApplyingState) + return; // Already networked in the same gamestate + + if (args.Container.ID != ent.Comp.SlotId) + return; + + AddComp(args.Entity); + if (SearchForBattery(args.Entity, out var battery)) + _battery.RefreshChargeRate(battery.Value.AsNullable()); + UpdateStatus(ent); + } + + private void OnRemoved(Entity ent, ref EntRemovedFromContainerMessage args) + { + if (_timing.ApplyingState) + return; // Already networked in the same gamestate + + if (args.Container.ID != ent.Comp.SlotId) + return; + + RemComp(args.Entity); + if (SearchForBattery(args.Entity, out var battery)) + _battery.RefreshChargeRate(battery.Value.AsNullable()); + UpdateStatus(ent); + } + + /// + /// Verify that the entity being inserted is actually rechargeable. + /// + private void OnInsertAttempt(EntityUid uid, ChargerComponent component, ContainerIsInsertingAttemptEvent args) + { + if (!component.Initialized) + return; + + if (args.Container.ID != component.SlotId) + return; + + if (!TryComp(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(uid, out var cellSlot)) + return; + + if (!cellSlot.FitsInCharger) + args.Cancelled = true; + } + private void OnEmpPulse(Entity ent, ref EmpPulseEvent args) + { + args.Affected = true; + args.Disabled = true; + RefreshAllBatteries(ent); + UpdateStatus(ent); + } + + private void OnEmpRemoved(Entity ent, ref EmpDisabledRemovedEvent args) + { + RefreshAllBatteries(ent); + UpdateStatus(ent); + } + + private void OnRefreshChargeRate(Entity ent, ref RefreshChargeRateEvent args) + { + var chargerUid = Transform(ent).ParentUid; + + if (HasComp(chargerUid)) + return; + + if (!TryComp(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 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(chargerUid, out var chargerComp)) + return; + + UpdateStatus((chargerUid, chargerComp)); + } + + private bool SearchForBattery(EntityUid uid, [NotNullWhen(true)] out Entity? battery) + { + // try get a battery directly on the inserted entity + if (TryComp(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 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 ent) + { + TryComp(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 ent) + { + if (!ent.Comp.Portable && !Transform(ent).Anchored) + return CellChargerStatus.Off; + + if (!ent.Comp.Portable && !_receiver.IsPowered(ent.Owner)) + return CellChargerStatus.Off; + + if (HasComp(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 index 0000000000..c06b458dc2 --- /dev/null +++ b/Content.Shared/Power/EntitySystems/PredictedBatterySystem.API.cs @@ -0,0 +1,278 @@ +using Content.Shared.Power.Components; +using JetBrains.Annotations; + +namespace Content.Shared.Power.EntitySystems; + +/// +/// Responsible for . +/// Predicted equivalent of . +/// If you make changes to this make sure to keep the two consistent. +/// +public sealed partial class PredictedBatterySystem +{ + /// + /// 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. + /// + /// The actually changed amount. + [PublicAPI] + public float ChangeCharge(Entity 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; + } + + /// + /// Removes the given amount of charge from the battery + /// and resets the self-recharge cooldown if it exists. + /// + /// The actually changed amount. + [PublicAPI] + public float UseCharge(Entity ent, float amount) + { + if (amount <= 0f) + return 0f; + + return ChangeCharge(ent, -amount); + } + + /// + /// 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. + /// + /// If the full amount was able to be removed. + [PublicAPI] + public bool TryUseCharge(Entity ent, float amount) + { + if (!Resolve(ent, ref ent.Comp, false) || amount > GetCharge(ent)) + return false; + + UseCharge(ent, amount); + return true; + } + + /// + /// Sets the battery's charge. + /// + [PublicAPI] + public void SetCharge(Entity 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); + } + + /// + /// Sets the battery's maximum charge. + /// + [PublicAPI] + public void SetMaxCharge(Entity 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); + } + + /// + /// Updates the battery's charge state and sends an event if it changed. + /// + [PublicAPI] + public void UpdateState(Entity 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); + } + + /// + /// Gets the battery's current charge. + /// + [PublicAPI] + public float GetCharge(Entity 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; + } + + /// + /// Gets number of remaining uses for the given charge cost. + /// + [PublicAPI] + public int GetRemainingUses(Entity ent, float cost) + { + if (cost <= 0) + return 0; + + if (!Resolve(ent, ref ent.Comp)) + return 0; + + return (int)(GetCharge(ent) / cost); + } + + /// + /// Gets number of maximum uses at full charge for the given charge cost. + /// + [PublicAPI] + public int GetMaxUses(Entity ent, float cost) + { + if (cost <= 0) + return 0; + + if (!Resolve(ent, ref ent.Comp)) + return 0; + + return (int)(ent.Comp.MaxCharge / cost); + } + + + /// + /// Refreshes the battery's current charge rate by raising a . + /// + /// The new charge rate. + [PublicAPI] + public float RefreshChargeRate(Entity 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; + } + + /// + /// Checks if the entity has a self recharge and puts it on cooldown if applicable. + /// Uses the cooldown time given in the component. + /// + [PublicAPI] + public void TrySetChargeCooldown(Entity 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); + } + + /// + /// Puts the entity's self recharge on cooldown for the specified time. + /// + [PublicAPI] + public void SetChargeCooldown(Entity 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. + } + + /// + /// Returns whether the battery is full. + /// + [PublicAPI] + public bool IsFull(Entity 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 index 0000000000..760d7277c6 --- /dev/null +++ b/Content.Shared/Power/EntitySystems/PredictedBatterySystem.cs @@ -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; + +/// +/// Responsible for . +/// Predicted equivalent of . +/// If you make changes to this make sure to keep the two consistent. +/// +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(OnInit); + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnEmpPulse); + SubscribeLocalEvent(OnRejuvenate); + SubscribeLocalEvent(OnExamine); + SubscribeLocalEvent(CalculateBatteryPrice); + SubscribeLocalEvent(OnChangeCharge); + SubscribeLocalEvent(OnGetCharge); + SubscribeLocalEvent(OnRefreshChargeRate); + SubscribeLocalEvent(OnRechargerStartup); + SubscribeLocalEvent(OnRechargerRemove); + SubscribeLocalEvent(OnVisualsChargeChanged); + SubscribeLocalEvent(OnVisualsStateChanged); + } + + private void OnInit(Entity ent, ref ComponentInit args) + { + DebugTools.Assert(!HasComp(ent), $"{ent} has both BatteryComponent and PredictedBatteryComponent"); + } + + private void OnStartup(Entity 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 ent, ref MapInitEvent args) + { + SetCharge(ent.AsNullable(), ent.Comp.StartingCharge); + RefreshChargeRate(ent.AsNullable()); + } + + private void OnRejuvenate(Entity ent, ref RejuvenateEvent args) + { + SetCharge(ent.AsNullable(), ent.Comp.MaxCharge); + } + + private void OnEmpPulse(Entity ent, ref EmpPulseEvent args) + { + args.Affected = true; + UseCharge(ent.AsNullable(), args.EnergyConsumption); + } + + private void OnExamine(Entity ent, ref ExaminedEvent args) + { + if (!args.IsInDetailsRange) + return; + + if (!HasComp(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") + ) + ); + } + + /// + /// Gets the price for the power contained in an entity's battery. + /// + private void CalculateBatteryPrice(Entity ent, ref PriceCalculationEvent args) + { + args.Price += GetCharge(ent.AsNullable()) * ent.Comp.PricePerJoule; + } + + private void OnChangeCharge(Entity ent, ref ChangeChargeEvent args) + { + if (args.ResidualValue == 0) + return; + + args.ResidualValue -= ChangeCharge(ent.AsNullable(), args.ResidualValue); + } + + private void OnGetCharge(Entity ent, ref GetChargeEvent args) + { + args.CurrentCharge += GetCharge(ent.AsNullable()); + args.MaxCharge += ent.Comp.MaxCharge; + } + + private void OnRefreshChargeRate(Entity 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(); + 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(); + 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 ent, ref ComponentStartup args) + { + // In case this component is added after the battery component. + RefreshChargeRate(ent.Owner); + } + + private void OnRechargerRemove(Entity 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 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 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); + } +} diff --git a/Content.Shared/Power/EntitySystems/SharedBatterySystem.cs b/Content.Shared/Power/EntitySystems/SharedBatterySystem.cs index d067a685d4..317dcb129e 100644 --- a/Content.Shared/Power/EntitySystems/SharedBatterySystem.cs +++ b/Content.Shared/Power/EntitySystems/SharedBatterySystem.cs @@ -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 } /// - /// 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. /// /// The actually changed amount. + [PublicAPI] public virtual float ChangeCharge(Entity ent, float amount) { return 0f; } /// - /// 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. /// /// The actually changed amount. + [PublicAPI] public virtual float UseCharge(Entity ent, float amount) { return 0f; @@ -41,9 +46,11 @@ public abstract class SharedBatterySystem : EntitySystem /// /// 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. /// /// If the full amount was able to be removed. + [PublicAPI] public virtual bool TryUseCharge(Entity ent, float amount) { return false; @@ -52,21 +59,25 @@ public abstract class SharedBatterySystem : EntitySystem /// /// Sets the battery's charge. /// + [PublicAPI] public virtual void SetCharge(Entity ent, float value) { } /// /// Sets the battery's maximum charge. /// + [PublicAPI] public virtual void SetMaxCharge(Entity ent, float value) { } /// /// Checks if the entity has a self recharge and puts it on cooldown if applicable. /// Uses the cooldown time given in the component. /// + [PublicAPI] public virtual void TrySetChargeCooldown(Entity ent) { } /// /// Puts the entity's self recharge on cooldown for the specified time. /// + [PublicAPI] public virtual void SetChargeCooldown(Entity ent, TimeSpan cooldown) { } } diff --git a/Content.Shared/Power/EntitySystems/SharedChargerSystem.cs b/Content.Shared/Power/EntitySystems/SharedChargerSystem.cs deleted file mode 100644 index a150436bef..0000000000 --- a/Content.Shared/Power/EntitySystems/SharedChargerSystem.cs +++ /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(OnEmpPulse); - } - - private void OnEmpPulse(EntityUid uid, ChargerComponent component, ref EmpPulseEvent args) - { - args.Affected = true; - args.Disabled = true; - } -} diff --git a/Content.Shared/Power/EntitySystems/SharedPowerReceiverSystem.cs b/Content.Shared/Power/EntitySystems/SharedPowerReceiverSystem.cs index 4a66a6ea97..c3ab0ce26d 100644 --- a/Content.Shared/Power/EntitySystems/SharedPowerReceiverSystem.cs +++ b/Content.Shared/Power/EntitySystems/SharedPowerReceiverSystem.cs @@ -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. } - /// - /// Checks if entity is APC-powered device, and if it have power. + /// + /// Sets the power load of this power receiver. + /// + public void SetLoad(Entity entity, float load) + { + if (!ResolveApc(entity.Owner, ref entity.Comp)) + return; + + entity.Comp.Load = load; + } + + /// + /// Checks if entity is APC-powered device, and if it have power. /// public bool IsPowered(Entity entity) { diff --git a/Content.Shared/Power/SharedPowerItemCharger.cs b/Content.Shared/Power/SharedPowerItemCharger.cs deleted file mode 100644 index ec51a047a2..0000000000 --- a/Content.Shared/Power/SharedPowerItemCharger.cs +++ /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, - } -} diff --git a/Content.Shared/PowerCell/Components/PowerCellComponent.cs b/Content.Shared/PowerCell/Components/PowerCellComponent.cs index 3d4f0472f8..fdada10958 100644 --- a/Content.Shared/PowerCell/Components/PowerCellComponent.cs +++ b/Content.Shared/PowerCell/Components/PowerCellComponent.cs @@ -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; /// -/// 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 . /// -[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 index 0000000000..d09c3ed798 --- /dev/null +++ b/Content.Shared/PowerCell/Components/PowerCellDrawComponent.cs @@ -0,0 +1,35 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.PowerCell.Components; + +/// +/// Indicates that the entity's ActivatableUI requires power or else it closes. +/// +/// +/// With ActivatableUI it will activate and deactivate when the ui is opened and closed, drawing power inbetween. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(PowerCellSystem))] +public sealed partial class PowerCellDrawComponent : Component +{ + /// + /// Whether drawing is enabled. + /// Having no cell will still disable it. + /// + [DataField, AutoNetworkedField, ViewVariables] + public bool Enabled = true; + + /// + /// 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. + /// + [DataField, AutoNetworkedField, ViewVariables] + public float DrawRate = 1f; + + /// + /// 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. + /// + [DataField, AutoNetworkedField] + public float UseCharge; +} diff --git a/Content.Shared/PowerCell/Components/PowerCellSlotComponent.cs b/Content.Shared/PowerCell/Components/PowerCellSlotComponent.cs index 5f21b397a7..b0aad89569 100644 --- a/Content.Shared/PowerCell/Components/PowerCellSlotComponent.cs +++ b/Content.Shared/PowerCell/Components/PowerCellSlotComponent.cs @@ -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 { /// @@ -10,29 +11,17 @@ public sealed partial class PowerCellSlotComponent : Component /// /// /// Given that 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. /// - [DataField("cellSlotId", required: true)] + [DataField(required: true)] public string CellSlotId = string.Empty; /// /// Can this entity be inserted directly into a charging station? If false, you need to manually remove the power /// cell and recharge it separately. /// - [DataField("fitsInCharger")] + [DataField, AutoNetworkedField] public bool FitsInCharger = true; } -/// -/// Raised directed at an entity with a power cell slot when the power cell inside has its charge updated or is ejected/inserted. -/// -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 index cecf23d041..0000000000 --- a/Content.Shared/PowerCell/PowerCellDrawComponent.cs +++ /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; - -/// -/// Indicates that the entity's ActivatableUI requires power or else it closes. -/// -/// -/// With ActivatableUI it will activate and deactivate when the ui is opened and closed, drawing power inbetween. -/// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] -public sealed partial class PowerCellDrawComponent : Component -{ - #region Prediction - - /// - /// Whether there is any charge available to draw. - /// - [DataField, AutoNetworkedField] - public bool CanDraw; - - /// - /// Whether there is sufficient charge to use. - /// - [DataField, AutoNetworkedField] - public bool CanUse; - - #endregion - - /// - /// Whether drawing is enabled. - /// Having no cell will still disable it. - /// - [DataField, AutoNetworkedField] - public bool Enabled = true; - - /// - /// 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. - /// - [DataField] - public float DrawRate = 1f; - - /// - /// 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. - /// - /// - /// This is not a rate how the datafield name implies, but a one-time cost. - /// - [DataField] - public float UseRate; - - /// - /// When the next automatic power draw will occur - /// - [DataField("nextUpdate", customTypeSerializer: typeof(TimeOffsetSerializer))] - [AutoPausedField] - public TimeSpan NextUpdateTime; - - /// - /// How long to wait between power drawing. - /// - [DataField] - public TimeSpan Delay = TimeSpan.FromSeconds(1); -} diff --git a/Content.Shared/PowerCell/PowerCellSlotEmptyEvent.cs b/Content.Shared/PowerCell/PowerCellEvents.cs similarity index 53% rename from Content.Shared/PowerCell/PowerCellSlotEmptyEvent.cs rename to Content.Shared/PowerCell/PowerCellEvents.cs index e4075175ae..0d3af9b969 100644 --- a/Content.Shared/PowerCell/PowerCellSlotEmptyEvent.cs +++ b/Content.Shared/PowerCell/PowerCellEvents.cs @@ -5,3 +5,9 @@ namespace Content.Shared.PowerCell; /// [ByRefEvent] public readonly record struct PowerCellSlotEmptyEvent; + +/// +/// Raised directed at an entity with a power cell slot when a power cell is ejected/inserted. +/// +[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 index 0000000000..98b24edcce --- /dev/null +++ b/Content.Shared/PowerCell/PowerCellSystem.API.cs @@ -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 +{ + /// + /// Gets the power cell battery inside a power cell slot. + /// + [PublicAPI] + public bool TryGetBatteryFromSlot( + Entity ent, + [NotNullWhen(true)] out Entity? 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(slot.Item, out var batteryComp)) + { + battery = null; + return false; + } + + battery = (slot.Item.Value, batteryComp); + return true; + } + + /// + /// Returns whether the entity has a slotted battery and charge for the requested action. + /// + /// The power cell. + /// The charge that is needed. + /// Show a popup to this user with the relevant details if specified. + /// Whether to predict the popup or not. + [PublicAPI] + public bool HasCharge(Entity 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; + } + + /// + /// Tries to use charge from a slotted battery. + /// + /// The power cell. + /// The charge that is needed. + /// Show a popup to this user with the relevant details if specified. + /// Whether to predict the popup or not. + [PublicAPI] + public bool TryUseCharge(Entity 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; + } + + /// + /// Gets number of remaining uses for the given charge cost. + /// + /// The power cell. + /// The cost per use. + [PublicAPI] + public int GetRemainingUses(Entity ent, float cost) + { + if (!TryGetBatteryFromSlot(ent, out var battery)) + return 0; + + return _battery.GetRemainingUses(battery.Value.AsNullable(), cost); + } + + /// + /// Gets number of maximum uses at full charge for the given charge cost. + /// + /// The power cell. + /// The cost per use. + [PublicAPI] + public int GetMaxUses(Entity 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 index 0000000000..8790ec941c --- /dev/null +++ b/Content.Shared/PowerCell/PowerCellSystem.Draw.cs @@ -0,0 +1,76 @@ +using Content.Shared.PowerCell.Components; +using JetBrains.Annotations; + +namespace Content.Shared.PowerCell; + +public sealed partial class PowerCellSystem +{ + /// + /// Enables or disables the power cell draw. + /// + [PublicAPI] + public void SetDrawEnabled(Entity 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()); + } + + + /// + /// Returns whether the entity has a slotted battery and charge. + /// + /// The device with the power cell slot. + /// Show a popup to this user with the relevant details if specified. + /// Whether to predict the popup or not. + [PublicAPI] + public bool HasActivatableCharge(Entity 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); + } + + /// + /// Tries to use the for this entity. + /// + /// The device with the power cell slot. + /// Show a popup to this user with the relevant details if specified. + /// Whether to predict the popup or not. + [PublicAPI] + public bool TryUseActivatableCharge(Entity 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; + } + + /// + /// Whether the power cell has any power at all for the draw rate. + /// + /// The device with the power cell slot. + /// Show a popup to this user with the relevant details if specified. + /// Whether to predict the popup or not. + [PublicAPI] + public bool HasDrawCharge(Entity 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 index 0000000000..ca4bc2a661 --- /dev/null +++ b/Content.Shared/PowerCell/PowerCellSystem.Relay.cs @@ -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(RelayToCell); + SubscribeLocalEvent(RelayToCell); + SubscribeLocalEvent(RelayToCell); + SubscribeLocalEvent(RelayToCell); + + SubscribeLocalEvent(RelayToCellSlot); // Prevent the ninja from EMPing its own battery + SubscribeLocalEvent(RelayToCellSlot); + SubscribeLocalEvent(RelayToCellSlot); // For shutting down devices if the battery is empty + SubscribeLocalEvent(RelayToCellSlot); // Allow devices to charge/drain inserted batteries + } + + private void RelayToCell(Entity 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(Entity ent, ref T args) where T : notnull + { + var parent = Transform(ent).ParentUid; + // Relay the event to the slot entity. + if (HasComp(parent)) + RaiseLocalEvent(parent, ref args); + } +} diff --git a/Content.Shared/PowerCell/PowerCellSystem.cs b/Content.Shared/PowerCell/PowerCellSystem.cs new file mode 100644 index 0000000000..dfc2dfdbe2 --- /dev/null +++ b/Content.Shared/PowerCell/PowerCellSystem.cs @@ -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(OnCellSlotInsertAttempt); + SubscribeLocalEvent(OnCellSlotInserted); + SubscribeLocalEvent(OnCellSlotRemoved); + SubscribeLocalEvent(OnCellSlotExamined); + SubscribeLocalEvent(OnCellSlotStateChanged); + + SubscribeLocalEvent(OnCellExamined); + + SubscribeLocalEvent(OnDrawRefreshChargeRate); + SubscribeLocalEvent(OnDrawStartup); + SubscribeLocalEvent(OnDrawRemove); + + } + + private void OnCellSlotInsertAttempt(Entity 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(args.EntityUid)) + args.Cancel(); + } + + private void OnCellSlotInserted(Entity 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(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 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(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 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 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 ent, ref ExaminedEvent args) + { + if (TryComp(ent, out var battery)) + OnBatteryExamined((ent.Owner, battery), ref args); + } + + private void OnBatteryExamined(Entity 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 ent, ref RefreshChargeRateEvent args) + { + if (ent.Comp.Enabled) + args.NewChargeRate -= ent.Comp.DrawRate; + } + + private void OnDrawStartup(Entity ent, ref ComponentStartup args) + { + if (ent.Comp.Enabled) + _battery.RefreshChargeRate(ent.Owner); + } + + private void OnDrawRemove(Entity 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 index a32687c15c..0000000000 --- a/Content.Shared/PowerCell/SharedPowerCellSystem.cs +++ /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(OnMapInit); - - SubscribeLocalEvent(OnRejuvenate); - SubscribeLocalEvent(OnCellInserted); - SubscribeLocalEvent(OnCellRemoved); - SubscribeLocalEvent(OnCellInsertAttempt); - - SubscribeLocalEvent(OnCellEmpAttempt); - } - - private void OnMapInit(Entity 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(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 entity, ref EmpAttemptEvent args) - { - var parent = Transform(entity).ParentUid; - // relay the attempt event to the slot so it can cancel it - if (HasComp(parent)) - RaiseLocalEvent(parent, ref args); - } - - public void SetDrawEnabled(Entity 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); - } - - /// - /// Returns whether the entity has a slotted battery and charge. - /// - /// - /// - /// - /// Popup to this user with the relevant detail if specified. - public abstract bool HasActivatableCharge( - EntityUid uid, - PowerCellDrawComponent? battery = null, - PowerCellSlotComponent? cell = null, - EntityUid? user = null); - - /// - /// Whether the power cell has any power at all for the draw rate. - /// - public abstract bool HasDrawCharge( - EntityUid uid, - PowerCellDrawComponent? battery = null, - PowerCellSlotComponent? cell = null, - EntityUid? user = null); -} diff --git a/Content.Shared/PowerCell/ToggleCellDrawSystem.cs b/Content.Shared/PowerCell/ToggleCellDrawSystem.cs index 14d91d2f5f..9c50a8aa60 100644 --- a/Content.Shared/PowerCell/ToggleCellDrawSystem.cs +++ b/Content.Shared/PowerCell/ToggleCellDrawSystem.cs @@ -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 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; } diff --git a/Content.Shared/Silicons/Borgs/Components/BorgChassisComponent.cs b/Content.Shared/Silicons/Borgs/Components/BorgChassisComponent.cs index f562ddefdd..98a5b205e9 100644 --- a/Content.Shared/Silicons/Borgs/Components/BorgChassisComponent.cs +++ b/Content.Shared/Silicons/Borgs/Components/BorgChassisComponent.cs @@ -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. /// -[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 NoBatteryAlert = "BorgBatteryNone"; + /// + /// The next update time for the battery charge level. + /// Used for the alert and borg UI. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoPausedField] + public TimeSpan NextBatteryUpdate = TimeSpan.Zero; + /// /// If the entity can open own UI. /// diff --git a/Content.Shared/Storage/Components/EntityStorageComponent.cs b/Content.Shared/Storage/Components/EntityStorageComponent.cs index 628cc85252..009083b2d8 100644 --- a/Content.Shared/Storage/Components/EntityStorageComponent.cs +++ b/Content.Shared/Storage/Components/EntityStorageComponent.cs @@ -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. /// [ByRefEvent] -public record struct InsertIntoEntityStorageAttemptEvent(EntityUid ItemToInsert, bool Cancelled = false); +public record struct InsertIntoEntityStorageAttemptEvent(BaseContainer Container, EntityUid ItemToInsert, bool Cancelled = false); /// /// Raised on the entity storage whenever checking if an entity can be inserted into it. /// [ByRefEvent] -public record struct EntityStorageInsertedIntoAttemptEvent(EntityUid ItemToInsert, bool Cancelled = false); +public record struct EntityStorageInsertedIntoAttemptEvent(BaseContainer Container, EntityUid ItemToInsert, bool Cancelled = false); /// /// Raised on the Container's owner whenever an entity storage tries to dump its diff --git a/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs b/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs index 066cd2d886..dd6f1a223e 100644 --- a/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs +++ b/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs @@ -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) diff --git a/Content.Shared/UserInterface/ActivatableUIRequiresPowerCellComponent.cs b/Content.Shared/UserInterface/ActivatableUIRequiresPowerCellComponent.cs index aa9e561e07..39b9a01955 100644 --- a/Content.Shared/UserInterface/ActivatableUIRequiresPowerCellComponent.cs +++ b/Content.Shared/UserInterface/ActivatableUIRequiresPowerCellComponent.cs @@ -1,13 +1,10 @@ -using Content.Shared.PowerCell; +using Content.Shared.PowerCell.Components; using Robust.Shared.GameStates; namespace Content.Shared.UserInterface; /// -/// Specifies that the attached entity requires power. +/// Specifies that the attached entity requires power to open the activatable UI. /// [RegisterComponent, NetworkedComponent] -public sealed partial class ActivatableUIRequiresPowerCellComponent : Component -{ - -} +public sealed partial class ActivatableUIRequiresPowerCellComponent : Component; diff --git a/Content.Shared/UserInterface/ActivatableUISystem.Power.cs b/Content.Shared/UserInterface/ActivatableUISystem.Power.cs index e494253c83..3e52e3ed36 100644 --- a/Content.Shared/UserInterface/ActivatableUISystem.Power.cs +++ b/Content.Shared/UserInterface/ActivatableUISystem.Power.cs @@ -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(OnBatteryOpenAttempt); + SubscribeLocalEvent(OnToggled); SubscribeLocalEvent(OnBatteryOpened); SubscribeLocalEvent(OnBatteryClosed); - SubscribeLocalEvent(OnToggled); + SubscribeLocalEvent(OnBatteryStateChanged); + SubscribeLocalEvent(OnBatteryOpenAttempt); } private void OnToggled(Entity ent, ref ItemToggledEvent args) { // only close ui when losing power - if (!TryComp(ent, out var activatable) || args.Activated) + if (args.Activated || !TryComp(ent, out var activatable)) return; if (activatable.Key == null) @@ -55,35 +57,25 @@ public sealed partial class ActivatableUISystem _toggle.TryDeactivate(uid); } - /// - /// Call if you want to check if the UI should close due to a recent battery usage. - /// - public void CheckUsage(EntityUid uid, ActivatableUIComponent? active = null, ActivatableUIRequiresPowerCellComponent? component = null, PowerCellDrawComponent? draw = null) + private void OnBatteryStateChanged(Entity 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(ent); + if (activatable.Key != null) + _uiSystem.CloseUi(ent.Owner, activatable.Key); } private void OnBatteryOpenAttempt(EntityUid uid, ActivatableUIRequiresPowerCellComponent component, ActivatableUIOpenAttemptEvent args) { - if (!TryComp(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(); } diff --git a/Content.Shared/Weapons/Ranged/Components/BatteryAmmoProviderComponent.cs b/Content.Shared/Weapons/Ranged/Components/BatteryAmmoProviderComponent.cs index 605e169c38..3378d82b44 100644 --- a/Content.Shared/Weapons/Ranged/Components/BatteryAmmoProviderComponent.cs +++ b/Content.Shared/Weapons/Ranged/Components/BatteryAmmoProviderComponent.cs @@ -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 +/// +/// Ammo provider that uses electric charge from a battery to provide ammunition to a weapon. +/// This works with both and +/// +[RegisterComponent, NetworkedComponent] +[AutoGenerateComponentState(raiseAfterAutoHandleState: true), AutoGenerateComponentPause] +public sealed partial class BatteryAmmoProviderComponent : AmmoProviderComponent { /// - /// How much battery it costs to fire once. + /// The projectile or hitscan entity to spawn when firing. + /// + [DataField("proto", required: true)] + public EntProtoId Prototype; + + /// + /// How much charge it costs to fire once, in watts. /// - [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! + /// + /// 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. + /// + /// + /// Not a datafield since this is refreshed along with the battery's charge rate anyways. + /// + [ViewVariables, AutoNetworkedField, AutoPausedField] + public TimeSpan? NextUpdate; - [ViewVariables(VVAccess.ReadWrite)] + /// + /// The time between reaching full charges at the current charge rate. + /// Only used for predicted batteries. + /// + /// + /// Not a datafield since this is refreshed along with the battery's charge rate anyways. + /// + [ViewVariables, AutoNetworkedField] + public TimeSpan ChargeTime = TimeSpan.Zero; + + /// + /// The current amount of available shots. + /// BatteryComponent is not predicted, so we need to manually network this for the ammo indicator and examination text. + /// + /// + /// 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. + /// + [ViewVariables, AutoNetworkedField] public int Shots; - [ViewVariables(VVAccess.ReadWrite)] + /// + /// The maximum amount of available shots. + /// BatteryComponent is not predicted, so we need to manually network this for the ammo indicator and examination text. + /// + /// + /// 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. + /// + [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 index cdbf51456e..0000000000 --- a/Content.Shared/Weapons/Ranged/Components/HitscanBatteryAmmoProviderComponent.cs +++ /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 index d0de436396..0000000000 --- a/Content.Shared/Weapons/Ranged/Components/ProjectileBatteryAmmoProviderComponent.cs +++ /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))] - 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 index 57f731889a..0000000000 --- a/Content.Shared/Weapons/Ranged/Events/UpdateClientAmmoEvent.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Content.Shared.Weapons.Ranged.Events; - -[ByRefEvent] -public readonly record struct UpdateClientAmmoEvent(); \ No newline at end of file diff --git a/Content.Shared/Weapons/Ranged/Systems/BatteryWeaponFireModesSystem.cs b/Content.Shared/Weapons/Ranged/Systems/BatteryWeaponFireModesSystem.cs index 974bfa1783..81f4ad3213 100644 --- a/Content.Shared/Weapons/Ranged/Systems/BatteryWeaponFireModesSystem.cs +++ b/Content.Shared/Weapons/Ranged/Systems/BatteryWeaponFireModesSystem.cs @@ -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)); } } } diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Battery.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Battery.cs index 663f5f1faa..24cfe35051 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Battery.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Battery.cs @@ -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(OnBatteryGetState); - SubscribeLocalEvent(OnBatteryHandleState); - SubscribeLocalEvent(OnBatteryTakeAmmo); - SubscribeLocalEvent(OnBatteryAmmoCount); - SubscribeLocalEvent(OnBatteryExamine); - SubscribeLocalEvent(OnBatteryDamageExamine); - - // Projectile - SubscribeLocalEvent(OnBatteryGetState); - SubscribeLocalEvent(OnBatteryHandleState); - SubscribeLocalEvent(OnBatteryTakeAmmo); - SubscribeLocalEvent(OnBatteryAmmoCount); - SubscribeLocalEvent(OnBatteryExamine); - SubscribeLocalEvent(OnBatteryDamageExamine); + SubscribeLocalEvent(OnBatteryStartup); + SubscribeLocalEvent(OnAfterAutoHandleState); + SubscribeLocalEvent(OnBatteryTakeAmmo); + SubscribeLocalEvent(OnBatteryAmmoCount); + SubscribeLocalEvent(OnBatteryExamine); + SubscribeLocalEvent(OnBatteryDamageExamine); + SubscribeLocalEvent(OnPowerCellChanged); + SubscribeLocalEvent(OnPredictedChargeChanged); + SubscribeLocalEvent(OnChargeChanged); } - private void OnBatteryHandleState(EntityUid uid, BatteryAmmoProviderComponent component, ref ComponentHandleState args) + private void OnBatteryExamine(Entity 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 ent, ref DamageExamineEvent args) { - args.State = new BatteryAmmoProviderComponentState() + var proto = ProtoManager.Index(ent.Comp.Prototype); + DamageSpecifier? damageSpec = null; + var damageType = string.Empty; + + if (proto.TryGetComponent(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(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(Entity entity, ref DamageExamineEvent args) where T : BatteryAmmoProviderComponent + private void OnBatteryTakeAmmo(Entity 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 ent, ref GetAmmoCountEvent args) { - if (component is ProjectileBatteryAmmoProviderComponent battery) - { - if (ProtoManager.Index(battery.Prototype).Components - .TryGetValue(Factory.GetComponentName(), 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(out var basicDamageComp, Factory)) - return null; + /// + /// Use up the required amount of battery charge for firing. + /// + public void TakeCharge(Entity 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 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(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 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. } - /// - /// Update the battery (server-only) whenever fired. - /// - protected virtual void TakeCharge(Entity entity) + // For when a power cell gets inserted or removed. + private void OnPowerCellChanged(Entity 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 ent, ref PredictedBatteryChargeChangedEvent args) { - if (!TryComp(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 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 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 ent, ref ComponentStartup args) + { + UpdateShots(ent); + } + + /// + /// Gets the current and maximum amount of shots from this entity's battery. + /// This works for BatteryComponent, PredictedBatteryComponent and PowercellSlotComponent. + /// + public (int, int) GetShots(Entity 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 + /// + /// 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. + /// + private void UpdateBattery(float frameTime) { - public int Shots; - public int MaxShots; - public float FireCost; + var curTime = Timing.CurTime; + var hitscanQuery = EntityQueryEnumerator(); + 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. + } } } diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs index ed8849a41a..e9170d45df 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs @@ -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); + } } /// diff --git a/Resources/Maps/Shuttles/emergency_meta.yml b/Resources/Maps/Shuttles/emergency_meta.yml index faf82ea997..83cdab338e 100644 --- a/Resources/Maps/Shuttles/emergency_meta.yml +++ b/Resources/Maps/Shuttles/emergency_meta.yml @@ -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 diff --git a/Resources/Prototypes/Catalog/Bounties/bounties.yml b/Resources/Prototypes/Catalog/Bounties/bounties.yml index 60fa5147b8..9ec49d6299 100644 --- a/Resources/Prototypes/Catalog/Bounties/bounties.yml +++ b/Resources/Prototypes/Catalog/Bounties/bounties.yml @@ -530,7 +530,7 @@ amount: 6 whitelist: components: - - HitscanBatteryAmmoProvider + - BatteryAmmoProvider blacklist: components: - PowerCell diff --git a/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml b/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml index 05dda23f0a..5dfc94a1f7 100644 --- a/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml +++ b/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml @@ -619,7 +619,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: GuideHelp diff --git a/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml b/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml index 3b85ce94c9..2d320c868c 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml @@ -243,10 +243,10 @@ 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 diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml index 341404af2b..1c23d18a97 100644 --- a/Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml +++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml @@ -296,10 +296,10 @@ 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 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml b/Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml index 65cc71aaa8..1cb00ef760 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml @@ -19,13 +19,13 @@ - 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 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/lavaland.yml b/Resources/Prototypes/Entities/Mobs/NPCs/lavaland.yml index 949fc76efe..4a205c9cb0 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/lavaland.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/lavaland.yml @@ -50,12 +50,12 @@ - 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 @@ -125,7 +125,7 @@ radius: 1 energy: 3 color: orangered - - type: ProjectileBatteryAmmoProvider + - type: BatteryAmmoProvider proto: WatcherBoltMagmawing fireCost: 50 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/living_light.yml b/Resources/Prototypes/Entities/Mobs/NPCs/living_light.yml index 1531e348f7..13eeb2b372 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/living_light.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/living_light.yml @@ -168,13 +168,13 @@ 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 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/miscellaneous.yml b/Resources/Prototypes/Entities/Mobs/NPCs/miscellaneous.yml index 173c5ad165..00aa20db18 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/miscellaneous.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/miscellaneous.yml @@ -47,12 +47,12 @@ - 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 diff --git a/Resources/Prototypes/Entities/Objects/Devices/base_handheld.yml b/Resources/Prototypes/Entities/Objects/Devices/base_handheld.yml index c377519ddb..e131e7b732 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/base_handheld.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/base_handheld.yml @@ -8,5 +8,5 @@ onUse: false # above component does the toggling - type: PowerCellDraw drawRate: 0 - useRate: 20 + useCharge: 20 - type: ToggleCellDraw diff --git a/Resources/Prototypes/Entities/Objects/Fun/spectral_locator.yml b/Resources/Prototypes/Entities/Objects/Fun/spectral_locator.yml index 194fd4d233..329d278bc8 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/spectral_locator.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/spectral_locator.yml @@ -43,7 +43,7 @@ components: - type: PowerCellDraw drawRate: 1 - useRate: 0 + useCharge: 0 - type: ToggleCellDraw - type: entity diff --git a/Resources/Prototypes/Entities/Objects/Power/portable_recharger.yml b/Resources/Prototypes/Entities/Objects/Power/portable_recharger.yml index 3c553fc812..2a29951f1f 100644 --- a/Resources/Prototypes/Entities/Objects/Power/portable_recharger.yml +++ b/Resources/Prototypes/Entities/Objects/Power/portable_recharger.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Objects/Power/powercells.yml b/Resources/Prototypes/Entities/Objects/Power/powercells.yml index 4ce96419bf..a896e3a6e2 100644 --- a/Resources/Prototypes/Entities/Objects/Power/powercells.yml +++ b/Resources/Prototypes/Entities/Objects/Power/powercells.yml @@ -5,7 +5,7 @@ components: - type: Item storedRotation: -90 - - type: Battery + - type: PredictedBattery pricePerJoule: 0.15 - type: PowerCell - type: Explosive @@ -30,9 +30,16 @@ - 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 @@ -97,7 +104,7 @@ 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 @@ -115,7 +122,7 @@ - map: [ "enum.PowerCellVisualLayers.Unshaded" ] state: o2 shader: unshaded - - type: Battery + - type: PredictedBattery maxCharge: 720 startingCharge: 720 @@ -132,7 +139,7 @@ state: o2 shader: unshaded visible: false - - type: Battery + - type: PredictedBattery maxCharge: 720 startingCharge: 0 @@ -150,7 +157,7 @@ - map: [ "enum.PowerCellVisualLayers.Unshaded" ] state: o2 shader: unshaded - - type: Battery + - type: PredictedBattery maxCharge: 1080 startingCharge: 1080 @@ -167,7 +174,7 @@ state: o2 shader: unshaded visible: false - - type: Battery + - type: PredictedBattery maxCharge: 1080 startingCharge: 0 @@ -185,7 +192,7 @@ - map: [ "enum.PowerCellVisualLayers.Unshaded" ] state: o2 shader: unshaded - - type: Battery + - type: PredictedBattery maxCharge: 1800 startingCharge: 1800 @@ -202,7 +209,7 @@ state: o2 shader: unshaded visible: false - - type: Battery + - type: PredictedBattery maxCharge: 1800 startingCharge: 0 @@ -220,10 +227,10 @@ - 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 @@ -239,7 +246,7 @@ state: o2 shader: unshaded visible: false - - type: Battery + - type: PredictedBattery startingCharge: 0 - type: entity @@ -255,10 +262,10 @@ - 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) @@ -302,7 +309,7 @@ - map: [ "enum.PowerCellVisualLayers.Unshaded" ] state: o2 shader: unshaded - - type: Battery + - type: PredictedBattery maxCharge: 1400 startingCharge: 1400 @@ -320,7 +327,7 @@ - map: [ "enum.PowerCellVisualLayers.Unshaded" ] state: o2 shader: unshaded - - type: Battery + - type: PredictedBattery maxCharge: 2700 startingCharge: 2700 @@ -338,7 +345,7 @@ - map: [ "enum.PowerCellVisualLayers.Unshaded" ] state: o2 shader: unshaded - - type: Battery + - type: PredictedBattery maxCharge: 6200 startingCharge: 6200 @@ -356,7 +363,7 @@ state: o2 shader: unshaded visible: false - - type: Battery + - type: PredictedBattery maxCharge: 1400 startingCharge: 0 @@ -374,7 +381,7 @@ state: o2 shader: unshaded visible: false - - type: Battery + - type: PredictedBattery startingCharge: 0 - type: entity @@ -391,5 +398,5 @@ state: o2 shader: unshaded visible: false - - type: Battery + - type: PredictedBattery startingCharge: 0 diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/defib.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/defib.yml index 894454b05c..86372c1769 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Medical/defib.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/defib.yml @@ -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: diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml index 0f2259f7e9..9fe4d377d7 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml @@ -34,10 +34,11 @@ - 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 @@ -61,3 +63,10 @@ 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" ] diff --git a/Resources/Prototypes/Entities/Objects/Specific/Research/anomaly.yml b/Resources/Prototypes/Entities/Objects/Specific/Research/anomaly.yml index e9b365de9a..b2eae73ddf 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Research/anomaly.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Research/anomaly.yml @@ -114,7 +114,7 @@ components: - type: PowerCellDraw drawRate: 1 - useRate: 0 + useCharge: 0 - type: ToggleCellDraw - type: entity @@ -149,7 +149,7 @@ components: - type: PowerCellDraw drawRate: 1 - useRate: 0 + useCharge: 0 - type: ToggleCellDraw - type: entity diff --git a/Resources/Prototypes/Entities/Objects/Specific/Salvage/scanner.yml b/Resources/Prototypes/Entities/Objects/Specific/Salvage/scanner.yml index 7c2d76b3b8..6081e2e277 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Salvage/scanner.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Salvage/scanner.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Objects/Tools/handheld_mass_scanner.yml b/Resources/Prototypes/Entities/Objects/Tools/handheld_mass_scanner.yml index 4dc2e65b77..086901b145 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/handheld_mass_scanner.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/handheld_mass_scanner.yml @@ -19,12 +19,14 @@ 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 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml index 10181b54e7..5134b2d711 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml @@ -20,7 +20,7 @@ - SemiAuto soundGunshot: path: /Audio/Weapons/Guns/Gunshots/laser.ogg - - type: Battery + - type: PredictedBattery maxCharge: 1000 startingCharge: 1000 - type: StaticPrice @@ -52,11 +52,14 @@ 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 @@ -157,6 +160,10 @@ magState: mag steps: 5 zeroVisible: true + - type: Tag + tags: + - Sidearm + - LaserWeapon - type: entity name: retro laser blaster @@ -174,7 +181,7 @@ shader: unshaded - type: Item storedOffset: 0,-5 - - type: HitscanBatteryAmmoProvider + - type: BatteryAmmoProvider proto: RedMediumLaser fireCost: 62.5 - type: MagazineVisuals @@ -182,6 +189,10 @@ steps: 5 zeroVisible: true - type: Appearance + - type: Tag + tags: + - Sidearm + - LaserWeapon - type: entity name: makeshift laser pistol @@ -206,12 +217,16 @@ - 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 @@ -237,7 +252,7 @@ path: /Audio/Effects/Lightning/lightningshock.ogg params: variation: 0.2 - - type: ProjectileBatteryAmmoProvider + - type: BatteryAmmoProvider proto: TeslaGunBullet fireCost: 300 - type: MagazineVisuals @@ -260,7 +275,7 @@ components: - type: StaticPrice price: 300 - - type: HitscanBatteryAmmoProvider + - type: BatteryAmmoProvider proto: RedLaserPractice fireCost: 62.5 - type: PacifismAllowedGun @@ -294,12 +309,16 @@ - 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 @@ -332,10 +351,10 @@ - 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 @@ -368,10 +387,10 @@ 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 @@ -404,7 +423,7 @@ fireRate: 1.5 soundGunshot: path: /Audio/Weapons/Guns/Gunshots/laser_cannon.ogg - - type: HitscanBatteryAmmoProvider + - type: BatteryAmmoProvider proto: RedHeavyLaser fireCost: 100 - type: Tag @@ -435,10 +454,10 @@ path: /Audio/Weapons/emitter.ogg params: pitch: 2 - - type: ProjectileBatteryAmmoProvider + - type: BatteryAmmoProvider proto: AntiParticlesProjectile fireCost: 500 - - type: Battery + - type: PredictedBattery maxCharge: 10000 startingCharge: 10000 @@ -465,7 +484,7 @@ - type: Gun soundGunshot: path: /Audio/Weapons/Guns/Gunshots/laser3.ogg - - type: HitscanBatteryAmmoProvider + - type: BatteryAmmoProvider proto: XrayLaser fireCost: 100 - type: MagazineVisuals @@ -507,7 +526,7 @@ 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 @@ -536,7 +555,7 @@ slots: - suitStorage - Belt - - type: ProjectileBatteryAmmoProvider + - type: BatteryAmmoProvider proto: BulletDisabler fireCost: 62.5 - type: GuideHelp @@ -574,7 +593,7 @@ - FullAuto soundGunshot: path: /Audio/Weapons/Guns/Gunshots/taser2.ogg - - type: ProjectileBatteryAmmoProvider + - type: BatteryAmmoProvider proto: BulletDisablerSmg fireCost: 25 - type: MagazineVisuals @@ -614,7 +633,7 @@ fireRate: 0.5 soundGunshot: path: /Audio/Weapons/Guns/Gunshots/taser.ogg - - type: ProjectileBatteryAmmoProvider + - type: BatteryAmmoProvider proto: BulletTaser fireCost: 200 - type: MagazineVisuals @@ -637,7 +656,7 @@ path: /Audio/Effects/tesla_collapse.ogg # The wrath of god... params: volume: -6 - - type: ProjectileBatteryAmmoProvider + - type: BatteryAmmoProvider proto: BulletTaserSuper fireCost: 200 @@ -660,10 +679,10 @@ - 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 @@ -675,6 +694,7 @@ - HighRiskItem - Sidearm - WeaponAntiqueLaser + - LaserWeapon - type: StaticPrice price: 750 - type: StealTarget @@ -707,10 +727,10 @@ - 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 @@ -719,6 +739,10 @@ - type: Appearance - type: StaticPrice price: 63 + - type: Tag + tags: + - Sidearm + - LaserWeapon - type: entity name: C.H.I.M.P. handcannon @@ -749,7 +773,7 @@ fireRate: 1.5 soundGunshot: path: /Audio/Weapons/Guns/Gunshots/taser2.ogg - - type: ProjectileBatteryAmmoProvider + - type: BatteryAmmoProvider proto: AnomalousParticleDeltaStrong fireCost: 100 - type: BatteryWeaponFireModes @@ -802,13 +826,17 @@ 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 @@ -835,7 +863,7 @@ fireRate: 2 soundGunshot: path: /Audio/Weapons/Guns/Gunshots/laser_cannon.ogg - - type: ProjectileBatteryAmmoProvider + - type: BatteryAmmoProvider proto: BulletLaserSpreadNarrow fireCost: 80 - type: BatteryWeaponFireModes @@ -850,7 +878,7 @@ 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 @@ -885,7 +913,7 @@ - type: Gun soundGunshot: path: /Audio/Weapons/Guns/Gunshots/laser_cannon.ogg - - type: ProjectileBatteryAmmoProvider + - type: BatteryAmmoProvider proto: BulletLaserMagnum fireCost: 150 - type: BatteryWeaponFireModes @@ -897,7 +925,7 @@ - proto: BulletDisabler fireCost: 62.5 pacifismAllowedMode: true - - type: BatterySelfRecharger + - type: PredictedBatterySelfRecharger autoRechargeRate: 48 autoRechargePauseTime: 10 @@ -930,7 +958,7 @@ fireRate: 1 soundGunshot: path: /Audio/Weapons/Guns/Gunshots/taser2.ogg - - type: ProjectileBatteryAmmoProvider + - type: BatteryAmmoProvider proto: BoltTempgunCold fireCost: 100 - type: BatteryWeaponFireModes @@ -939,7 +967,7 @@ fireCost: 100 - proto: BoltTempgunHot fireCost: 100 - - type: Battery + - type: PredictedBattery maxCharge: 1000 startingCharge: 1000 - type: StaticPrice @@ -955,5 +983,5 @@ parent: [WeaponAdvancedLaser, BaseXenoborgContraband] id: XenoborgHeavyLaserGun components: - - type: HitscanBatteryAmmoProvider + - type: BatteryAmmoProvider proto: RedHeavyLaser diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/LMGs/lmgs.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/LMGs/lmgs.yml index be57d5f0f9..acf29c9e1a 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/LMGs/lmgs.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/LMGs/lmgs.yml @@ -117,12 +117,12 @@ - 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 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml index ca40d1940d..1a1095d981 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml @@ -128,13 +128,13 @@ - 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 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml index 4ca2e0c0f5..e567d06c03 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml @@ -153,13 +153,13 @@ - 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 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_base.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_base.yml index f60297d223..dc4be40c82 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_base.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_base.yml @@ -120,7 +120,7 @@ fireRate: 1.5 soundGunshot: path: /Audio/Weapons/Guns/Gunshots/taser2.ogg - - type: ProjectileBatteryAmmoProvider + - type: BatteryAmmoProvider proto: BulletEnergyTurretLaser fireCost: 100 - type: Battery @@ -137,4 +137,4 @@ rootTask: task: EnergyTurretCompound - type: StaticPrice - price: 200 \ No newline at end of file + price: 200 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_energy.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_energy.yml index 33d5fd6e97..01a0569ad5 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_energy.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_energy.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml index 6b8c4eebe1..26520c2fd5 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml @@ -125,13 +125,13 @@ 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 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/stunprod.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/stunprod.yml index 295a782190..27857e24a1 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/stunprod.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/stunprod.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/security.yml b/Resources/Prototypes/Entities/Objects/Weapons/security.yml index 2f6ac834ba..217286a5c5 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/security.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/security.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Structures/Power/chargers.yml b/Resources/Prototypes/Entities/Structures/Power/chargers.yml index 0bbe95efb5..907c2ddbd4 100644 --- a/Resources/Prototypes/Entities/Structures/Power/chargers.yml +++ b/Resources/Prototypes/Entities/Structures/Power/chargers.yml @@ -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 @@ -167,8 +169,7 @@ ejectOnInteract: true whitelist: components: - - HitscanBatteryAmmoProvider - - ProjectileBatteryAmmoProvider + - BatteryAmmoProvider - Stunbaton - PowerCell blacklist: @@ -193,8 +194,7 @@ ejectOnInteract: true whitelist: components: - - HitscanBatteryAmmoProvider - - ProjectileBatteryAmmoProvider + - BatteryAmmoProvider - Stunbaton - PowerCell blacklist: @@ -223,8 +223,7 @@ ejectOnInteract: true whitelist: components: - - HitscanBatteryAmmoProvider - - ProjectileBatteryAmmoProvider + - BatteryAmmoProvider - Stunbaton blacklist: tags: diff --git a/Resources/Prototypes/Entities/Structures/Shuttles/cannons.yml b/Resources/Prototypes/Entities/Structures/Shuttles/cannons.yml index 92e86f3223..516b90ed27 100644 --- a/Resources/Prototypes/Entities/Structures/Shuttles/cannons.yml +++ b/Resources/Prototypes/Entities/Structures/Shuttles/cannons.yml @@ -92,7 +92,7 @@ tags: - PowerCell - PowerCellSmall - - type: HitscanBatteryAmmoProvider + - type: BatteryAmmoProvider proto: RedLightLaser fireCost: 50 @@ -148,7 +148,7 @@ whitelist: tags: - PowerCage - - type: HitscanBatteryAmmoProvider + - type: BatteryAmmoProvider proto: RedShuttleLaser fireCost: 150 diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml index 274d9447b4..34dfe443ad 100644 --- a/Resources/Prototypes/tags.yml +++ b/Resources/Prototypes/tags.yml @@ -839,6 +839,9 @@ ## 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. -- 2.52.0