+++ /dev/null
-using Content.Shared.Power.EntitySystems;
-
-namespace Content.Client.Power.EntitySystems;
-
-public sealed class ChargerSystem : SharedChargerSystem;
+++ /dev/null
-using Content.Shared.PowerCell;
-using Content.Shared.PowerCell.Components;
-using JetBrains.Annotations;
-using Robust.Client.GameObjects;
-
-namespace Content.Client.PowerCell;
-
-[UsedImplicitly]
-public sealed class PowerCellSystem : SharedPowerCellSystem
-{
- [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
- [Dependency] private readonly SpriteSystem _sprite = default!;
-
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent<PowerCellVisualsComponent, AppearanceChangeEvent>(OnPowerCellVisualsChange);
- }
-
- /// <inheritdoc/>
- public override bool HasActivatableCharge(EntityUid uid, PowerCellDrawComponent? battery = null, PowerCellSlotComponent? cell = null,
- EntityUid? user = null)
- {
- if (!Resolve(uid, ref battery, ref cell, false))
- return true;
-
- return battery.CanUse;
- }
-
- /// <inheritdoc/>
- public override bool HasDrawCharge(
- EntityUid uid,
- PowerCellDrawComponent? battery = null,
- PowerCellSlotComponent? cell = null,
- EntityUid? user = null)
- {
- if (!Resolve(uid, ref battery, ref cell, false))
- return true;
-
- return battery.CanDraw;
- }
-
- private void OnPowerCellVisualsChange(EntityUid uid, PowerCellVisualsComponent component, ref AppearanceChangeEvent args)
- {
- if (args.Sprite == null)
- return;
-
- if (!_sprite.LayerExists((uid, args.Sprite), PowerCellVisualLayers.Unshaded))
- return;
-
- // If no appearance data is set, rely on whatever existing sprite state is set being correct.
- if (!_appearance.TryGetData<byte>(uid, PowerCellVisuals.ChargeLevel, out var level, args.Component))
- return;
-
- var positiveCharge = level > 0;
- _sprite.LayerSetVisible((uid, args.Sprite), PowerCellVisualLayers.Unshaded, positiveCharge);
-
- if (positiveCharge)
- _sprite.LayerSetRsiState((uid, args.Sprite), PowerCellVisualLayers.Unshaded, $"o{level}");
- }
-
- private enum PowerCellVisualLayers : byte
- {
- Base,
- Unshaded,
- }
-}
--- /dev/null
+namespace Content.Client.PowerCell;
+
+/// <summary>
+/// Sprite layers for power cells.
+/// For use with the generic visualizer.
+/// </summary>
+public enum PowerCellVisualLayers : byte
+{
+ Base,
+ Unshaded,
+}
+++ /dev/null
-namespace Content.Client.PowerCell;
-
-[RegisterComponent]
-public sealed partial class PowerCellVisualsComponent : Component {}
-using Content.Shared.Power;
+using Content.Shared.Power.Components;
namespace Content.Client.PowerCell;
/// <summary>
/// The base sprite state used if the power cell charger does not contain a power cell.
/// </summary>
- [DataField("emptyState")]
- [ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
public string EmptyState = "empty";
/// <summary>
/// The base sprite state used if the power cell charger contains a power cell.
/// </summary>
- [DataField("occupiedState")]
- [ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
public string OccupiedState = "full";
/// <summary>
/// <see cref="CellChargerStatus.Charging"/> Maps to the state used when the charger is charging a power cell.
/// <see cref="CellChargerStatus.Charged"/> Maps to the state used when the charger contains a fully charged power cell.
/// </summary>
- [DataField("lightStates")]
- [ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
public Dictionary<CellChargerStatus, string> LightStates = new()
{
[CellChargerStatus.Off] = "light-off",
-using Content.Shared.Power;
+using Content.Shared.Power.Components;
using Robust.Client.GameObjects;
namespace Content.Client.PowerCell;
protected override void InitializeBattery()
{
base.InitializeBattery();
- // Hitscan
- SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
- SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
- // Projectile
- SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
- SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
+ SubscribeLocalEvent<BatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
+ SubscribeLocalEvent<BatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
}
- private void OnAmmoCountUpdate(EntityUid uid, BatteryAmmoProviderComponent component, UpdateAmmoCounterEvent args)
+ private void OnAmmoCountUpdate(Entity<BatteryAmmoProviderComponent> ent, ref UpdateAmmoCounterEvent args)
{
- if (args.Control is not BoxesStatusControl boxes) return;
+ if (args.Control is not BoxesStatusControl boxes)
+ return;
- boxes.Update(component.Shots, component.Capacity);
+ boxes.Update(ent.Comp.Shots, ent.Comp.Capacity);
}
- private void OnControl(EntityUid uid, BatteryAmmoProviderComponent component, AmmoCounterControlEvent args)
+ private void OnControl(Entity<BatteryAmmoProviderComponent> ent, ref AmmoCounterControlEvent args)
{
args.Control = new BoxesStatusControl();
}
base.Initialize();
UpdatesOutsidePrediction = true;
SubscribeLocalEvent<AmmoCounterComponent, ItemStatusCollectMessage>(OnAmmoCounterCollect);
- SubscribeLocalEvent<AmmoCounterComponent, UpdateClientAmmoEvent>(OnUpdateClientAmmo);
SubscribeAllEvent<MuzzleFlashEvent>(OnMuzzleFlash);
// Plays animated effects on the client.
InitializeSpentAmmo();
}
- private void OnUpdateClientAmmo(EntityUid uid, AmmoCounterComponent ammoComp, ref UpdateClientAmmoEvent args)
- {
- UpdateAmmoCount(uid, ammoComp);
- }
private void OnMuzzleFlash(MuzzleFlashEvent args)
{
public override void Update(float frameTime)
{
+ base.Update(frameTime);
+
if (!Timing.IsFirstTimePredicted)
return;
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;
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;
[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!;
args.Verbs.Add(makeVulnerable);
}
+ if (TryComp<PredictedBatteryComponent>(args.Target, out var pBattery))
+ {
+ Verb refillBattery = new()
+ {
+ Text = Loc.GetString("admin-verbs-refill-battery"),
+ Category = VerbCategory.Tricks,
+ Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/fill_battery.png")),
+ Act = () =>
+ {
+ _predictedBatterySystem.SetCharge((args.Target, pBattery), pBattery.MaxCharge);
+ },
+ Impact = LogImpact.Medium,
+ Message = Loc.GetString("admin-trick-refill-battery-description"),
+ Priority = (int)TricksVerbPriorities.RefillBattery,
+ };
+ args.Verbs.Add(refillBattery);
+
+ Verb drainBattery = new()
+ {
+ Text = Loc.GetString("admin-verbs-drain-battery"),
+ Category = VerbCategory.Tricks,
+ Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/drain_battery.png")),
+ Act = () =>
+ {
+ _predictedBatterySystem.SetCharge((args.Target, pBattery), 0);
+ },
+ Impact = LogImpact.Medium,
+ Priority = (int)TricksVerbPriorities.DrainBattery,
+ };
+ args.Verbs.Add(drainBattery);
+
+ Verb infiniteBattery = new()
+ {
+ Text = Loc.GetString("admin-verbs-infinite-battery"),
+ Category = VerbCategory.Tricks,
+ Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/infinite_battery.png")),
+ Act = () =>
+ {
+ var recharger = EnsureComp<PredictedBatterySelfRechargerComponent>(args.Target);
+ recharger.AutoRechargeRate = pBattery.MaxCharge; // Instant refill.
+ recharger.AutoRechargePauseTime = TimeSpan.Zero; // No delay.
+ Dirty(args.Target, recharger);
+ _predictedBatterySystem.RefreshChargeRate((args.Target, pBattery));
+ },
+ Impact = LogImpact.Medium,
+ Message = Loc.GetString("admin-trick-infinite-battery-object-description"),
+ Priority = (int)TricksVerbPriorities.InfiniteBattery,
+ };
+ args.Verbs.Add(infiniteBattery);
+ }
+
if (TryComp<BatteryComponent>(args.Target, out var battery))
{
Verb refillBattery = new()
var cell = container.ContainedEntities[0];
- if (!entityManager.TryGetComponent<BatteryComponent>(cell, out var batteryComponent))
+ if (!entityManager.TryGetComponent<PredictedBatteryComponent>(cell, out var batteryComponent))
{
Logger.Warning($"Mech construct entity {uid} had an invalid entity in container \"{Container}\"! Aborting build mech action.");
return;
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;
{
// 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)))
{
// 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);
- }
}
[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;
- }
- }
}
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;
[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!;
// Curently every single flashlight has the same number of levels for status and that's all it uses the charge for
// Thus we'll just check if the level changes.
- if (!_powerCell.TryGetBatteryFromSlot(ent, out var battery))
+ if (!_powerCell.TryGetBatteryFromSlot(ent.Owner, out var battery))
return null;
- if (MathHelper.CloseToPercent(battery.CurrentCharge, 0) || ent.Comp.Wattage > battery.CurrentCharge)
+ var currentCharge = _battery.GetCharge(battery.Value.AsNullable());
+
+ if (MathHelper.CloseToPercent(currentCharge, 0) || ent.Comp.Wattage > currentCharge)
return 0;
- return (byte?) ContentHelpers.RoundToNearestLevels(battery.CurrentCharge / battery.MaxCharge * 255, 255, HandheldLightComponent.StatusLevels);
+ return (byte?)ContentHelpers.RoundToNearestLevels(currentCharge / battery.Value.Comp.MaxCharge * 255, 255, HandheldLightComponent.StatusLevels);
}
private void OnRemove(Entity<HandheldLightComponent> ent, ref ComponentRemove args)
_activeLights.Clear();
}
+ // TODO: Very important: Make this charge rate based instead of instantly removing charge each update step.
+ // See PredictedBatteryComponent
public override void Update(float frameTime)
{
var toRemove = new RemQueue<Entity<HandheldLightComponent>>();
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);
// 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);
public void TryUpdate(Entity<HandheldLightComponent> uid, float frameTime)
{
var component = uid.Comp;
- if (!_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery, null) &&
- !TryComp(uid, out battery))
+ if (!_powerCell.TryGetBatteryFromSlot(uid.Owner, out var battery))
{
TurnOff(uid, false);
return;
}
- if (batteryUid == null)
- return;
-
var appearanceComponent = EntityManager.GetComponentOrNull<AppearanceComponent>(uid);
- var fraction = battery.CurrentCharge / battery.MaxCharge;
+ var fraction = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge;
if (fraction >= 0.30)
{
_appearance.SetData(uid, HandheldLightVisuals.Power, HandheldLightPowerStates.FullPower, appearanceComponent);
_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);
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;
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;
{
[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!;
if (TryComp<WiresPanelComponent>(uid, out var panel) && !panel.Open)
return;
- if (component.BatterySlot.ContainedEntity == null && TryComp<BatteryComponent>(args.Used, out var battery))
+ if (component.BatterySlot.ContainedEntity == null && TryComp<PredictedBatteryComponent>(args.Used, out var battery))
{
InsertBattery(uid, args.Used, component, battery);
_actionBlocker.UpdateCanMove(uid);
private void OnInsertBattery(EntityUid uid, MechComponent component, EntInsertedIntoContainerMessage args)
{
- if (args.Container != component.BatterySlot || !TryComp<BatteryComponent>(args.Entity, out var battery))
+ if (args.Container != component.BatterySlot || !TryComp<PredictedBatteryComponent>(args.Entity, out var battery))
return;
- component.Energy = battery.CurrentCharge;
+ component.Energy = _battery.GetCharge((args.Entity, battery));
component.MaxEnergy = battery.MaxCharge;
Dirty(uid, component);
if (battery == null)
return false;
- if (!TryComp<BatteryComponent>(battery, out var batteryComp))
+ if (!TryComp<PredictedBatteryComponent>(battery, out var batteryComp))
return false;
- _battery.SetCharge((battery.Value, batteryComp), batteryComp.CurrentCharge + delta.Float());
- if (batteryComp.CurrentCharge != component.Energy) //if there's a discrepency, we have to resync them
+ _battery.SetCharge((battery.Value, batteryComp), _battery.GetCharge((battery.Value, batteryComp)) + delta.Float());
+ // TODO: Power cells are predicted now, so no need to duplicate the charge level
+ var charge = _battery.GetCharge((battery.Value, batteryComp));
+ if (charge != component.Energy) //if there's a discrepency, we have to resync them
{
- Log.Debug($"Battery charge was not equal to mech charge. Battery {batteryComp.CurrentCharge}. Mech {component.Energy}");
- component.Energy = batteryComp.CurrentCharge;
+ Log.Debug($"Battery charge was not equal to mech charge. Battery {charge}. Mech {component.Energy}");
+ component.Energy = charge;
Dirty(uid, component);
}
_actionBlocker.UpdateCanMove(uid);
return true;
}
- public void InsertBattery(EntityUid uid, EntityUid toInsert, MechComponent? component = null, BatteryComponent? battery = null)
+ public void InsertBattery(EntityUid uid, EntityUid toInsert, MechComponent? component = null, PredictedBatteryComponent? battery = null)
{
if (!Resolve(uid, ref component, false))
return;
return;
_container.Insert(toInsert, component.BatterySlot);
- component.Energy = battery.CurrentCharge;
+ component.Energy = _battery.GetCharge((toInsert, battery));
component.MaxEnergy = battery.MaxCharge;
_actionBlocker.UpdateCanMove(uid);
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;
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;
using Content.Server.Medical.Components;
-using Content.Server.PowerCell;
using Content.Shared.Body.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Damage.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;
/// </summary>
private void OnAfterInteract(Entity<HealthAnalyzerComponent> uid, ref AfterInteractEvent args)
{
- if (args.Target == null || !args.CanReach || !HasComp<MobStateComponent>(args.Target) || !_cell.HasDrawCharge(uid, user: args.User))
+ if (args.Target == null || !args.CanReach || !HasComp<MobStateComponent>(args.Target) || !_cell.HasDrawCharge(uid.Owner, user: args.User))
return;
_audio.PlayPvs(uid.Comp.ScanningBeginSound, uid);
private void OnDoAfter(Entity<HealthAnalyzerComponent> uid, ref HealthAnalyzerDoAfterEvent args)
{
- if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid, user: args.User))
+ if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid.Owner, user: args.User))
return;
if (!uid.Comp.Silent)
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;
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!;
{
var (uid, comp) = ent;
var target = args.Target;
- if (args.Handled || comp.BatteryUid is not {} battery || !HasComp<PowerNetworkBatteryComponent>(target))
+ if (args.Handled || comp.BatteryUid is not { } battery || !HasComp<PowerNetworkBatteryComponent>(target))
return;
// handles even if battery is full so you can actually see the poup
{
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();
}
protected override bool TryDrainPower(Entity<BatteryDrainerComponent> ent, EntityUid target)
{
var (uid, comp) = ent;
- if (comp.BatteryUid == null || !TryComp<BatteryComponent>(comp.BatteryUid.Value, out var battery))
+ if (comp.BatteryUid == null || !TryComp<PredictedBatteryComponent>(comp.BatteryUid.Value, out var battery))
return false;
if (!TryComp<BatteryComponent>(target, out var targetBattery) || !TryComp<PowerNetworkBatteryComponent>(target, out var pnb))
}
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);
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));
}
}
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!;
private void OnCreateItem(Entity<ItemCreatorComponent> ent, ref CreateItemEvent args)
{
var (uid, comp) = ent;
- if (comp.Battery is not {} battery)
+ if (comp.Battery is not { } battery)
return;
args.Handled = true;
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;
/// <summary>
/// Handles power cell upgrading and actions.
+/// TODO: Move all of this to shared and predict it
/// </summary>
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
return;
// no power cell for some reason??? allow it
- if (!_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery))
+ if (!_powerCell.TryGetBatteryFromSlot(uid, out var battery))
return;
- if (!TryComp<BatteryComponent>(args.EntityUid, out var inserting))
+ if (!TryComp<PredictedBatteryComponent>(args.EntityUid, out var inserting))
{
args.Cancel();
return;
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);
}
// this function assigns a score to a power cell depending on the capacity, to be used when comparing which cell is better.
- private float GetCellScore(EntityUid uid, BatteryComponent battcomp)
+ private float GetCellScore(EntityUid uid, PredictedBatteryComponent battcomp)
{
// if a cell is able to automatically recharge, boost the score drastically depending on the recharge rate,
// this is to ensure a ninja can still upgrade to a micro reactor cell even if they already have a medium or high.
- if (TryComp<BatterySelfRechargerComponent>(uid, out var selfcomp) && selfcomp.AutoRecharge)
+ if (TryComp<PredictedBatterySelfRechargerComponent>(uid, out var selfcomp))
return battcomp.MaxCharge + selfcomp.AutoRechargeRate * AutoRechargeValue;
return battcomp.MaxCharge;
}
Popup.PopupEntity(Loc.GetString(message), user, user);
}
- // TODO: Move this to shared when power cells are predicted.
private void OnEmp(Entity<NinjaSuitComponent> ent, ref NinjaEmpEvent args)
{
var (uid, comp) = ent;
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;
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;
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!;
SubscribeLocalEvent<SpaceNinjaComponent, CriminalRecordsHackedEvent>(OnCriminalRecordsHacked);
}
+ // TODO: Make this charge rate based instead of updating it every single tick.
+ // Or make it client side, since power cells are predicted.
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<SpaceNinjaComponent>();
return newCount - oldCount;
}
- // TODO: can probably copy paste borg code here
+ // TODO: Generic charge indicator that is combined with borg code.
/// <summary>
/// Update the alert for the ninja's suit power indicator.
/// </summary>
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
{
/// <summary>
/// Get the battery component in a ninja's suit, if it's worn.
/// </summary>
- public bool GetNinjaBattery(EntityUid user, [NotNullWhen(true)] out EntityUid? uid, [NotNullWhen(true)] out BatteryComponent? battery)
+ public bool GetNinjaBattery(EntityUid user, [NotNullWhen(true)] out EntityUid? batteryUid, [NotNullWhen(true)] out PredictedBatteryComponent? batteryComp)
{
if (TryComp<SpaceNinjaComponent>(user, out var ninja)
&& ninja.Suit != null
- && _powerCell.TryGetBatteryFromSlot(ninja.Suit.Value, out uid, out battery))
+ && _powerCell.TryGetBatteryFromSlot(ninja.Suit.Value, out var battery))
{
+ batteryUid = battery.Value.Owner;
+ batteryComp = battery.Value.Comp;
return true;
}
- uid = null;
- battery = null;
+ batteryUid = null;
+ batteryComp = null;
return false;
}
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;
/// </summary>
public sealed class StunProviderSystem : SharedStunProviderSystem
{
- [Dependency] private readonly BatterySystem _battery = default!;
+ [Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
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;
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;
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;
-using Content.Server.PowerCell;
+using Content.Shared.PowerCell;
using Content.Shared.Pinpointer;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
+++ /dev/null
-using Content.Shared.Containers.ItemSlots;
-using Content.Shared.Power;
-
-namespace Content.Server.Power.Components
-{
- [RegisterComponent]
- public sealed partial class ActiveChargerComponent : Component
- {
- }
-}
+++ /dev/null
-namespace Content.Server.Power.Components
-{
- [RegisterComponent]
- public sealed partial class ExaminableBatteryComponent : Component
- {}
-}
using Content.Shared.Power;
using Content.Shared.Power.Components;
+using Content.Shared.Power.EntitySystems;
namespace Content.Server.Power.EntitySystems;
+/// <summary>
+/// Responsible for <see cref="BatteryComponent"/>.
+/// Unpredicted equivalent of <see cref="PredictedBatterySystem"/>.
+/// If you make changes to this make sure to keep the two consistent.
+/// </summary>
public sealed partial class BatterySystem
{
public override float ChangeCharge(Entity<BatteryComponent?> ent, float amount)
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);
public override float UseCharge(Entity<BatteryComponent?> ent, float amount)
{
- if (amount <= 0 || !Resolve(ent, ref ent.Comp) || ent.Comp.CurrentCharge == 0)
- return 0;
+ if (amount <= 0f || !Resolve(ent, ref ent.Comp) || ent.Comp.CurrentCharge == 0)
+ return 0f;
return ChangeCharge(ent, -amount);
}
RaiseLocalEvent(ent, ref ev);
}
+ /// <summary>
+ /// Gets the battery's current charge.
+ /// </summary>
+ public float GetCharge(Entity<BatteryComponent?> ent)
+ {
+ if (!Resolve(ent, ref ent.Comp, false))
+ return 0f;
+
+ return ent.Comp.CurrentCharge;
+ }
+
+ /// <summary>
+ /// Gets number of remaining uses for the given charge cost.
+ /// </summary>
+ public int GetRemainingUses(Entity<BatteryComponent?> ent, float cost)
+ {
+ if (cost <= 0)
+ return 0;
+
+ if (!Resolve(ent, ref ent.Comp))
+ return 0;
+
+ return (int)(ent.Comp.CurrentCharge / cost);
+ }
+
+ /// <summary>
+ /// Gets number of maximum uses at full charge for the given charge cost.
+ /// </summary>
+ public int GetMaxUses(Entity<BatteryComponent?> ent, float cost)
+ {
+ if (cost <= 0)
+ return 0;
+
+ if (!Resolve(ent, ref ent.Comp))
+ return 0;
+
+ return (int)(ent.Comp.MaxCharge / cost);
+ }
+
public override void TrySetChargeCooldown(Entity<BatterySelfRechargerComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
namespace Content.Server.Power.EntitySystems;
+/// <summary>
+/// Responsible for <see cref="BatteryComponent"/>.
+/// Unpredicted equivalent of <see cref="PredictedBatterySystem"/>.
+/// If you make changes to this make sure to keep the two consistent.
+/// </summary>
[UsedImplicitly]
public sealed partial class BatterySystem : SharedBatterySystem
{
{
base.Initialize();
- SubscribeLocalEvent<ExaminableBatteryComponent, ExaminedEvent>(OnExamine);
+ SubscribeLocalEvent<BatteryComponent, ComponentInit>(OnInit);
+ SubscribeLocalEvent<BatteryComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<BatteryComponent, RejuvenateEvent>(OnBatteryRejuvenate);
SubscribeLocalEvent<PowerNetworkBatteryComponent, RejuvenateEvent>(OnNetBatteryRejuvenate);
SubscribeLocalEvent<BatteryComponent, PriceCalculationEvent>(CalculateBatteryPrice);
SubscribeLocalEvent<NetworkBatteryPostSync>(PostSync);
}
+ private void OnInit(Entity<BatteryComponent> ent, ref ComponentInit args)
+ {
+ DebugTools.Assert(!HasComp<PredictedBatteryComponent>(ent), $"{ent} has both BatteryComponent and PredictedBatteryComponent");
+ }
private void OnNetBatteryRejuvenate(Entity<PowerNetworkBatteryComponent> ent, ref RejuvenateEvent args)
{
ent.Comp.NetworkBattery.CurrentStorage = ent.Comp.NetworkBattery.Capacity;
}
-
private void OnBatteryRejuvenate(Entity<BatteryComponent> ent, ref RejuvenateEvent args)
{
SetCharge(ent.AsNullable(), ent.Comp.MaxCharge);
}
- private void OnExamine(Entity<ExaminableBatteryComponent> ent, ref ExaminedEvent args)
+ private void OnExamine(Entity<BatteryComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
- if (!TryComp<BatteryComponent>(ent, out var battery))
+ if (!HasComp<ExaminableBatteryComponent>(ent))
return;
var chargePercentRounded = 0;
- if (battery.MaxCharge != 0)
- chargePercentRounded = (int)(100 * battery.CurrentCharge / battery.MaxCharge);
+ if (ent.Comp.MaxCharge != 0)
+ chargePercentRounded = (int)(100 * ent.Comp.CurrentCharge / ent.Comp.MaxCharge);
+
args.PushMarkup(
Loc.GetString(
"examinable-battery-component-examine-detail",
+++ /dev/null
-using Content.Server.Power.Components;
-using Content.Shared.Examine;
-using Content.Server.PowerCell;
-using Content.Shared.Power;
-using Content.Shared.Power.Components;
-using Content.Shared.Power.EntitySystems;
-using Content.Shared.PowerCell.Components;
-using Content.Shared.Emp;
-using JetBrains.Annotations;
-using Robust.Shared.Containers;
-using System.Diagnostics.CodeAnalysis;
-using Content.Shared.Storage.Components;
-using Robust.Server.Containers;
-using Content.Shared.Whitelist;
-
-namespace Content.Server.Power.EntitySystems;
-
-[UsedImplicitly]
-public sealed class ChargerSystem : SharedChargerSystem
-{
- [Dependency] private readonly ContainerSystem _container = default!;
- [Dependency] private readonly PowerCellSystem _powerCell = default!;
- [Dependency] private readonly BatterySystem _battery = default!;
- [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
- [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent<ChargerComponent, ComponentStartup>(OnStartup);
- SubscribeLocalEvent<ChargerComponent, PowerChangedEvent>(OnPowerChanged);
- SubscribeLocalEvent<ChargerComponent, EntInsertedIntoContainerMessage>(OnInserted);
- SubscribeLocalEvent<ChargerComponent, EntRemovedFromContainerMessage>(OnRemoved);
- SubscribeLocalEvent<ChargerComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
- SubscribeLocalEvent<ChargerComponent, InsertIntoEntityStorageAttemptEvent>(OnEntityStorageInsertAttempt);
- SubscribeLocalEvent<ChargerComponent, ExaminedEvent>(OnChargerExamine);
- }
-
- private void OnStartup(EntityUid uid, ChargerComponent component, ComponentStartup args)
- {
- UpdateStatus(uid, component);
- }
-
- private void OnChargerExamine(EntityUid uid, ChargerComponent component, ExaminedEvent args)
- {
- using (args.PushGroup(nameof(ChargerComponent)))
- {
- // rate at which the charger charges
- args.PushMarkup(Loc.GetString("charger-examine", ("color", "yellow"), ("chargeRate", (int)component.ChargeRate)));
-
- // try to get contents of the charger
- if (!_container.TryGetContainer(uid, component.SlotId, out var container))
- return;
-
- if (HasComp<PowerCellSlotComponent>(uid))
- return;
-
- // if charger is empty and not a power cell type charger, add empty message
- // power cells have their own empty message by default, for things like flash lights
- if (container.ContainedEntities.Count == 0)
- {
- args.PushMarkup(Loc.GetString("charger-empty"));
- }
- else
- {
- // add how much each item is charged it
- foreach (var contained in container.ContainedEntities)
- {
- if (!TryComp<BatteryComponent>(contained, out var battery))
- continue;
-
- var chargePercentage = (battery.CurrentCharge / battery.MaxCharge) * 100;
- args.PushMarkup(Loc.GetString("charger-content", ("chargePercentage", (int)chargePercentage)));
- }
- }
- }
- }
-
- public override void Update(float frameTime)
- {
- var query = EntityQueryEnumerator<ActiveChargerComponent, ChargerComponent, ContainerManagerComponent>();
- while (query.MoveNext(out var uid, out _, out var charger, out var containerComp))
- {
- if (!_container.TryGetContainer(uid, charger.SlotId, out var container, containerComp))
- continue;
-
- if (charger.Status == CellChargerStatus.Empty || charger.Status == CellChargerStatus.Charged || container.ContainedEntities.Count == 0)
- continue;
-
- foreach (var contained in container.ContainedEntities)
- {
- TransferPower(uid, contained, charger, frameTime);
- }
- }
- }
-
- private void OnPowerChanged(EntityUid uid, ChargerComponent component, ref PowerChangedEvent args)
- {
- UpdateStatus(uid, component);
- }
-
- private void OnInserted(EntityUid uid, ChargerComponent component, EntInsertedIntoContainerMessage args)
- {
- if (!component.Initialized)
- return;
-
- if (args.Container.ID != component.SlotId)
- return;
-
- UpdateStatus(uid, component);
- }
-
- private void OnRemoved(EntityUid uid, ChargerComponent component, EntRemovedFromContainerMessage args)
- {
- if (args.Container.ID != component.SlotId)
- return;
-
- UpdateStatus(uid, component);
- }
-
- /// <summary>
- /// Verify that the entity being inserted is actually rechargeable.
- /// </summary>
- private void OnInsertAttempt(EntityUid uid, ChargerComponent component, ContainerIsInsertingAttemptEvent args)
- {
- if (!component.Initialized)
- return;
-
- if (args.Container.ID != component.SlotId)
- return;
-
- if (!TryComp<PowerCellSlotComponent>(args.EntityUid, out var cellSlot))
- return;
-
- if (!cellSlot.FitsInCharger)
- args.Cancel();
- }
-
- private void OnEntityStorageInsertAttempt(EntityUid uid, ChargerComponent component, ref InsertIntoEntityStorageAttemptEvent args)
- {
- if (!component.Initialized || args.Cancelled)
- return;
-
- if (!TryComp<PowerCellSlotComponent>(uid, out var cellSlot))
- return;
-
- if (!cellSlot.FitsInCharger)
- args.Cancelled = true;
- }
-
- private void UpdateStatus(EntityUid uid, ChargerComponent component)
- {
- var status = GetStatus(uid, component);
- TryComp(uid, out AppearanceComponent? appearance);
-
- if (!_container.TryGetContainer(uid, component.SlotId, out var container))
- return;
-
- _appearance.SetData(uid, CellVisual.Occupied, container.ContainedEntities.Count != 0, appearance);
- if (component.Status == status || !TryComp(uid, out ApcPowerReceiverComponent? receiver))
- return;
-
- component.Status = status;
-
- if (component.Status == CellChargerStatus.Charging)
- {
- AddComp<ActiveChargerComponent>(uid);
- }
- else
- {
- RemComp<ActiveChargerComponent>(uid);
- }
-
- switch (component.Status)
- {
- case CellChargerStatus.Off:
- receiver.Load = 0;
- _appearance.SetData(uid, CellVisual.Light, CellChargerStatus.Off, appearance);
- break;
- case CellChargerStatus.Empty:
- receiver.Load = 0;
- _appearance.SetData(uid, CellVisual.Light, CellChargerStatus.Empty, appearance);
- break;
- case CellChargerStatus.Charging:
- receiver.Load = component.ChargeRate;
- _appearance.SetData(uid, CellVisual.Light, CellChargerStatus.Charging, appearance);
- break;
- case CellChargerStatus.Charged:
- receiver.Load = 0;
- _appearance.SetData(uid, CellVisual.Light, CellChargerStatus.Charged, appearance);
- break;
- default:
- throw new ArgumentOutOfRangeException();
- }
- }
-
- private CellChargerStatus GetStatus(EntityUid uid, ChargerComponent component)
- {
- if (!component.Portable)
- {
- if (!TryComp(uid, out TransformComponent? transformComponent) || !transformComponent.Anchored)
- return CellChargerStatus.Off;
- }
-
- if (!TryComp(uid, out ApcPowerReceiverComponent? apcPowerReceiverComponent))
- return CellChargerStatus.Off;
-
- if (!component.Portable && !apcPowerReceiverComponent.Powered)
- return CellChargerStatus.Off;
-
- if (HasComp<EmpDisabledComponent>(uid))
- return CellChargerStatus.Off;
-
- if (!_container.TryGetContainer(uid, component.SlotId, out var container))
- return CellChargerStatus.Off;
-
- if (container.ContainedEntities.Count == 0)
- return CellChargerStatus.Empty;
-
- if (!SearchForBattery(container.ContainedEntities[0], out var heldEnt, out var heldBattery))
- return CellChargerStatus.Off;
-
- if (_battery.IsFull((heldEnt.Value, heldBattery)))
- return CellChargerStatus.Charged;
-
- return CellChargerStatus.Charging;
- }
-
- private void TransferPower(EntityUid uid, EntityUid targetEntity, ChargerComponent component, float frameTime)
- {
- if (!TryComp(uid, out ApcPowerReceiverComponent? receiverComponent))
- return;
-
- if (!receiverComponent.Powered)
- return;
-
- if (_whitelistSystem.IsWhitelistFail(component.Whitelist, targetEntity))
- return;
-
- if (!SearchForBattery(targetEntity, out var batteryUid, out var heldBattery))
- return;
-
- _battery.SetCharge((batteryUid.Value, heldBattery), heldBattery.CurrentCharge + component.ChargeRate * frameTime);
- UpdateStatus(uid, component);
- }
-
- private bool SearchForBattery(EntityUid uid, [NotNullWhen(true)] out EntityUid? batteryUid, [NotNullWhen(true)] out BatteryComponent? component)
- {
- // try get a battery directly on the inserted entity
- if (!TryComp(uid, out component))
- {
- // or by checking for a power cell slot on the inserted entity
- return _powerCell.TryGetBatteryFromSlot(uid, out batteryUid, out component);
- }
- batteryUid = uid;
- return true;
- }
-}
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)
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;
{
[Dependency] private readonly ExplosionSystem _explosionSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
+ [Dependency] private readonly PredictedBatterySystem _predictedBattery = default!;
public override void Initialize()
{
SubscribeLocalEvent<RiggableComponent, RejuvenateEvent>(OnRejuvenate);
SubscribeLocalEvent<RiggableComponent, BeingMicrowavedEvent>(OnMicrowaved);
SubscribeLocalEvent<RiggableComponent, SolutionContainerChangedEvent>(OnSolutionChanged);
+ SubscribeLocalEvent<RiggableComponent, ChargeChangedEvent>(OnChargeChanged);
+ SubscribeLocalEvent<RiggableComponent, PredictedBatteryChargeChangedEvent>(OnChargeChanged);
}
private void OnRejuvenate(Entity<RiggableComponent> entity, ref RejuvenateEvent args)
{
if (TryComp<BatteryComponent>(entity, out var batteryComponent))
{
- if (batteryComponent.CurrentCharge == 0)
+ if (batteryComponent.CurrentCharge == 0f)
return;
+
+ Explode(entity, batteryComponent.CurrentCharge);
+ args.Handled = true;
}
- args.Handled = true;
+ if (TryComp<PredictedBatteryComponent>(entity, out var predictedBatteryComponent))
+ {
+ var charge = _predictedBattery.GetCharge((entity, predictedBatteryComponent));
+ if (charge == 0f)
+ return;
- // What the fuck are you doing???
- Explode(entity.Owner, batteryComponent, args.User);
+ Explode(entity, charge);
+ args.Handled = true;
+ }
}
private void OnSolutionChanged(Entity<RiggableComponent> entity, ref SolutionContainerChangedEvent args)
}
}
- public void Explode(EntityUid uid, BatteryComponent? battery = null, EntityUid? cause = null)
+ public void Explode(EntityUid uid, float charge, EntityUid? cause = null)
+ {
+ var radius = MathF.Min(5, MathF.Sqrt(charge) / 9);
+
+ _explosionSystem.TriggerExplosive(uid, radius: radius, user: cause);
+ QueueDel(uid);
+ }
+
+ // non-predicted batteries
+ private void OnChargeChanged(Entity<RiggableComponent> ent, ref ChargeChangedEvent args)
+ {
+ if (!ent.Comp.IsRigged)
+ return;
+
+ if (TryComp<BatteryComponent>(ent, out var batteryComponent))
+ {
+ if (batteryComponent.CurrentCharge == 0f)
+ return;
+
+ Explode(ent, batteryComponent.CurrentCharge);
+ }
+ }
+
+ // predicted batteries
+ private void OnChargeChanged(Entity<RiggableComponent> ent, ref PredictedBatteryChargeChangedEvent args)
{
- if (!Resolve(uid, ref battery))
+ if (!ent.Comp.IsRigged)
return;
- var radius = MathF.Min(5, MathF.Sqrt(battery.CurrentCharge) / 9);
+ if (TryComp<PredictedBatteryComponent>(ent, out var predictedBatteryComponent))
+ {
+ var charge = _predictedBattery.GetCharge((ent.Owner, predictedBatteryComponent));
+ if (charge == 0f)
+ return;
- _explosionSystem.TriggerExplosive(uid, radius: radius, user:cause);
- QueueDel(uid);
+ Explode(ent, charge);
+ }
}
}
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
public sealed class SetBatteryPercentCommand : LocalizedEntityCommands
{
[Dependency] private readonly BatterySystem _batterySystem = default!;
+ [Dependency] private readonly PredictedBatterySystem _predictedBatterySystem = default!;
public override string Command => "setbatterypercent";
return;
}
- if (!EntityManager.TryGetComponent<BatteryComponent>(id, out var battery))
+ if (EntityManager.TryGetComponent<BatteryComponent>(id, out var battery))
+ _batterySystem.SetCharge((id.Value, battery), battery.MaxCharge * percent / 100);
+ else if (EntityManager.TryGetComponent<PredictedBatteryComponent>(id, out var pBattery))
+ _predictedBatterySystem.SetCharge((id.Value, pBattery), pBattery.MaxCharge * percent / 100);
+ else
{
shell.WriteLine(Loc.GetString($"cmd-setbatterypercent-battery-not-found", ("id", id)));
return;
}
- _batterySystem.SetCharge((id.Value, battery), battery.MaxCharge * percent / 100);
// Don't acknowledge b/c people WILL forall this
}
}
+++ /dev/null
-using Content.Shared.Power;
-using Content.Shared.PowerCell;
-using Content.Shared.PowerCell.Components;
-
-namespace Content.Server.PowerCell;
-
-public sealed partial class PowerCellSystem
-{
- /*
- * Handles PowerCellDraw
- */
-
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
- var query = EntityQueryEnumerator<PowerCellDrawComponent, PowerCellSlotComponent>();
-
- while (query.MoveNext(out var uid, out var comp, out var slot))
- {
- if (!comp.Enabled)
- continue;
-
- if (Timing.CurTime < comp.NextUpdateTime)
- continue;
-
- comp.NextUpdateTime += comp.Delay;
-
- if (!TryGetBatteryFromSlot(uid, out var batteryEnt, out var battery, slot))
- continue;
-
- if (_battery.TryUseCharge((batteryEnt.Value, battery), comp.DrawRate * (float)comp.Delay.TotalSeconds))
- continue;
-
- var ev = new PowerCellSlotEmptyEvent();
- RaiseLocalEvent(uid, ref ev);
- }
- }
-
- private void OnDrawChargeChanged(EntityUid uid, PowerCellDrawComponent component, ref ChargeChangedEvent args)
- {
- // Update the bools for client prediction.
- var canUse = component.UseRate <= 0f || args.Charge > component.UseRate;
-
- var canDraw = component.DrawRate <= 0f || args.Charge > 0f;
-
- if (canUse != component.CanUse || canDraw != component.CanDraw)
- {
- component.CanDraw = canDraw;
- component.CanUse = canUse;
- Dirty(uid, component);
- }
- }
-
- private void OnDrawCellChanged(EntityUid uid, PowerCellDrawComponent component, PowerCellChangedEvent args)
- {
- var canDraw = !args.Ejected && HasCharge(uid, float.MinValue);
- var canUse = !args.Ejected && HasActivatableCharge(uid, component);
-
- if (!canDraw)
- {
- var ev = new PowerCellSlotEmptyEvent();
- RaiseLocalEvent(uid, ref ev);
- }
-
- if (canUse != component.CanUse || canDraw != component.CanDraw)
- {
- component.CanDraw = canDraw;
- component.CanUse = canUse;
- Dirty(uid, component);
- }
- }
-}
+++ /dev/null
-using System.Diagnostics.CodeAnalysis;
-using Content.Server.Kitchen.Components;
-using Content.Server.Power.Components;
-using Content.Server.Power.EntitySystems;
-using Content.Shared.Containers.ItemSlots;
-using Content.Shared.Examine;
-using Content.Shared.Popups;
-using Content.Shared.Power;
-using Content.Shared.Power.Components;
-using Content.Shared.PowerCell;
-using Content.Shared.PowerCell.Components;
-using Content.Shared.Rounding;
-using Content.Shared.UserInterface;
-using Robust.Shared.Containers;
-
-namespace Content.Server.PowerCell;
-
-/// <summary>
-/// Handles Power cells
-/// </summary>
-public sealed partial class PowerCellSystem : SharedPowerCellSystem
-{
- [Dependency] private readonly ActivatableUISystem _activatable = default!;
- [Dependency] private readonly BatterySystem _battery = default!;
- [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
- [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!;
- [Dependency] private readonly SharedAppearanceSystem _sharedAppearanceSystem = default!;
- [Dependency] private readonly SharedPopupSystem _popup = default!;
- [Dependency] private readonly RiggableSystem _riggableSystem = default!;
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent<PowerCellComponent, ChargeChangedEvent>(OnChargeChanged);
- SubscribeLocalEvent<PowerCellComponent, ExaminedEvent>(OnCellExamined);
-
- SubscribeLocalEvent<PowerCellDrawComponent, ChargeChangedEvent>(OnDrawChargeChanged);
- SubscribeLocalEvent<PowerCellDrawComponent, PowerCellChangedEvent>(OnDrawCellChanged);
-
- SubscribeLocalEvent<PowerCellSlotComponent, ExaminedEvent>(OnCellSlotExamined);
- // funny
- SubscribeLocalEvent<PowerCellSlotComponent, BeingMicrowavedEvent>(OnSlotMicrowaved);
-
- SubscribeLocalEvent<PowerCellSlotComponent, GetChargeEvent>(OnGetCharge);
- SubscribeLocalEvent<PowerCellSlotComponent, ChangeChargeEvent>(OnChangeCharge);
- }
-
- private void OnSlotMicrowaved(EntityUid uid, PowerCellSlotComponent component, BeingMicrowavedEvent args)
- {
- if (!_itemSlotsSystem.TryGetSlot(uid, component.CellSlotId, out var slot))
- return;
-
- if (slot.Item == null)
- return;
-
- RaiseLocalEvent(slot.Item.Value, args);
- }
-
- private void OnChargeChanged(EntityUid uid, PowerCellComponent component, ref ChargeChangedEvent args)
- {
- if (TryComp<RiggableComponent>(uid, out var rig) && rig.IsRigged)
- {
- _riggableSystem.Explode(uid, cause: null);
- return;
- }
-
- var frac = args.Charge / args.MaxCharge;
- var level = (byte)ContentHelpers.RoundToNearestLevels(frac, 1, PowerCellComponent.PowerCellVisualsLevels);
- _sharedAppearanceSystem.SetData(uid, PowerCellVisuals.ChargeLevel, level);
-
- // If this power cell is inside a cell-slot, inform that entity that the power has changed (for updating visuals n such).
- if (_containerSystem.TryGetContainingContainer((uid, null, null), out var container)
- && TryComp(container.Owner, out PowerCellSlotComponent? slot)
- && _itemSlotsSystem.TryGetSlot(container.Owner, slot.CellSlotId, out var itemSlot))
- {
- if (itemSlot.Item == uid)
- RaiseLocalEvent(container.Owner, new PowerCellChangedEvent(false));
- }
- }
-
- protected override void OnCellRemoved(EntityUid uid, PowerCellSlotComponent component, EntRemovedFromContainerMessage args)
- {
- base.OnCellRemoved(uid, component, args);
-
- if (args.Container.ID != component.CellSlotId)
- return;
-
- var ev = new PowerCellSlotEmptyEvent();
- RaiseLocalEvent(uid, ref ev);
- }
-
- #region Activatable
- /// <inheritdoc/>
- public override bool HasActivatableCharge(EntityUid uid, PowerCellDrawComponent? battery = null, PowerCellSlotComponent? cell = null, EntityUid? user = null)
- {
- // Default to true if we don't have the components.
- if (!Resolve(uid, ref battery, ref cell, false))
- return true;
-
- return HasCharge(uid, battery.UseRate, cell, user);
- }
-
- /// <summary>
- /// Tries to use the <see cref="PowerCellDrawComponent.UseRate"/> for this entity.
- /// </summary>
- /// <param name="user">Popup to this user with the relevant detail if specified.</param>
- public bool TryUseActivatableCharge(EntityUid uid, PowerCellDrawComponent? battery = null, PowerCellSlotComponent? cell = null, EntityUid? user = null)
- {
- // Default to true if we don't have the components.
- if (!Resolve(uid, ref battery, ref cell, false))
- return true;
-
- if (TryUseCharge(uid, battery.UseRate, cell, user))
- {
- _sharedAppearanceSystem.SetData(uid, PowerCellSlotVisuals.Enabled, HasActivatableCharge(uid, battery, cell, user));
- _activatable.CheckUsage(uid);
- return true;
- }
-
- return false;
- }
-
- /// <inheritdoc/>
- public override bool HasDrawCharge(
- EntityUid uid,
- PowerCellDrawComponent? battery = null,
- PowerCellSlotComponent? cell = null,
- EntityUid? user = null)
- {
- if (!Resolve(uid, ref battery, ref cell, false))
- return true;
-
- return HasCharge(uid, battery.DrawRate, cell, user);
- }
-
- #endregion
-
- /// <summary>
- /// Returns whether the entity has a slotted battery and charge for the requested action.
- /// </summary>
- /// <param name="user">Popup to this user with the relevant detail if specified.</param>
- public bool HasCharge(EntityUid uid, float charge, PowerCellSlotComponent? component = null, EntityUid? user = null)
- {
- if (!TryGetBatteryFromSlot(uid, out var battery, component))
- {
- if (user != null)
- _popup.PopupEntity(Loc.GetString("power-cell-no-battery"), uid, user.Value);
-
- return false;
- }
-
- if (battery.CurrentCharge < charge)
- {
- if (user != null)
- _popup.PopupEntity(Loc.GetString("power-cell-insufficient"), uid, user.Value);
-
- return false;
- }
-
- return true;
- }
-
- /// <summary>
- /// Tries to use charge from a slotted battery.
- /// </summary>
- public bool TryUseCharge(EntityUid uid, float charge, PowerCellSlotComponent? component = null, EntityUid? user = null)
- {
- if (!TryGetBatteryFromSlot(uid, out var batteryEnt, out var battery, component))
- {
- if (user != null)
- _popup.PopupEntity(Loc.GetString("power-cell-no-battery"), uid, user.Value);
-
- return false;
- }
-
- if (!_battery.TryUseCharge((batteryEnt.Value, battery), charge))
- {
- if (user != null)
- _popup.PopupEntity(Loc.GetString("power-cell-insufficient"), uid, user.Value);
-
- return false;
- }
-
- _sharedAppearanceSystem.SetData(uid, PowerCellSlotVisuals.Enabled, battery.CurrentCharge > 0);
- return true;
- }
-
- public bool TryGetBatteryFromSlot(EntityUid uid, [NotNullWhen(true)] out BatteryComponent? battery, PowerCellSlotComponent? component = null)
- {
- return TryGetBatteryFromSlot(uid, out _, out battery, component);
- }
-
- public bool TryGetBatteryFromSlot(EntityUid uid,
- [NotNullWhen(true)] out EntityUid? batteryEnt,
- [NotNullWhen(true)] out BatteryComponent? battery,
- PowerCellSlotComponent? component = null)
- {
- if (!Resolve(uid, ref component, false))
- {
- batteryEnt = null;
- battery = null;
- return false;
- }
-
- if (_itemSlotsSystem.TryGetSlot(uid, component.CellSlotId, out ItemSlot? slot))
- {
- batteryEnt = slot.Item;
- return TryComp(slot.Item, out battery);
- }
-
- batteryEnt = null;
- battery = null;
- return false;
- }
-
- private void OnCellExamined(EntityUid uid, PowerCellComponent component, ExaminedEvent args)
- {
- TryComp<BatteryComponent>(uid, out var battery);
- OnBatteryExamined(uid, battery, args);
- }
-
- private void OnCellSlotExamined(EntityUid uid, PowerCellSlotComponent component, ExaminedEvent args)
- {
- TryGetBatteryFromSlot(uid, out var battery);
- OnBatteryExamined(uid, battery, args);
- }
-
- private void OnBatteryExamined(EntityUid uid, BatteryComponent? component, ExaminedEvent args)
- {
- if (component != null)
- {
- var charge = component.CurrentCharge / component.MaxCharge * 100;
- args.PushMarkup(Loc.GetString("power-cell-component-examine-details", ("currentCharge", $"{charge:F0}")));
- }
- else
- {
- args.PushMarkup(Loc.GetString("power-cell-component-examine-details-no-battery"));
- }
- }
-
- private void OnGetCharge(Entity<PowerCellSlotComponent> entity, ref GetChargeEvent args)
- {
- if (!TryGetBatteryFromSlot(entity, out var batteryUid, out _))
- return;
-
- RaiseLocalEvent(batteryUid.Value, ref args);
- }
-
- private void OnChangeCharge(Entity<PowerCellSlotComponent> entity, ref ChangeChargeEvent args)
- {
- if (!TryGetBatteryFromSlot(entity, out var batteryUid, out _))
- return;
-
- RaiseLocalEvent(batteryUid.Value, ref args);
- }
-}
-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;
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!;
SubscribeLocalEvent<RadioSendAttemptEvent>(OnRadioSendAttempt);
}
+ // TODO: Very important: Make this charge rate based instead of updating every single tick
+ // See PredictedBatteryComponent
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<ActiveRadioJammerComponent, RadioJammerComponent>();
while (query.MoveNext(out var uid, out var _, out var jam))
{
- if (_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery))
+ if (_powerCell.TryGetBatteryFromSlot(uid, out var battery))
{
- if (!_battery.TryUseCharge((batteryUid.Value, battery), GetCurrentWattage((uid, jam)) * frameTime))
+ if (!_battery.TryUseCharge(battery.Value.AsNullable(), GetCurrentWattage((uid, jam)) * frameTime))
{
ChangeLEDState(uid, false);
RemComp<ActiveRadioJammerComponent>(uid);
}
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,
var activated = !HasComp<ActiveRadioJammerComponent>(ent) &&
_powerCell.TryGetBatteryFromSlot(ent.Owner, out var battery) &&
- battery.CurrentCharge > GetCurrentWattage(ent);
+ _battery.GetCharge(battery.Value.AsNullable()) > GetCurrentWattage(ent);
if (activated)
{
ChangeLEDState(ent.Owner, true);
SubscribeLocalEvent<BorgTransponderComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
}
- public override void Update(float frameTime)
+ public void UpdateTransponder(float frameTime)
{
- base.Update(frameTime);
-
var now = _timing.CurTime;
var query = EntityQueryEnumerator<BorgTransponderComponent, BorgChassisComponent, DeviceNetworkComponent, MetaDataComponent>();
while (query.MoveNext(out var uid, out var comp, out var chassis, out var device, out var meta))
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);
UpdateUI(uid, component);
}
+ public void UpdateBattery(Entity<BorgChassisComponent> ent)
+ {
+ UpdateBatteryAlert(ent);
+
+ // if we aren't drawing and suddenly get enough power to draw again, reeanble.
+ if (_powerCell.HasDrawCharge(ent.Owner))
+ {
+ Toggle.TryActivate(ent.Owner);
+ }
+
+ UpdateUI(ent, ent);
+ }
+
+ // TODO: Move to client so we don't have to network this periodically.
+ private void UpdateBatteryAlert(Entity<BorgChassisComponent> ent, PowerCellSlotComponent? slotComponent = null)
+ {
+ if (!_powerCell.TryGetBatteryFromSlot((ent.Owner, slotComponent), out var battery))
+ {
+ _alerts.ClearAlert(ent.Owner, ent.Comp.BatteryAlert);
+ _alerts.ShowAlert(ent.Owner, ent.Comp.NoBatteryAlert);
+ return;
+ }
+
+ var chargePercent = (short)MathF.Round(_battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge * 10f);
+
+ // we make sure 0 only shows if they have absolutely no battery.
+ // also account for floating point imprecision
+ if (chargePercent == 0 && _powerCell.HasDrawCharge((ent.Owner, null, slotComponent)))
+ {
+ chargePercent = 1;
+ }
+
+ _alerts.ClearAlert(ent.Owner, ent.Comp.NoBatteryAlert);
+ _alerts.ShowAlert(ent.Owner, ent.Comp.BatteryAlert, chargePercent);
+ }
+
+ // TODO: Component states and update this on the client
public void UpdateUI(EntityUid uid, BorgChassisComponent? component = null)
{
if (!Resolve(uid, ref component))
if (_powerCell.TryGetBatteryFromSlot(uid, out var battery))
{
hasBattery = true;
- chargePercent = battery.CurrentCharge / battery.MaxCharge;
+ chargePercent = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge;
}
var state = new BorgBuiState(chargePercent, hasBattery);
_ui.SetUiState(uid, BorgUiKey.Key, state);
}
+
+ // periodically update the charge indicator
+ // TODO: Move this to the client.
+ public void UpdateBattery(float frameTime)
+ {
+ var curTime = _timing.CurTime;
+ var query = EntityQueryEnumerator<BorgChassisComponent>();
+ while (query.MoveNext(out var uid, out var borgChassis))
+ {
+ if (curTime < borgChassis.NextBatteryUpdate)
+ continue;
+
+ UpdateBattery((uid, borgChassis));
+ borgChassis.NextBatteryUpdate = curTime + TimeSpan.FromSeconds(1);
+ }
+ }
}
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;
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;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly ISharedPlayerManager _player = default!;
+ [Dependency] private readonly PredictedBatterySystem _battery = default!;
public static readonly ProtoId<JobPrototype> BorgJobId = "Borg";
SubscribeLocalEvent<BorgChassisComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<BorgChassisComponent, BeingGibbedEvent>(OnBeingGibbed);
SubscribeLocalEvent<BorgChassisComponent, PowerCellChangedEvent>(OnPowerCellChanged);
+ SubscribeLocalEvent<BorgChassisComponent, PredictedBatteryChargeChangedEvent>(OnBatteryChargeChanged);
SubscribeLocalEvent<BorgChassisComponent, PowerCellSlotEmptyEvent>(OnPowerCellSlotEmpty);
SubscribeLocalEvent<BorgChassisComponent, GetCharactedDeadIcEvent>(OnGetDeadIC);
SubscribeLocalEvent<BorgChassisComponent, GetCharacterUnrevivableIcEvent>(OnGetUnrevivableIC);
_container.EmptyContainer(component.ModuleContainer);
}
- private void OnPowerCellChanged(EntityUid uid, BorgChassisComponent component, PowerCellChangedEvent args)
+ private void OnPowerCellChanged(Entity<BorgChassisComponent> ent, ref PowerCellChangedEvent args)
{
- UpdateBatteryAlert((uid, component));
-
- // if we aren't drawing and suddenly get enough power to draw again, reeanble.
- if (_powerCell.HasDrawCharge(uid))
- {
- Toggle.TryActivate(uid);
- }
+ UpdateBattery(ent);
+ }
- UpdateUI(uid, component);
+ private void OnBatteryChargeChanged(Entity<BorgChassisComponent> ent, ref PredictedBatteryChargeChangedEvent args)
+ {
+ UpdateBattery(ent);
}
private void OnPowerCellSlotEmpty(EntityUid uid, BorgChassisComponent component, ref PowerCellSlotEmptyEvent args)
args.Cancel();
}
- private void UpdateBatteryAlert(Entity<BorgChassisComponent> ent, PowerCellSlotComponent? slotComponent = null)
- {
- if (!_powerCell.TryGetBatteryFromSlot(ent, out var battery, slotComponent))
- {
- _alerts.ClearAlert(ent.Owner, ent.Comp.BatteryAlert);
- _alerts.ShowAlert(ent.Owner, ent.Comp.NoBatteryAlert);
- return;
- }
-
- var chargePercent = (short) MathF.Round(battery.CurrentCharge / battery.MaxCharge * 10f);
-
- // we make sure 0 only shows if they have absolutely no battery.
- // also account for floating point imprecision
- if (chargePercent == 0 && _powerCell.HasDrawCharge(ent, cell: slotComponent))
- {
- chargePercent = 1;
- }
-
- _alerts.ClearAlert(ent.Owner, ent.Comp.NoBatteryAlert);
- _alerts.ShowAlert(ent.Owner, ent.Comp.BatteryAlert, chargePercent);
- }
-
public bool TryEjectPowerCell(EntityUid uid, BorgChassisComponent component, [NotNullWhen(true)] out List<EntityUid>? ents)
{
ents = null;
return true;
}
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ UpdateTransponder(frameTime);
+ UpdateBattery(frameTime);
+ }
}
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;
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!;
}
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)
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;
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
{
[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()
SubscribeLocalEvent<StunbatonComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<StunbatonComponent, SolutionContainerChangedEvent>(OnSolutionChange);
SubscribeLocalEvent<StunbatonComponent, StaminaDamageOnHitAttemptEvent>(OnStaminaHitAttempt);
- SubscribeLocalEvent<StunbatonComponent, ChargeChangedEvent>(OnChargeChanged);
+ SubscribeLocalEvent<StunbatonComponent, PredictedBatteryChargeChangedEvent>(OnChargeChanged);
}
private void OnStaminaHitAttempt(Entity<StunbatonComponent> entity, ref StaminaDamageOnHitAttemptEvent args)
{
if (!_itemToggle.IsActivated(entity.Owner) ||
- !TryComp<BatteryComponent>(entity.Owner, out var battery) || !_battery.TryUseCharge((entity.Owner, battery), entity.Comp.EnergyPerUse))
+ !TryComp<PredictedBatteryComponent>(entity.Owner, out var battery) || !_battery.TryUseCharge((entity.Owner, battery), entity.Comp.EnergyPerUse))
{
args.Cancelled = true;
}
: Loc.GetString("comp-stunbaton-examined-off");
args.PushMarkup(onMsg);
- if (TryComp<BatteryComponent>(entity.Owner, out var battery))
+ if (TryComp<PredictedBatteryComponent>(entity.Owner, out var battery))
{
- var count = (int) (battery.CurrentCharge / entity.Comp.EnergyPerUse);
+ var count = _battery.GetRemainingUses((entity.Owner, battery), entity.Comp.EnergyPerUse);
args.PushMarkup(Loc.GetString("melee-battery-examine", ("color", "yellow"), ("count", count)));
}
}
{
base.TryTurnOn(entity, ref args);
- if (!TryComp<BatteryComponent>(entity, out var battery) || battery.CurrentCharge < entity.Comp.EnergyPerUse)
+ if (!TryComp<PredictedBatteryComponent>(entity, out var battery) || _battery.GetCharge((entity, battery)) < entity.Comp.EnergyPerUse)
{
args.Cancelled = true;
if (args.User != null)
if (TryComp<RiggableComponent>(entity, out var rig) && rig.IsRigged)
{
- _riggableSystem.Explode(entity.Owner, battery, args.User);
+ _riggableSystem.Explode(entity.Owner, _battery.GetCharge((entity, battery)), args.User);
}
}
{
// Explode if baton is activated and rigged.
if (!TryComp<RiggableComponent>(entity, out var riggable) ||
- !TryComp<BatteryComponent>(entity, out var battery))
+ !TryComp<PredictedBatteryComponent>(entity, out var battery))
return;
if (_itemToggle.IsActivated(entity.Owner) && riggable.IsRigged)
- _riggableSystem.Explode(entity.Owner, battery);
+ _riggableSystem.Explode(entity.Owner, _battery.GetCharge((entity, battery)));
}
+ // TODO: Not used anywhere?
private void SendPowerPulse(EntityUid target, EntityUid? user, EntityUid used)
{
RaiseLocalEvent(target, new PowerPulseEvent()
});
}
- private void OnChargeChanged(Entity<StunbatonComponent> entity, ref ChargeChangedEvent args)
+ private void OnChargeChanged(Entity<StunbatonComponent> entity, ref PredictedBatteryChargeChangedEvent args)
{
- if (TryComp<BatteryComponent>(entity.Owner, out var battery) &&
- battery.CurrentCharge < entity.Comp.EnergyPerUse)
+ if (TryComp<PredictedBatteryComponent>(entity.Owner, out var battery) &&
+ _battery.GetCharge((entity.Owner, battery)) < entity.Comp.EnergyPerUse)
{
_itemToggle.TryDeactivate(entity.Owner, predicted: false);
}
-using Content.Server.PowerCell;
using Content.Shared.Item.ItemToggle;
using Content.Shared.PowerCell;
using Content.Shared.Weapons.Misc;
+++ /dev/null
-using Content.Shared.Power;
-using Content.Shared.PowerCell.Components;
-using Content.Shared.Weapons.Ranged.Components;
-using Content.Shared.Weapons.Ranged.Events;
-
-namespace Content.Server.Weapons.Ranged.Systems;
-
-public sealed partial class GunSystem
-{
- protected override void InitializeBattery()
- {
- base.InitializeBattery();
-
- // Hitscan
- SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, ComponentStartup>(OnBatteryStartup);
- SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, ChargeChangedEvent>(OnBatteryChargeChange);
- SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, PowerCellChangedEvent>(OnPowerCellChanged);
-
- // Projectile
- SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, ComponentStartup>(OnBatteryStartup);
- SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, ChargeChangedEvent>(OnBatteryChargeChange);
- SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, PowerCellChangedEvent>(OnPowerCellChanged);
- }
-
- private void OnBatteryStartup<T>(Entity<T> entity, ref ComponentStartup args) where T : BatteryAmmoProviderComponent
- {
- UpdateShots(entity, entity.Comp);
- }
-
- private void OnBatteryChargeChange<T>(Entity<T> entity, ref ChargeChangedEvent args) where T : BatteryAmmoProviderComponent
- {
- UpdateShots(entity, entity.Comp, args.Charge, args.MaxCharge);
- }
-
- private void OnPowerCellChanged<T>(Entity<T> entity, ref PowerCellChangedEvent args) where T : BatteryAmmoProviderComponent
- {
- UpdateShots(entity, entity.Comp);
- }
-
- private void UpdateShots(EntityUid uid, BatteryAmmoProviderComponent component)
- {
- var ev = new GetChargeEvent();
- RaiseLocalEvent(uid, ref ev);
-
- UpdateShots(uid, component, ev.CurrentCharge, ev.MaxCharge);
- }
-
- private void UpdateShots(EntityUid uid, BatteryAmmoProviderComponent component, float charge, float maxCharge)
- {
- var shots = (int) (charge / component.FireCost);
- var maxShots = (int) (maxCharge / component.FireCost);
-
- if (component.Shots != shots || component.Capacity != maxShots)
- {
- Dirty(uid, component);
- }
-
- component.Shots = shots;
-
- if (maxShots > 0)
- component.Capacity = maxShots;
-
- UpdateBatteryAppearance(uid, component);
-
- var updateAmmoEv = new UpdateClientAmmoEvent();
- RaiseLocalEvent(uid, ref updateAmmoEv);
- }
-
- protected override void TakeCharge(Entity<BatteryAmmoProviderComponent> entity)
- {
- // Take charge from either the BatteryComponent or PowerCellSlotComponent.
- var ev = new ChangeChargeEvent(-entity.Comp.FireCost);
- RaiseLocalEvent(entity, ref ev);
- }
-}
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;
public sealed class XAEChargeBatterySystem : BaseXAESystem<XAEChargeBatteryComponent>
{
[Dependency] private readonly BatterySystem _battery = default!;
+ [Dependency] private readonly PredictedBatterySystem _predictedBattery = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
/// <summary> Pre-allocated and re-used collection.</summary>
private readonly HashSet<Entity<BatteryComponent>> _batteryEntities = new();
+ private readonly HashSet<Entity<PredictedBatteryComponent>> _pBatteryEntities = new();
/// <inheritdoc />
protected override void OnActivated(Entity<XAEChargeBatteryComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
- var chargeBatteryComponent = ent.Comp;
_batteryEntities.Clear();
- _lookup.GetEntitiesInRange(args.Coordinates, chargeBatteryComponent.Radius, _batteryEntities);
+ _pBatteryEntities.Clear();
+
+ _lookup.GetEntitiesInRange(args.Coordinates, ent.Comp.Radius, _batteryEntities);
foreach (var battery in _batteryEntities)
{
_battery.SetCharge(battery.AsNullable(), battery.Comp.MaxCharge);
}
+
+ _lookup.GetEntitiesInRange(args.Coordinates, ent.Comp.Radius, _pBatteryEntities);
+ foreach (var pBattery in _pBatteryEntities)
+ {
+ _predictedBattery.SetCharge(pBattery.AsNullable(), pBattery.Comp.MaxCharge);
+ }
}
}
--- /dev/null
+namespace Content.Shared.Kitchen;
+
+/// <summary>
+/// Raised on an entity when it is inside a microwave and it starts cooking.
+/// </summary>
+public sealed class BeingMicrowavedEvent(EntityUid microwave, EntityUid? user) : HandledEntityEventArgs
+{
+ public EntityUid Microwave = microwave;
+ public EntityUid? User = user;
+}
/// <summary>
/// Attempts to change the amount of energy in the mech.
+ /// TODO: Power cells are predicted now, so no need to duplicate the charge level
/// </summary>
/// <param name="uid">The mech itself</param>
/// <param name="delta">The change in energy</param>
/// <summary>
/// Raised when a battery's charge or capacity changes (capacity affects relative charge percentage).
+/// Only raised for entities with <see cref="BatteryComponent"/>.
/// </summary>
[ByRefEvent]
public readonly record struct ChargeChangedEvent(float Charge, float MaxCharge);
+/// <summary>
+/// Raised when a predicted battery's charge or capacity changes (capacity affects relative charge percentage).
+/// Unlike <see cref="ChargeChangedEvent"/> this is not raised repeatedly each time the charge changes, but only when the charge rate is changed
+/// or a charge amount was added or removed instantaneously. The current charge can be inferred from the time of the last update and the charge and
+/// charge rate at that time.
+/// Only raised for entities with <see cref="PredictedBatteryComponent"/>.
+/// </summary>
+[ByRefEvent]
+public readonly record struct PredictedBatteryChargeChangedEvent(float CurrentCharge, float CurrentChargeRate, TimeSpan CurrentTime, float MaxCharge);
+
+/// <summary>
+/// Raised when a battery changes its state between full, empty, or neither.
+/// Used only for <see cref="PredictedBatteryComponent"/>.
+/// </summary>
+[ByRefEvent]
+public record struct PredictedBatteryStateChangedEvent(BatteryState OldState, BatteryState NewState);
+
+/// <summary>
+/// Raised to calculate a predicted battery's recharge rate.
+/// Subscribe to this to offset its current charge rate.
+/// Used only for <see cref="PredictedBatteryComponent"/>.
+/// </summary>
+[ByRefEvent]
+public record struct RefreshChargeRateEvent(float MaxCharge)
+{
+ public readonly float MaxCharge = MaxCharge;
+ public float NewChargeRate;
+}
+
/// <summary>
/// Event that supports multiple battery types.
/// Raised when it is necessary to get information about battery charges.
-/// Works with either <see cref="BatteryComponent"/> or <see cref="PowerCellSlotComponent"/>.
+/// Works with either <see cref="BatteryComponent"/>, <see cref="PredictedBatteryComponent"/>, or <see cref="PowerCellSlotComponent"/>.
/// If there are multiple batteries then the results will be summed up.
/// </summary>
[ByRefEvent]
/// <summary>
/// Method event that supports multiple battery types.
/// Raised when it is necessary to change the current battery charge by some value.
-/// Works with either <see cref="BatteryComponent"/> or <see cref="PowerCellSlotComponent"/>.
+/// Works with either <see cref="BatteryComponent"/>, <see cref="PredictedBatteryComponent"/>, or <see cref="PowerCellSlotComponent"/>.
/// If there are multiple batteries then they will be changed in order of subscription until the total value was reached.
/// </summary>
[ByRefEvent]
/// <summary>
/// Battery node on the pow3r network. Needs other components to connect to actual networks.
+/// Use this for batteries that cannot be predicted.
+/// Use <see cref="PredictedBatteryComponent"/> otherwise.
/// </summary>
[RegisterComponent]
[Virtual]
/// <summary>
/// Self-recharging battery.
/// To be used in combination with <see cref="BatteryComponent"/>.
+/// For <see cref="PredictedBatteryComponent"/> use <see cref="PredictedBatterySelfRechargerComponent"/> instead.
/// </summary>
[RegisterComponent, AutoGenerateComponentPause]
public sealed partial class BatterySelfRechargerComponent : Component
public bool AutoRecharge = true;
/// <summary>
- /// At what rate does the entity automatically recharge?
+ /// At what rate does the entity automatically recharge? In watts.
/// </summary>
[DataField]
public float AutoRechargeRate;
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
namespace Content.Shared.Power.Components;
-[RegisterComponent, NetworkedComponent]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ChargerComponent : Component
{
- [ViewVariables]
- public CellChargerStatus Status;
-
/// <summary>
- /// The charge rate of the charger, in watts
+ /// The charge rate of the charger, in watts.
/// </summary>
- [DataField]
+ [DataField, AutoNetworkedField]
public float ChargeRate = 20.0f;
+ /// <summary>
+ /// Passive draw when no power cell is inserted, in watts.
+ /// This should be larger than 0 or the charger will be considered as powered even without a LV supply.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public float PassiveDraw = 1f;
+
/// <summary>
/// The container ID that is holds the entities being charged.
/// </summary>
/// <summary>
/// A whitelist for what entities can be charged by this Charger.
/// </summary>
- [DataField]
+ [DataField, AutoNetworkedField]
public EntityWhitelist? Whitelist;
/// <summary>
/// Indicates whether the charger is portable and thus subject to EMP effects
/// and bypasses checks for transform, anchored, and ApcPowerReceiverComponent.
/// </summary>
- [DataField]
+ [DataField, AutoNetworkedField]
public bool Portable = false;
}
+
+[Serializable, NetSerializable]
+public enum CellChargerStatus
+{
+ Off,
+ Empty,
+ Charging,
+ Charged,
+}
+
+[Serializable, NetSerializable]
+public enum CellVisual
+{
+ Occupied, // If there's an item in it
+ Light,
+}
--- /dev/null
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Power.Components;
+
+/// <summary>
+/// Allows the charge of a battery to be seen by examination.
+/// Works with either <see cref="BatteryComponent"/> or <see cref="PredictedBatteryComponent"/>.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class ExaminableBatteryComponent : Component;
--- /dev/null
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Power.Components;
+
+/// <summary>
+/// This entity is currently inside the charging slot of an entity with <see cref="ChargerComponent"/>.
+/// Added regardless whether or not the charger is powered.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class InsideChargerComponent : Component;
--- /dev/null
+using Content.Shared.Power.EntitySystems;
+using Content.Shared.Guidebook;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared.Power.Components;
+
+/// <summary>
+/// Predicted equivalent to <see cref="BatteryComponent"/>.
+/// Use this for electrical power storages that only have a constant charge rate or instantaneous power draw.
+/// Devices being directly charged by the power network do not fulfill that requirement as their power supply ramps up over time.
+/// </summary>
+/// <remarks>
+/// We cannot simply network <see cref="BatteryComponent"/> since it would get dirtied every single tick when it updates.
+/// This component solves this by requiring a constant charge rate and having the client infer the current charge from the rate
+/// and the timestamp the charge was last networked at. This can possibly be expanded in the future by adding a second time derivative.
+/// </remarks>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
+[Access(typeof(PredictedBatterySystem))]
+public sealed partial class PredictedBatteryComponent : Component
+{
+ /// <summary>
+ /// Maximum charge of the battery in joules (ie. watt seconds)
+ /// </summary>
+ [DataField, AutoNetworkedField, ViewVariables]
+ [GuidebookData]
+ public float MaxCharge;
+
+ /// <summary>
+ /// The price per one joule. Default is 1 speso for 10kJ.
+ /// </summary>
+ [DataField]
+ public float PricePerJoule = 0.0001f;
+
+ /// <summary>
+ /// Time stamp of the last networked update.
+ /// </summary>
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+ [AutoNetworkedField, AutoPausedField, ViewVariables]
+ public TimeSpan LastUpdate = TimeSpan.Zero;
+
+ /// <summary>
+ /// The intial charge to be set on map init.
+ /// </summary>
+ [DataField]
+ public float StartingCharge;
+
+ /// <summary>
+ /// The charge at the last update in joules (i.e. watt seconds).
+ /// </summary>
+ [DataField, AutoNetworkedField, ViewVariables]
+ public float LastCharge;
+
+ /// <summary>
+ /// The current charge rate in watt.
+ /// </summary>
+ /// <remarks>
+ /// Not a datafield as this is only cached and recalculated on component startup.
+ /// </remarks>
+ [ViewVariables, AutoNetworkedField]
+ public float ChargeRate;
+
+ /// <summary>
+ /// The current charge state of the battery.
+ /// Used to track state changes for raising <see cref="PredictedBatteryStateChangedEvent"/>.
+ /// </summary>
+ /// <remarks>
+ /// Not a datafield as this is only cached and recalculated in an update loop.
+ /// </remarks>
+ [ViewVariables, AutoNetworkedField]
+ public BatteryState State = BatteryState.Neither;
+}
+
+/// <summary>
+/// Charge level status of the battery.
+/// </summary>
+[Serializable, NetSerializable]
+public enum BatteryState : byte
+{
+ /// <summary>
+ /// Full charge.
+ /// </summary>
+ Full,
+ /// <summary>
+ /// No charge.
+ /// </summary>
+ Empty,
+ /// <summary>
+ /// Neither full nor empty.
+ /// </summary>
+ Neither,
+}
+
--- /dev/null
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared.Power.Components;
+
+/// <summary>
+/// Self-recharging battery.
+/// To be used in combination with <see cref="PredictedBatteryComponent"/>.
+/// For <see cref="BatteryComponent"/> use <see cref="BatterySelfRechargerComponent"/> instead.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+[AutoGenerateComponentState, AutoGenerateComponentPause]
+public sealed partial class PredictedBatterySelfRechargerComponent : Component
+{
+ /// <summary>
+ /// At what rate does the entity automatically recharge? In watts.
+ /// </summary>
+ [DataField, AutoNetworkedField, ViewVariables]
+ public float AutoRechargeRate;
+
+ /// <summary>
+ /// How long should the entity stop automatically recharging if a charge is used?
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public TimeSpan AutoRechargePauseTime = TimeSpan.Zero;
+
+ /// <summary>
+ /// Do not auto recharge if this timestamp has yet to happen, set for the auto recharge pause system.
+ /// </summary>
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+ [AutoNetworkedField, AutoPausedField, ViewVariables]
+ public TimeSpan? NextAutoRecharge = TimeSpan.FromSeconds(0);
+}
--- /dev/null
+using Content.Shared.PowerCell.Components;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Power.Components;
+
+/// <summary>
+/// Marker component that makes an entity with <see cref="PredictedBatteryComponent"/> update its appearance data for use with visualizers.
+/// Also works with an entity with <see cref="PowerCellSlotComponent"/> and will relay the state of the inserted powercell.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class PredictedBatteryVisualsComponent : Component;
+
+/// <summary>
+/// Keys for the appearance data.
+/// </summary>
+[Serializable, NetSerializable]
+public enum BatteryVisuals : byte
+{
+ /// <summary>
+ /// The current charge state of the battery.
+ /// Either full, empty, or neither.
+ /// Uses a <see cref="BatteryState"/>.
+ /// </summary>
+ State,
+ /// <summary>
+ /// Is the battery currently charging or discharging?
+ /// Uses a <see cref="BatteryChargingState"/>.
+ /// </summary>
+ Charging,
+}
+
+/// <summary>
+/// Charge level status of the battery.
+/// </summary>
+[Serializable, NetSerializable]
+public enum BatteryChargingState : byte
+{
+ /// <summary>
+ /// PredictedBatteryComponent.ChargeRate > 0
+ /// </summary>
+ Charging,
+ /// <summary>
+ /// PredictedBatteryComponent.ChargeRate < 0
+ /// </summary>
+ Decharging,
+ /// <summary>
+ /// PredictedBatteryComponent.ChargeRate == 0
+ /// </summary>
+ Constant,
+}
--- /dev/null
+using System.Diagnostics.CodeAnalysis;
+using Content.Shared.Emp;
+using Content.Shared.Examine;
+using Content.Shared.Power.Components;
+using Content.Shared.PowerCell;
+using Content.Shared.PowerCell.Components;
+using Content.Shared.Storage.Components;
+using Content.Shared.Whitelist;
+using Robust.Shared.Containers;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.Power.EntitySystems;
+
+public sealed class ChargerSystem : EntitySystem
+{
+ [Dependency] private readonly PredictedBatterySystem _battery = default!;
+ [Dependency] private readonly SharedPowerReceiverSystem _receiver = default!;
+ [Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
+ [Dependency] private readonly SharedContainerSystem _container = default!;
+ [Dependency] private readonly PowerCellSystem _powerCell = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<ChargerComponent, ComponentStartup>(OnStartup);
+ SubscribeLocalEvent<ChargerComponent, PowerChangedEvent>(OnPowerChanged);
+ SubscribeLocalEvent<ChargerComponent, EntInsertedIntoContainerMessage>(OnInserted);
+ SubscribeLocalEvent<ChargerComponent, EntRemovedFromContainerMessage>(OnRemoved);
+ SubscribeLocalEvent<ChargerComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
+ SubscribeLocalEvent<ChargerComponent, InsertIntoEntityStorageAttemptEvent>(OnEntityStorageInsertAttempt);
+ SubscribeLocalEvent<ChargerComponent, ExaminedEvent>(OnChargerExamine);
+ SubscribeLocalEvent<ChargerComponent, EmpPulseEvent>(OnEmpPulse);
+ SubscribeLocalEvent<ChargerComponent, EmpDisabledRemovedEvent>(OnEmpRemoved);
+ SubscribeLocalEvent<InsideChargerComponent, RefreshChargeRateEvent>(OnRefreshChargeRate);
+ SubscribeLocalEvent<InsideChargerComponent, PredictedBatteryStateChangedEvent>(OnStatusChanged);
+ }
+
+ private void OnStartup(Entity<ChargerComponent> ent, ref ComponentStartup args)
+ {
+ UpdateStatus(ent);
+ }
+
+ private void OnChargerExamine(EntityUid uid, ChargerComponent component, ExaminedEvent args)
+ {
+ using (args.PushGroup(nameof(ChargerComponent)))
+ {
+ // rate at which the charger charges
+ args.PushMarkup(Loc.GetString("charger-examine", ("color", "yellow"), ("chargeRate", (int)component.ChargeRate)));
+
+ // try to get contents of the charger
+ if (!_container.TryGetContainer(uid, component.SlotId, out var container))
+ return;
+
+ if (HasComp<PowerCellSlotComponent>(uid))
+ return;
+
+ // if charger is empty and not a power cell type charger, add empty message
+ // power cells have their own empty message by default, for things like flash lights
+ if (container.ContainedEntities.Count == 0)
+ {
+ args.PushMarkup(Loc.GetString("charger-empty"));
+ }
+ else
+ {
+ // add how much each item is charged it
+ foreach (var contained in container.ContainedEntities)
+ {
+ if (!SearchForBattery(contained, out var battery))
+ continue;
+
+ var chargePercentage = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge * 100;
+ args.PushMarkup(Loc.GetString("charger-content", ("chargePercentage", (int)chargePercentage)));
+ }
+ }
+ }
+ }
+
+ private void OnPowerChanged(Entity<ChargerComponent> ent, ref PowerChangedEvent args)
+ {
+ RefreshAllBatteries(ent);
+ UpdateStatus(ent);
+ }
+
+ private void OnInserted(Entity<ChargerComponent> ent, ref EntInsertedIntoContainerMessage args)
+ {
+ if (_timing.ApplyingState)
+ return; // Already networked in the same gamestate
+
+ if (args.Container.ID != ent.Comp.SlotId)
+ return;
+
+ AddComp<InsideChargerComponent>(args.Entity);
+ if (SearchForBattery(args.Entity, out var battery))
+ _battery.RefreshChargeRate(battery.Value.AsNullable());
+ UpdateStatus(ent);
+ }
+
+ private void OnRemoved(Entity<ChargerComponent> ent, ref EntRemovedFromContainerMessage args)
+ {
+ if (_timing.ApplyingState)
+ return; // Already networked in the same gamestate
+
+ if (args.Container.ID != ent.Comp.SlotId)
+ return;
+
+ RemComp<InsideChargerComponent>(args.Entity);
+ if (SearchForBattery(args.Entity, out var battery))
+ _battery.RefreshChargeRate(battery.Value.AsNullable());
+ UpdateStatus(ent);
+ }
+
+ /// <summary>
+ /// Verify that the entity being inserted is actually rechargeable.
+ /// </summary>
+ private void OnInsertAttempt(EntityUid uid, ChargerComponent component, ContainerIsInsertingAttemptEvent args)
+ {
+ if (!component.Initialized)
+ return;
+
+ if (args.Container.ID != component.SlotId)
+ return;
+
+ if (!TryComp<PowerCellSlotComponent>(args.EntityUid, out var cellSlot))
+ return;
+
+ if (!cellSlot.FitsInCharger)
+ args.Cancel();
+ }
+
+ private void OnEntityStorageInsertAttempt(EntityUid uid, ChargerComponent component, ref InsertIntoEntityStorageAttemptEvent args)
+ {
+ if (!component.Initialized || args.Cancelled)
+ return;
+
+ if (args.Container.ID != component.SlotId)
+ return;
+
+ if (!TryComp<PowerCellSlotComponent>(uid, out var cellSlot))
+ return;
+
+ if (!cellSlot.FitsInCharger)
+ args.Cancelled = true;
+ }
+ private void OnEmpPulse(Entity<ChargerComponent> ent, ref EmpPulseEvent args)
+ {
+ args.Affected = true;
+ args.Disabled = true;
+ RefreshAllBatteries(ent);
+ UpdateStatus(ent);
+ }
+
+ private void OnEmpRemoved(Entity<ChargerComponent> ent, ref EmpDisabledRemovedEvent args)
+ {
+ RefreshAllBatteries(ent);
+ UpdateStatus(ent);
+ }
+
+ private void OnRefreshChargeRate(Entity<InsideChargerComponent> ent, ref RefreshChargeRateEvent args)
+ {
+ var chargerUid = Transform(ent).ParentUid;
+
+ if (HasComp<EmpDisabledComponent>(chargerUid))
+ return;
+
+ if (!TryComp<ChargerComponent>(chargerUid, out var chargerComp))
+ return;
+
+ if (!chargerComp.Portable && !_receiver.IsPowered(chargerUid))
+ return;
+
+ if (_whitelist.IsWhitelistFail(chargerComp.Whitelist, ent.Owner))
+ return;
+
+ args.NewChargeRate += chargerComp.ChargeRate;
+ }
+ private void OnStatusChanged(Entity<InsideChargerComponent> ent, ref PredictedBatteryStateChangedEvent args)
+ {
+ // If the battery is full update the visuals and power draw of the charger.
+
+ var chargerUid = Transform(ent).ParentUid;
+ if (!TryComp<ChargerComponent>(chargerUid, out var chargerComp))
+ return;
+
+ UpdateStatus((chargerUid, chargerComp));
+ }
+
+ private bool SearchForBattery(EntityUid uid, [NotNullWhen(true)] out Entity<PredictedBatteryComponent>? battery)
+ {
+ // try get a battery directly on the inserted entity
+ if (TryComp<PredictedBatteryComponent>(uid, out var batteryComp))
+ {
+ battery = (uid, batteryComp);
+ return true;
+ }
+ // or by checking for a power cell slot on the inserted entity
+ if (_powerCell.TryGetBatteryFromSlot(uid, out battery))
+ return true;
+
+ battery = null;
+ return false;
+ }
+
+ private void RefreshAllBatteries(Entity<ChargerComponent> ent)
+ {
+ // try to get contents of the charger
+ if (!_container.TryGetContainer(ent.Owner, ent.Comp.SlotId, out var container))
+ return;
+
+ foreach (var item in container.ContainedEntities)
+ {
+ if (SearchForBattery(item, out var battery))
+ _battery.RefreshChargeRate(battery.Value.AsNullable());
+ }
+ }
+
+ private void UpdateStatus(Entity<ChargerComponent> ent)
+ {
+ TryComp<AppearanceComponent>(ent, out var appearance);
+
+ if (!_container.TryGetContainer(ent.Owner, ent.Comp.SlotId, out var container))
+ return;
+
+ _appearance.SetData(ent.Owner, CellVisual.Occupied, container.ContainedEntities.Count != 0, appearance);
+
+ var status = GetStatus(ent);
+ switch (status)
+ {
+ case CellChargerStatus.Charging:
+ // TODO: If someone ever adds chargers that can charge multiple batteries at once then set this to the total draw rate.
+ _receiver.SetLoad(ent.Owner, ent.Comp.ChargeRate);
+ break;
+ default:
+ // Don't set the load to 0 or the charger will be considered as powered even if the LV connection is unpowered.
+ // TODO: Fix this on an ApcPowerReceiver level.
+ _receiver.SetLoad(ent.Owner, ent.Comp.PassiveDraw);
+ break;
+ }
+ _appearance.SetData(ent.Owner, CellVisual.Light, status, appearance);
+ }
+
+ private CellChargerStatus GetStatus(Entity<ChargerComponent> ent)
+ {
+ if (!ent.Comp.Portable && !Transform(ent).Anchored)
+ return CellChargerStatus.Off;
+
+ if (!ent.Comp.Portable && !_receiver.IsPowered(ent.Owner))
+ return CellChargerStatus.Off;
+
+ if (HasComp<EmpDisabledComponent>(ent))
+ return CellChargerStatus.Off;
+
+ if (!_container.TryGetContainer(ent.Owner, ent.Comp.SlotId, out var container))
+ return CellChargerStatus.Off;
+
+ if (container.ContainedEntities.Count == 0)
+ return CellChargerStatus.Empty;
+
+ // Use the first stored battery for visuals. If someone ever makes a multi-slot charger then this will need to be changed.
+ if (!SearchForBattery(container.ContainedEntities[0], out var battery))
+ return CellChargerStatus.Off;
+
+ if (_battery.IsFull(battery.Value.AsNullable()))
+ return CellChargerStatus.Charged;
+
+ return CellChargerStatus.Charging;
+ }
+}
--- /dev/null
+using Content.Shared.Power.Components;
+using JetBrains.Annotations;
+
+namespace Content.Shared.Power.EntitySystems;
+
+/// <summary>
+/// Responsible for <see cref="PredictedBatteryComponent"/>.
+/// Predicted equivalent of <see cref="Content.Server.Power.EntitySystems.BatterySystem"/>.
+/// If you make changes to this make sure to keep the two consistent.
+/// </summary>
+public sealed partial class PredictedBatterySystem
+{
+ /// <summary>
+ /// Changes the battery's charge by the given amount
+ /// and resets the self-recharge cooldown if it exists.
+ /// A positive value will add charge, a negative value will remove charge.
+ /// </summary>
+ /// <returns>The actually changed amount.</returns>
+ [PublicAPI]
+ public float ChangeCharge(Entity<PredictedBatteryComponent?> ent, float amount)
+ {
+ if (!Resolve(ent, ref ent.Comp))
+ return 0;
+
+ var oldValue = GetCharge(ent);
+ var newValue = Math.Clamp(oldValue + amount, 0, ent.Comp.MaxCharge);
+ var delta = newValue - oldValue;
+
+ if (delta == 0f)
+ return 0f;
+
+ var curTime = _timing.CurTime;
+ ent.Comp.LastCharge = newValue;
+ ent.Comp.LastUpdate = curTime;
+ Dirty(ent);
+
+ TrySetChargeCooldown(ent.Owner);
+
+ var changedEv = new PredictedBatteryChargeChangedEvent(newValue, ent.Comp.ChargeRate, curTime, ent.Comp.MaxCharge);
+ RaiseLocalEvent(ent, ref changedEv);
+
+ // Raise events if the battery status changed between full, empty, or neither.
+ UpdateState(ent);
+ return delta;
+ }
+
+ /// <summary>
+ /// Removes the given amount of charge from the battery
+ /// and resets the self-recharge cooldown if it exists.
+ /// </summary>
+ /// <returns>The actually changed amount.</returns>
+ [PublicAPI]
+ public float UseCharge(Entity<PredictedBatteryComponent?> ent, float amount)
+ {
+ if (amount <= 0f)
+ return 0f;
+
+ return ChangeCharge(ent, -amount);
+ }
+
+ /// <summary>
+ /// If sufficient charge is available on the battery, use it. Otherwise, don't.
+ /// Resets the self-recharge cooldown if it exists.
+ /// Always returns false on the client.
+ /// </summary>
+ /// <returns>If the full amount was able to be removed.</returns>
+ [PublicAPI]
+ public bool TryUseCharge(Entity<PredictedBatteryComponent?> ent, float amount)
+ {
+ if (!Resolve(ent, ref ent.Comp, false) || amount > GetCharge(ent))
+ return false;
+
+ UseCharge(ent, amount);
+ return true;
+ }
+
+ /// <summary>
+ /// Sets the battery's charge.
+ /// </summary>
+ [PublicAPI]
+ public void SetCharge(Entity<PredictedBatteryComponent?> ent, float value)
+ {
+ if (!Resolve(ent, ref ent.Comp))
+ return;
+
+ var oldValue = GetCharge(ent);
+ var newValue = Math.Clamp(value, 0, ent.Comp.MaxCharge);
+ var delta = newValue - oldValue;
+
+ if (delta == 0f)
+ return;
+
+ var curTime = _timing.CurTime;
+ ent.Comp.LastCharge = newValue;
+ ent.Comp.LastUpdate = curTime;
+ Dirty(ent);
+
+ var ev = new PredictedBatteryChargeChangedEvent(newValue, ent.Comp.ChargeRate, curTime, ent.Comp.MaxCharge);
+ RaiseLocalEvent(ent, ref ev);
+
+ // Raise events if the battery status changed between full, empty, or neither.
+ UpdateState(ent);
+ }
+
+ /// <summary>
+ /// Sets the battery's maximum charge.
+ /// </summary>
+ [PublicAPI]
+ public void SetMaxCharge(Entity<PredictedBatteryComponent?> ent, float value)
+ {
+ if (!Resolve(ent, ref ent.Comp))
+ return;
+
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ if (value == ent.Comp.MaxCharge)
+ return;
+
+ ent.Comp.MaxCharge = Math.Max(value, 0);
+ ent.Comp.LastCharge = GetCharge(ent); // This clamps it using the new max.
+ var curTime = _timing.CurTime;
+ ent.Comp.LastUpdate = curTime;
+ Dirty(ent);
+
+ var ev = new PredictedBatteryChargeChangedEvent(ent.Comp.LastCharge, ent.Comp.ChargeRate, curTime, ent.Comp.MaxCharge);
+ RaiseLocalEvent(ent, ref ev);
+
+ // Raise events if the battery status changed between full, empty, or neither.
+ UpdateState(ent);
+ }
+
+ /// <summary>
+ /// Updates the battery's charge state and sends an event if it changed.
+ /// </summary>
+ [PublicAPI]
+ public void UpdateState(Entity<PredictedBatteryComponent?> ent)
+ {
+ if (!Resolve(ent, ref ent.Comp))
+ return;
+
+ var oldState = ent.Comp.State;
+
+ var newState = BatteryState.Neither;
+
+ var charge = GetCharge(ent);
+
+ if (charge == ent.Comp.MaxCharge)
+ newState = BatteryState.Full;
+ else if (charge == 0f)
+ newState = BatteryState.Empty;
+
+ if (oldState == newState)
+ return;
+
+ ent.Comp.State = newState;
+ Dirty(ent);
+
+ var changedEv = new PredictedBatteryStateChangedEvent(oldState, newState);
+ RaiseLocalEvent(ent, ref changedEv);
+ }
+
+ /// <summary>
+ /// Gets the battery's current charge.
+ /// </summary>
+ [PublicAPI]
+ public float GetCharge(Entity<PredictedBatteryComponent?> ent)
+ {
+ if (!Resolve(ent, ref ent.Comp, false))
+ return 0f;
+
+ var curTime = _timing.CurTime;
+ // We have a constant charge rate, so the charge changes linearly over time.
+ var dt = (curTime - ent.Comp.LastUpdate).TotalSeconds;
+ var charge = Math.Clamp(ent.Comp.LastCharge + (float)(dt * ent.Comp.ChargeRate), 0f, ent.Comp.MaxCharge);
+ return charge;
+ }
+
+ /// <summary>
+ /// Gets number of remaining uses for the given charge cost.
+ /// </summary>
+ [PublicAPI]
+ public int GetRemainingUses(Entity<PredictedBatteryComponent?> ent, float cost)
+ {
+ if (cost <= 0)
+ return 0;
+
+ if (!Resolve(ent, ref ent.Comp))
+ return 0;
+
+ return (int)(GetCharge(ent) / cost);
+ }
+
+ /// <summary>
+ /// Gets number of maximum uses at full charge for the given charge cost.
+ /// </summary>
+ [PublicAPI]
+ public int GetMaxUses(Entity<PredictedBatteryComponent?> ent, float cost)
+ {
+ if (cost <= 0)
+ return 0;
+
+ if (!Resolve(ent, ref ent.Comp))
+ return 0;
+
+ return (int)(ent.Comp.MaxCharge / cost);
+ }
+
+
+ /// <summary>
+ /// Refreshes the battery's current charge rate by raising a <see cref="RefreshChargeRateEvent"/>.
+ /// </summary>
+ /// <returns>The new charge rate.</returns>
+ [PublicAPI]
+ public float RefreshChargeRate(Entity<PredictedBatteryComponent?> ent)
+ {
+ if (!Resolve(ent, ref ent.Comp, false))
+ return 0f;
+
+ ent.Comp.LastCharge = GetCharge(ent); // Prevent the new rate from modifying the current charge.
+ var curTime = _timing.CurTime;
+ ent.Comp.LastUpdate = curTime;
+
+ var refreshEv = new RefreshChargeRateEvent(ent.Comp.MaxCharge);
+ RaiseLocalEvent(ent, ref refreshEv);
+ ent.Comp.ChargeRate = refreshEv.NewChargeRate;
+ Dirty(ent);
+
+ // Inform other systems about the new rate;
+ var changedEv = new PredictedBatteryChargeChangedEvent(ent.Comp.LastCharge, ent.Comp.ChargeRate, curTime, ent.Comp.MaxCharge);
+ RaiseLocalEvent(ent, ref changedEv);
+
+ return refreshEv.NewChargeRate;
+ }
+
+ /// <summary>
+ /// Checks if the entity has a self recharge and puts it on cooldown if applicable.
+ /// Uses the cooldown time given in the component.
+ /// </summary>
+ [PublicAPI]
+ public void TrySetChargeCooldown(Entity<PredictedBatterySelfRechargerComponent?> ent)
+ {
+ if (!Resolve(ent, ref ent.Comp, false))
+ return;
+
+ if (ent.Comp.AutoRechargePauseTime == TimeSpan.Zero)
+ return; // no recharge pause
+
+ if (_timing.CurTime + ent.Comp.AutoRechargePauseTime <= ent.Comp.NextAutoRecharge)
+ return; // the current pause is already longer
+
+ SetChargeCooldown(ent, ent.Comp.AutoRechargePauseTime);
+ }
+
+ /// <summary>
+ /// Puts the entity's self recharge on cooldown for the specified time.
+ /// </summary>
+ [PublicAPI]
+ public void SetChargeCooldown(Entity<PredictedBatterySelfRechargerComponent?> ent, TimeSpan cooldown)
+ {
+ if (!Resolve(ent, ref ent.Comp))
+ return;
+
+ ent.Comp.NextAutoRecharge = _timing.CurTime + cooldown;
+ Dirty(ent);
+ RefreshChargeRate(ent.Owner); // Apply the new charge rate.
+ }
+
+ /// <summary>
+ /// Returns whether the battery is full.
+ /// </summary>
+ [PublicAPI]
+ public bool IsFull(Entity<PredictedBatteryComponent?> ent)
+ {
+ if (!Resolve(ent, ref ent.Comp))
+ return false;
+
+ return GetCharge(ent) >= ent.Comp.MaxCharge;
+ }
+}
--- /dev/null
+using Content.Shared.Cargo;
+using Content.Shared.Emp;
+using Content.Shared.Examine;
+using Content.Shared.Power.Components;
+using Content.Shared.Rejuvenate;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Power.EntitySystems;
+
+/// <summary>
+/// Responsible for <see cref="PredictedBatteryComponent"/>.
+/// Predicted equivalent of <see cref="Content.Server.Power.EntitySystems.BatterySystem"/>.
+/// If you make changes to this make sure to keep the two consistent.
+/// </summary>
+public sealed partial class PredictedBatterySystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<PredictedBatteryComponent, ComponentInit>(OnInit);
+ SubscribeLocalEvent<PredictedBatteryComponent, ComponentStartup>(OnStartup);
+ SubscribeLocalEvent<PredictedBatteryComponent, MapInitEvent>(OnMapInit);
+ SubscribeLocalEvent<PredictedBatteryComponent, EmpPulseEvent>(OnEmpPulse);
+ SubscribeLocalEvent<PredictedBatteryComponent, RejuvenateEvent>(OnRejuvenate);
+ SubscribeLocalEvent<PredictedBatteryComponent, ExaminedEvent>(OnExamine);
+ SubscribeLocalEvent<PredictedBatteryComponent, PriceCalculationEvent>(CalculateBatteryPrice);
+ SubscribeLocalEvent<PredictedBatteryComponent, ChangeChargeEvent>(OnChangeCharge);
+ SubscribeLocalEvent<PredictedBatteryComponent, GetChargeEvent>(OnGetCharge);
+ SubscribeLocalEvent<PredictedBatterySelfRechargerComponent, RefreshChargeRateEvent>(OnRefreshChargeRate);
+ SubscribeLocalEvent<PredictedBatterySelfRechargerComponent, ComponentStartup>(OnRechargerStartup);
+ SubscribeLocalEvent<PredictedBatterySelfRechargerComponent, ComponentRemove>(OnRechargerRemove);
+ SubscribeLocalEvent<PredictedBatteryVisualsComponent, PredictedBatteryChargeChangedEvent>(OnVisualsChargeChanged);
+ SubscribeLocalEvent<PredictedBatteryVisualsComponent, PredictedBatteryStateChangedEvent>(OnVisualsStateChanged);
+ }
+
+ private void OnInit(Entity<PredictedBatteryComponent> ent, ref ComponentInit args)
+ {
+ DebugTools.Assert(!HasComp<BatteryComponent>(ent), $"{ent} has both BatteryComponent and PredictedBatteryComponent");
+ }
+
+ private void OnStartup(Entity<PredictedBatteryComponent> ent, ref ComponentStartup args)
+ {
+ // In case a recharging component was added before the battery component itself.
+ // Doing this only on map init is not enough because the charge rate is not a datafield, but cached, so it would get lost when reloading the game.
+ // If we would make it a datafield then the integration tests would complain about modifying it before map init.
+ RefreshChargeRate(ent.AsNullable());
+ }
+
+ private void OnMapInit(Entity<PredictedBatteryComponent> ent, ref MapInitEvent args)
+ {
+ SetCharge(ent.AsNullable(), ent.Comp.StartingCharge);
+ RefreshChargeRate(ent.AsNullable());
+ }
+
+ private void OnRejuvenate(Entity<PredictedBatteryComponent> ent, ref RejuvenateEvent args)
+ {
+ SetCharge(ent.AsNullable(), ent.Comp.MaxCharge);
+ }
+
+ private void OnEmpPulse(Entity<PredictedBatteryComponent> ent, ref EmpPulseEvent args)
+ {
+ args.Affected = true;
+ UseCharge(ent.AsNullable(), args.EnergyConsumption);
+ }
+
+ private void OnExamine(Entity<PredictedBatteryComponent> ent, ref ExaminedEvent args)
+ {
+ if (!args.IsInDetailsRange)
+ return;
+
+ if (!HasComp<ExaminableBatteryComponent>(ent))
+ return;
+
+ var chargePercentRounded = 0;
+ var currentCharge = GetCharge(ent.AsNullable());
+ if (ent.Comp.MaxCharge != 0)
+ chargePercentRounded = (int)(100 * currentCharge / ent.Comp.MaxCharge);
+ args.PushMarkup(
+ Loc.GetString(
+ "examinable-battery-component-examine-detail",
+ ("percent", chargePercentRounded),
+ ("markupPercentColor", "green")
+ )
+ );
+ }
+
+ /// <summary>
+ /// Gets the price for the power contained in an entity's battery.
+ /// </summary>
+ private void CalculateBatteryPrice(Entity<PredictedBatteryComponent> ent, ref PriceCalculationEvent args)
+ {
+ args.Price += GetCharge(ent.AsNullable()) * ent.Comp.PricePerJoule;
+ }
+
+ private void OnChangeCharge(Entity<PredictedBatteryComponent> ent, ref ChangeChargeEvent args)
+ {
+ if (args.ResidualValue == 0)
+ return;
+
+ args.ResidualValue -= ChangeCharge(ent.AsNullable(), args.ResidualValue);
+ }
+
+ private void OnGetCharge(Entity<PredictedBatteryComponent> ent, ref GetChargeEvent args)
+ {
+ args.CurrentCharge += GetCharge(ent.AsNullable());
+ args.MaxCharge += ent.Comp.MaxCharge;
+ }
+
+ private void OnRefreshChargeRate(Entity<PredictedBatterySelfRechargerComponent> ent, ref RefreshChargeRateEvent args)
+ {
+ if (_timing.CurTime < ent.Comp.NextAutoRecharge)
+ return; // Still on cooldown
+
+ args.NewChargeRate += ent.Comp.AutoRechargeRate;
+ }
+
+ public override void Update(float frameTime)
+ {
+ var curTime = _timing.CurTime;
+
+ // Update self-recharging cooldowns.
+ var rechargerQuery = EntityQueryEnumerator<PredictedBatterySelfRechargerComponent, PredictedBatteryComponent>();
+ while (rechargerQuery.MoveNext(out var uid, out var recharger, out var battery))
+ {
+ if (recharger.NextAutoRecharge == null || curTime < recharger.NextAutoRecharge)
+ continue;
+
+ recharger.NextAutoRecharge = null; // Don't refresh every tick.
+ Dirty(uid, recharger);
+ RefreshChargeRate((uid, battery)); // Cooldown is over, apply the new recharge rate.
+ }
+
+ // Raise events when the battery is full or empty so that other systems can react and visuals can get updated.
+ // This is not doing that many calculations, it only has to get the current charge and only raises events if something did change.
+ // If this turns out to be too expensive and shows up on grafana consider updating it less often.
+ var batteryQuery = EntityQueryEnumerator<PredictedBatteryComponent>();
+ while (batteryQuery.MoveNext(out var uid, out var battery))
+ {
+ if (battery.ChargeRate == 0f)
+ continue; // No need to check if it's constant.
+
+ UpdateState((uid, battery));
+ }
+ }
+
+ private void OnRechargerStartup(Entity<PredictedBatterySelfRechargerComponent> ent, ref ComponentStartup args)
+ {
+ // In case this component is added after the battery component.
+ RefreshChargeRate(ent.Owner);
+ }
+
+ private void OnRechargerRemove(Entity<PredictedBatterySelfRechargerComponent> ent, ref ComponentRemove args)
+ {
+ // We use ComponentRemove to make sure this component no longer subscribes to the refresh event.
+ RefreshChargeRate(ent.Owner);
+ }
+
+ private void OnVisualsChargeChanged(Entity<PredictedBatteryVisualsComponent> ent, ref PredictedBatteryChargeChangedEvent args)
+ {
+ // Update the appearance data for the charge rate.
+ // We have a separate component for this to not duplicate the networking cost unless we actually use it.
+ var state = BatteryChargingState.Constant;
+ if (args.CurrentChargeRate > 0f)
+ state = BatteryChargingState.Charging;
+ else if (args.CurrentChargeRate < 0f)
+ state = BatteryChargingState.Decharging;
+
+ _appearance.SetData(ent.Owner, BatteryVisuals.Charging, state);
+ }
+
+ private void OnVisualsStateChanged(Entity<PredictedBatteryVisualsComponent> ent, ref PredictedBatteryStateChangedEvent args)
+ {
+ // Update the appearance data for the fill level (empty, full, in-between).
+ // We have a separate component for this to not duplicate the networking cost unless we actually use it.
+ _appearance.SetData(ent.Owner, BatteryVisuals.State, args.NewState);
+ }
+}
using Content.Shared.Emp;
using Content.Shared.Power.Components;
+using JetBrains.Annotations;
namespace Content.Shared.Power.EntitySystems;
}
/// <summary>
- /// Changes the battery's charge by the given amount.
+ /// Changes the battery's charge by the given amount
+ /// and resets the self-recharge cooldown if it exists.
/// A positive value will add charge, a negative value will remove charge.
/// </summary>
/// <returns>The actually changed amount.</returns>
+ [PublicAPI]
public virtual float ChangeCharge(Entity<BatteryComponent?> ent, float amount)
{
return 0f;
}
/// <summary>
- /// Removes the given amount of charge from the battery.
+ /// Removes the given amount of charge from the battery
+ /// and resets the self-recharge cooldown if it exists.
/// </summary>
/// <returns>The actually changed amount.</returns>
+ [PublicAPI]
public virtual float UseCharge(Entity<BatteryComponent?> ent, float amount)
{
return 0f;
/// <summary>
/// If sufficient charge is available on the battery, use it. Otherwise, don't.
+ /// Resets the self-recharge cooldown if it exists.
/// Always returns false on the client.
/// </summary>
/// <returns>If the full amount was able to be removed.</returns>
+ [PublicAPI]
public virtual bool TryUseCharge(Entity<BatteryComponent?> ent, float amount)
{
return false;
/// <summary>
/// Sets the battery's charge.
/// </summary>
+ [PublicAPI]
public virtual void SetCharge(Entity<BatteryComponent?> ent, float value) { }
/// <summary>
/// Sets the battery's maximum charge.
/// </summary>
+ [PublicAPI]
public virtual void SetMaxCharge(Entity<BatteryComponent?> ent, float value) { }
/// <summary>
/// Checks if the entity has a self recharge and puts it on cooldown if applicable.
/// Uses the cooldown time given in the component.
/// </summary>
+ [PublicAPI]
public virtual void TrySetChargeCooldown(Entity<BatterySelfRechargerComponent?> ent) { }
/// <summary>
/// Puts the entity's self recharge on cooldown for the specified time.
/// </summary>
+ [PublicAPI]
public virtual void SetChargeCooldown(Entity<BatterySelfRechargerComponent?> ent, TimeSpan cooldown) { }
}
+++ /dev/null
-using Content.Shared.Emp;
-using Content.Shared.Power.Components;
-
-namespace Content.Shared.Power.EntitySystems;
-
-public abstract class SharedChargerSystem : EntitySystem
-{
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent<ChargerComponent, EmpPulseEvent>(OnEmpPulse);
- }
-
- private void OnEmpPulse(EntityUid uid, ChargerComponent component, ref EmpPulseEvent args)
- {
- args.Affected = true;
- args.Disabled = true;
- }
-}
// NOOP on server because client has 0 idea of load so we can't raise it properly in shared.
}
- /// <summary>
- /// Checks if entity is APC-powered device, and if it have power.
+ /// <summary>
+ /// Sets the power load of this power receiver.
+ /// </summary>
+ public void SetLoad(Entity<SharedApcPowerReceiverComponent?> entity, float load)
+ {
+ if (!ResolveApc(entity.Owner, ref entity.Comp))
+ return;
+
+ entity.Comp.Load = load;
+ }
+
+ /// <summary>
+ /// Checks if entity is APC-powered device, and if it have power.
/// </summary>
public bool IsPowered(Entity<SharedApcPowerReceiverComponent?> entity)
{
+++ /dev/null
-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,
- }
-}
+using Content.Shared.Power.Components;
using Robust.Shared.GameStates;
-using Robust.Shared.Serialization;
-namespace Content.Shared.PowerCell;
+namespace Content.Shared.PowerCell.Components;
/// <summary>
-/// This component enables power-cell related interactions (e.g., entity white-lists, cell sizes, examine, rigging).
-/// The actual power functionality is provided by the server-side BatteryComponent.
+/// This component enables power-cell related interactions (e.g. EntityWhitelists, cell sizes, examine, rigging).
+/// The actual power functionality is provided by the <see cref="PredictedBatteryComponent"/>.
/// </summary>
-[NetworkedComponent]
-[RegisterComponent]
-public sealed partial class PowerCellComponent : Component
-{
- public const int PowerCellVisualsLevels = 2;
-}
-
-[Serializable, NetSerializable]
-public enum PowerCellVisuals : byte
-{
- ChargeLevel
-}
-[Serializable, NetSerializable]
-public enum PowerCellSlotVisuals : byte
-{
- Enabled
-}
+[RegisterComponent, NetworkedComponent]
+public sealed partial class PowerCellComponent : Component;
--- /dev/null
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.PowerCell.Components;
+
+/// <summary>
+/// Indicates that the entity's ActivatableUI requires power or else it closes.
+/// </summary>
+/// <remarks>
+/// With ActivatableUI it will activate and deactivate when the ui is opened and closed, drawing power inbetween.
+/// </remarks>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(PowerCellSystem))]
+public sealed partial class PowerCellDrawComponent : Component
+{
+ /// <summary>
+ /// Whether drawing is enabled.
+ /// Having no cell will still disable it.
+ /// </summary>
+ [DataField, AutoNetworkedField, ViewVariables]
+ public bool Enabled = true;
+
+ /// <summary>
+ /// How much the entity draws while the UI is open (in Watts).
+ /// Set to 0 if you just wish to check for power upon opening the UI.
+ /// </summary>
+ [DataField, AutoNetworkedField, ViewVariables]
+ public float DrawRate = 1f;
+
+ /// <summary>
+ /// How much power is used whenever the entity is "used" (in Joules).
+ /// This is used to ensure the UI won't open again without a minimum use power.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public float UseCharge;
+}
using Content.Shared.Containers.ItemSlots;
+using Robust.Shared.GameStates;
namespace Content.Shared.PowerCell.Components;
-[RegisterComponent]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class PowerCellSlotComponent : Component
{
/// <summary>
/// </summary>
/// <remarks>
/// Given that <see cref="PowerCellSystem"/> needs to verify that a given cell has the correct cell-size before
- /// inserting anyways, there is no need to specify a separate entity whitelist. In this slot's yaml definition.
+ /// inserting anyways, there is no need to specify a separate entity whitelist in this slot's yaml definition.
/// </remarks>
- [DataField("cellSlotId", required: true)]
+ [DataField(required: true)]
public string CellSlotId = string.Empty;
/// <summary>
/// Can this entity be inserted directly into a charging station? If false, you need to manually remove the power
/// cell and recharge it separately.
/// </summary>
- [DataField("fitsInCharger")]
+ [DataField, AutoNetworkedField]
public bool FitsInCharger = true;
}
-/// <summary>
-/// Raised directed at an entity with a power cell slot when the power cell inside has its charge updated or is ejected/inserted.
-/// </summary>
-public sealed class PowerCellChangedEvent : EntityEventArgs
-{
- public readonly bool Ejected;
-
- public PowerCellChangedEvent(bool ejected)
- {
- Ejected = ejected;
- }
-}
+++ /dev/null
-using Content.Shared.Item.ItemToggle.Components;
-using Robust.Shared.GameStates;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
-
-namespace Content.Shared.PowerCell;
-
-/// <summary>
-/// Indicates that the entity's ActivatableUI requires power or else it closes.
-/// </summary>
-/// <remarks>
-/// With ActivatableUI it will activate and deactivate when the ui is opened and closed, drawing power inbetween.
-/// </remarks>
-[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
-public sealed partial class PowerCellDrawComponent : Component
-{
- #region Prediction
-
- /// <summary>
- /// Whether there is any charge available to draw.
- /// </summary>
- [DataField, AutoNetworkedField]
- public bool CanDraw;
-
- /// <summary>
- /// Whether there is sufficient charge to use.
- /// </summary>
- [DataField, AutoNetworkedField]
- public bool CanUse;
-
- #endregion
-
- /// <summary>
- /// Whether drawing is enabled.
- /// Having no cell will still disable it.
- /// </summary>
- [DataField, AutoNetworkedField]
- public bool Enabled = true;
-
- /// <summary>
- /// How much the entity draws while the UI is open (in Watts).
- /// Set to 0 if you just wish to check for power upon opening the UI.
- /// </summary>
- [DataField]
- public float DrawRate = 1f;
-
- /// <summary>
- /// How much power is used whenever the entity is "used" (in Joules).
- /// This is used to ensure the UI won't open again without a minimum use power.
- /// </summary>
- /// <remarks>
- /// This is not a rate how the datafield name implies, but a one-time cost.
- /// </remarks>
- [DataField]
- public float UseRate;
-
- /// <summary>
- /// When the next automatic power draw will occur
- /// </summary>
- [DataField("nextUpdate", customTypeSerializer: typeof(TimeOffsetSerializer))]
- [AutoPausedField]
- public TimeSpan NextUpdateTime;
-
- /// <summary>
- /// How long to wait between power drawing.
- /// </summary>
- [DataField]
- public TimeSpan Delay = TimeSpan.FromSeconds(1);
-}
/// </summary>
[ByRefEvent]
public readonly record struct PowerCellSlotEmptyEvent;
+
+/// <summary>
+/// Raised directed at an entity with a power cell slot when a power cell is ejected/inserted.
+/// </summary>
+[ByRefEvent]
+public record struct PowerCellChangedEvent(bool Ejected);
--- /dev/null
+using System.Diagnostics.CodeAnalysis;
+using Content.Shared.Containers.ItemSlots;
+using Content.Shared.Power.Components;
+using Content.Shared.PowerCell.Components;
+using JetBrains.Annotations;
+
+namespace Content.Shared.PowerCell;
+
+public sealed partial class PowerCellSystem
+{
+ /// <summary>
+ /// Gets the power cell battery inside a power cell slot.
+ /// </summary>
+ [PublicAPI]
+ public bool TryGetBatteryFromSlot(
+ Entity<PowerCellSlotComponent?> ent,
+ [NotNullWhen(true)] out Entity<PredictedBatteryComponent>? battery)
+ {
+ if (!Resolve(ent, ref ent.Comp, false))
+ {
+ battery = null;
+ return false;
+ }
+
+ if (!_itemSlots.TryGetSlot(ent.Owner, ent.Comp.CellSlotId, out ItemSlot? slot))
+ {
+ battery = null;
+ return false;
+ }
+
+ if (!TryComp<PredictedBatteryComponent>(slot.Item, out var batteryComp))
+ {
+ battery = null;
+ return false;
+ }
+
+ battery = (slot.Item.Value, batteryComp);
+ return true;
+ }
+
+ /// <summary>
+ /// Returns whether the entity has a slotted battery and charge for the requested action.
+ /// </summary>
+ /// <param name="ent">The power cell.</param>
+ /// <param name="charge">The charge that is needed.</param>
+ /// <param name="user">Show a popup to this user with the relevant details if specified.</param>
+ /// <param name="predicted">Whether to predict the popup or not.</param>
+ [PublicAPI]
+ public bool HasCharge(Entity<PowerCellSlotComponent?> ent, float charge, EntityUid? user = null, bool predicted = false)
+ {
+ if (!TryGetBatteryFromSlot(ent, out var battery))
+ {
+ if (user == null)
+ return false;
+
+ if (predicted)
+ _popup.PopupClient(Loc.GetString("power-cell-no-battery"), ent.Owner, user.Value);
+ else
+ _popup.PopupEntity(Loc.GetString("power-cell-no-battery"), ent.Owner, user.Value);
+
+ return false;
+ }
+
+ if (_battery.GetCharge(battery.Value.AsNullable()) < charge)
+ {
+ if (user == null)
+ return false;
+
+ if (predicted)
+ _popup.PopupClient(Loc.GetString("power-cell-insufficient"), ent.Owner, user.Value);
+ else
+ _popup.PopupEntity(Loc.GetString("power-cell-insufficient"), ent.Owner, user.Value);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Tries to use charge from a slotted battery.
+ /// </summary>
+ /// <param name="ent">The power cell.</param>
+ /// <param name="charge">The charge that is needed.</param>
+ /// <param name="user">Show a popup to this user with the relevant details if specified.</param>
+ /// <param name="predicted">Whether to predict the popup or not.</param>
+ [PublicAPI]
+ public bool TryUseCharge(Entity<PowerCellSlotComponent?> ent, float charge, EntityUid? user = null, bool predicted = false)
+ {
+ if (!TryGetBatteryFromSlot(ent, out var battery))
+ {
+ if (user == null)
+ return false;
+
+ if (predicted)
+ _popup.PopupClient(Loc.GetString("power-cell-no-battery"), ent.Owner, user.Value);
+ else
+ _popup.PopupEntity(Loc.GetString("power-cell-no-battery"), ent.Owner, user.Value);
+
+ return false;
+ }
+
+ if (!_battery.TryUseCharge((battery.Value, battery), charge))
+ {
+ if (user == null)
+ return false;
+
+ if (predicted)
+ _popup.PopupClient(Loc.GetString("power-cell-insufficient"), ent.Owner, user.Value);
+ else
+ _popup.PopupEntity(Loc.GetString("power-cell-insufficient"), ent.Owner, user.Value);
+
+ return false;
+ }
+ return true;
+ }
+
+ /// <summary>
+ /// Gets number of remaining uses for the given charge cost.
+ /// </summary>
+ /// <param name="ent">The power cell.</param>
+ /// <param name="cost">The cost per use.</param>
+ [PublicAPI]
+ public int GetRemainingUses(Entity<PowerCellSlotComponent?> ent, float cost)
+ {
+ if (!TryGetBatteryFromSlot(ent, out var battery))
+ return 0;
+
+ return _battery.GetRemainingUses(battery.Value.AsNullable(), cost);
+ }
+
+ /// <summary>
+ /// Gets number of maximum uses at full charge for the given charge cost.
+ /// </summary>
+ /// <param name="ent">The power cell.</param>
+ /// <param name="cost">The cost per use.</param>
+ [PublicAPI]
+ public int GetMaxUses(Entity<PowerCellSlotComponent?> ent, float cost)
+ {
+ if (!TryGetBatteryFromSlot(ent, out var battery))
+ return 0;
+
+ return _battery.GetMaxUses(battery.Value.AsNullable(), cost);
+ }
+}
--- /dev/null
+using Content.Shared.PowerCell.Components;
+using JetBrains.Annotations;
+
+namespace Content.Shared.PowerCell;
+
+public sealed partial class PowerCellSystem
+{
+ /// <summary>
+ /// Enables or disables the power cell draw.
+ /// </summary>
+ [PublicAPI]
+ public void SetDrawEnabled(Entity<PowerCellDrawComponent?> ent, bool enabled)
+ {
+ if (!Resolve(ent, ref ent.Comp, false) || ent.Comp.Enabled == enabled)
+ return;
+
+ ent.Comp.Enabled = enabled;
+ Dirty(ent, ent.Comp);
+
+ if (TryGetBatteryFromSlot(ent.Owner, out var battery))
+ _battery.RefreshChargeRate(battery.Value.AsNullable());
+ }
+
+
+ /// <summary>
+ /// Returns whether the entity has a slotted battery and <see cref="PowerCellDrawComponent.UseCharge"/> charge.
+ /// </summary>
+ /// <param name="ent">The device with the power cell slot.</param>
+ /// <param name="user">Show a popup to this user with the relevant details if specified.</param>
+ /// <param name="user">Whether to predict the popup or not.</param>
+ [PublicAPI]
+ public bool HasActivatableCharge(Entity<PowerCellDrawComponent?, PowerCellSlotComponent?> ent, EntityUid? user = null, bool predicted = false)
+ {
+ // Default to true if we don't have the components.
+ if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2, false))
+ return true;
+
+ return HasCharge((ent, ent.Comp2), ent.Comp1.UseCharge, user, predicted);
+ }
+
+ /// <summary>
+ /// Tries to use the <see cref="PowerCellDrawComponent.UseCharge"/> for this entity.
+ /// </summary>
+ /// <param name="ent">The device with the power cell slot.</param>
+ /// <param name="user">Show a popup to this user with the relevant details if specified.</param>
+ /// <param name="user">Whether to predict the popup or not.</param>
+ [PublicAPI]
+ public bool TryUseActivatableCharge(Entity<PowerCellDrawComponent?, PowerCellSlotComponent?> ent, EntityUid? user = null, bool predicted = false)
+ {
+ // Default to true if we don't have the components.
+ if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2, false))
+ return true;
+
+ if (TryUseCharge((ent, ent.Comp2), ent.Comp1.UseCharge, user, predicted))
+ return true;
+
+ return false;
+ }
+
+ /// <summary>
+ /// Whether the power cell has any power at all for the draw rate.
+ /// </summary>
+ /// <param name="ent">The device with the power cell slot.</param>
+ /// <param name="user">Show a popup to this user with the relevant details if specified.</param>
+ /// <param name="user">Whether to predict the popup or not.</param>
+ [PublicAPI]
+ public bool HasDrawCharge(Entity<PowerCellDrawComponent?, PowerCellSlotComponent?> ent, EntityUid? user = null, bool predicted = false)
+ {
+ // Default to true if we don't have the components.
+ if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2, false))
+ return true;
+
+ // 1 second of charge at the required draw rate.
+ return HasCharge((ent, ent.Comp2), ent.Comp1.DrawRate, user, predicted);
+ }
+}
--- /dev/null
+using Content.Shared.Emp;
+using Content.Shared.Kitchen;
+using Content.Shared.Power;
+using Content.Shared.PowerCell.Components;
+using Content.Shared.Rejuvenate;
+
+namespace Content.Shared.PowerCell;
+
+public sealed partial class PowerCellSystem
+{
+ public void InitializeRelay()
+ {
+ SubscribeLocalEvent<PowerCellSlotComponent, BeingMicrowavedEvent>(RelayToCell);
+ SubscribeLocalEvent<PowerCellSlotComponent, RejuvenateEvent>(RelayToCell);
+ SubscribeLocalEvent<PowerCellSlotComponent, GetChargeEvent>(RelayToCell);
+ SubscribeLocalEvent<PowerCellSlotComponent, ChangeChargeEvent>(RelayToCell);
+
+ SubscribeLocalEvent<PowerCellComponent, EmpAttemptEvent>(RelayToCellSlot); // Prevent the ninja from EMPing its own battery
+ SubscribeLocalEvent<PowerCellComponent, PredictedBatteryChargeChangedEvent>(RelayToCellSlot);
+ SubscribeLocalEvent<PowerCellComponent, PredictedBatteryStateChangedEvent>(RelayToCellSlot); // For shutting down devices if the battery is empty
+ SubscribeLocalEvent<PowerCellComponent, RefreshChargeRateEvent>(RelayToCellSlot); // Allow devices to charge/drain inserted batteries
+ }
+
+ private void RelayToCell<T>(Entity<PowerCellSlotComponent> ent, ref T args) where T : notnull
+ {
+ if (!_itemSlots.TryGetSlot(ent.Owner, ent.Comp.CellSlotId, out var slot) || !slot.Item.HasValue)
+ return;
+
+ // Relay the event to the power cell.
+ RaiseLocalEvent(slot.Item.Value, ref args);
+ }
+
+ private void RelayToCellSlot<T>(Entity<PowerCellComponent> ent, ref T args) where T : notnull
+ {
+ var parent = Transform(ent).ParentUid;
+ // Relay the event to the slot entity.
+ if (HasComp<PowerCellSlotComponent>(parent))
+ RaiseLocalEvent(parent, ref args);
+ }
+}
--- /dev/null
+using Content.Shared.Containers.ItemSlots;
+using Content.Shared.PowerCell.Components;
+using Content.Shared.Examine;
+using Content.Shared.Popups;
+using Content.Shared.Power;
+using Content.Shared.Power.Components;
+using Content.Shared.Power.EntitySystems;
+using Robust.Shared.Containers;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.PowerCell;
+
+public sealed partial class PowerCellSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] private readonly PredictedBatterySystem _battery = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ InitializeRelay();
+
+ SubscribeLocalEvent<PowerCellSlotComponent, ContainerIsInsertingAttemptEvent>(OnCellSlotInsertAttempt);
+ SubscribeLocalEvent<PowerCellSlotComponent, EntInsertedIntoContainerMessage>(OnCellSlotInserted);
+ SubscribeLocalEvent<PowerCellSlotComponent, EntRemovedFromContainerMessage>(OnCellSlotRemoved);
+ SubscribeLocalEvent<PowerCellSlotComponent, ExaminedEvent>(OnCellSlotExamined);
+ SubscribeLocalEvent<PowerCellSlotComponent, PredictedBatteryStateChangedEvent>(OnCellSlotStateChanged);
+
+ SubscribeLocalEvent<PowerCellComponent, ExaminedEvent>(OnCellExamined);
+
+ SubscribeLocalEvent<PowerCellDrawComponent, RefreshChargeRateEvent>(OnDrawRefreshChargeRate);
+ SubscribeLocalEvent<PowerCellDrawComponent, ComponentStartup>(OnDrawStartup);
+ SubscribeLocalEvent<PowerCellDrawComponent, ComponentRemove>(OnDrawRemove);
+
+ }
+
+ private void OnCellSlotInsertAttempt(Entity<PowerCellSlotComponent> ent, ref ContainerIsInsertingAttemptEvent args)
+ {
+ if (!ent.Comp.Initialized)
+ return;
+
+ if (args.Container.ID != ent.Comp.CellSlotId)
+ return;
+
+ // TODO: Can't this just use the ItemSlot's whitelist?
+ if (!HasComp<PowerCellComponent>(args.EntityUid))
+ args.Cancel();
+ }
+
+ private void OnCellSlotInserted(Entity<PowerCellSlotComponent> ent, ref EntInsertedIntoContainerMessage args)
+ {
+ if (args.Container.ID != ent.Comp.CellSlotId)
+ return;
+
+ if (_timing.ApplyingState)
+ return; // The change in appearance data is already networked separately.
+
+
+ var ev = new PowerCellChangedEvent(false);
+ RaiseLocalEvent(ent, ref ev);
+
+ _battery.RefreshChargeRate(args.Entity);
+
+ // Only update the visuals if we actually use them.
+ if (!HasComp<PredictedBatteryVisualsComponent>(ent))
+ return;
+
+ // Set the data to that of the power cell
+ if (_appearance.TryGetData(args.Entity, BatteryVisuals.State, out BatteryState state))
+ _appearance.SetData(ent.Owner, BatteryVisuals.State, state);
+
+ // Set the data to that of the power cell
+ if (_appearance.TryGetData(args.Entity, BatteryVisuals.Charging, out BatteryChargingState charging))
+ _appearance.SetData(ent.Owner, BatteryVisuals.Charging, charging);
+ }
+
+ private void OnCellSlotRemoved(Entity<PowerCellSlotComponent> ent, ref EntRemovedFromContainerMessage args)
+ {
+ if (args.Container.ID != ent.Comp.CellSlotId)
+ return;
+
+ if (_timing.ApplyingState)
+ return; // The change in appearance data is already networked separately.
+
+ var ev = new PowerCellChangedEvent(true);
+ RaiseLocalEvent(ent, ref ev);
+
+ var emptyEv = new PowerCellSlotEmptyEvent();
+ RaiseLocalEvent(ent, ref emptyEv);
+
+ _battery.RefreshChargeRate(args.Entity);
+
+ // Only update the visuals if we actually use them.
+ if (!HasComp<PredictedBatteryVisualsComponent>(ent))
+ return;
+
+ // Set the appearance to empty.
+ _appearance.SetData(ent.Owner, BatteryVisuals.State, BatteryState.Empty);
+ _appearance.SetData(ent.Owner, BatteryVisuals.Charging, BatteryChargingState.Constant);
+ }
+
+
+ private void OnCellSlotStateChanged(Entity<PowerCellSlotComponent> ent, ref PredictedBatteryStateChangedEvent args)
+ {
+ if (args.NewState != BatteryState.Empty)
+ return;
+
+ // Inform the device that the battery is empty.
+ var ev = new PowerCellSlotEmptyEvent();
+ RaiseLocalEvent(ent, ref ev);
+ }
+
+ private void OnCellSlotExamined(Entity<PowerCellSlotComponent> ent, ref ExaminedEvent args)
+ {
+ if (TryGetBatteryFromSlot(ent.AsNullable(), out var battery))
+ OnBatteryExamined(battery.Value, ref args);
+ else
+ args.PushMarkup(Loc.GetString("power-cell-component-examine-details-no-battery"));
+ }
+
+ private void OnCellExamined(Entity<PowerCellComponent> ent, ref ExaminedEvent args)
+ {
+ if (TryComp<PredictedBatteryComponent>(ent, out var battery))
+ OnBatteryExamined((ent.Owner, battery), ref args);
+ }
+
+ private void OnBatteryExamined(Entity<PredictedBatteryComponent> ent, ref ExaminedEvent args)
+ {
+ var charge = _battery.GetCharge(ent.AsNullable()) / ent.Comp.MaxCharge * 100;
+ args.PushMarkup(Loc.GetString("power-cell-component-examine-details", ("currentCharge", $"{charge:F0}")));
+ }
+
+ private void OnDrawRefreshChargeRate(Entity<PowerCellDrawComponent> ent, ref RefreshChargeRateEvent args)
+ {
+ if (ent.Comp.Enabled)
+ args.NewChargeRate -= ent.Comp.DrawRate;
+ }
+
+ private void OnDrawStartup(Entity<PowerCellDrawComponent> ent, ref ComponentStartup args)
+ {
+ if (ent.Comp.Enabled)
+ _battery.RefreshChargeRate(ent.Owner);
+ }
+
+ private void OnDrawRemove(Entity<PowerCellDrawComponent> ent, ref ComponentRemove args)
+ {
+ // We use ComponentRemove to make sure this component no longer subscribes to the refresh event.
+ if (ent.Comp.Enabled)
+ _battery.RefreshChargeRate(ent.Owner);
+ }
+}
+++ /dev/null
-using Content.Shared.Containers.ItemSlots;
-using Content.Shared.Emp;
-using Content.Shared.PowerCell.Components;
-using Content.Shared.Rejuvenate;
-using Robust.Shared.Containers;
-using Robust.Shared.Timing;
-
-namespace Content.Shared.PowerCell;
-
-public abstract class SharedPowerCellSystem : EntitySystem
-{
- [Dependency] protected readonly IGameTiming Timing = default!;
- [Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
- [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent<PowerCellDrawComponent, MapInitEvent>(OnMapInit);
-
- SubscribeLocalEvent<PowerCellSlotComponent, RejuvenateEvent>(OnRejuvenate);
- SubscribeLocalEvent<PowerCellSlotComponent, EntInsertedIntoContainerMessage>(OnCellInserted);
- SubscribeLocalEvent<PowerCellSlotComponent, EntRemovedFromContainerMessage>(OnCellRemoved);
- SubscribeLocalEvent<PowerCellSlotComponent, ContainerIsInsertingAttemptEvent>(OnCellInsertAttempt);
-
- SubscribeLocalEvent<PowerCellComponent, EmpAttemptEvent>(OnCellEmpAttempt);
- }
-
- private void OnMapInit(Entity<PowerCellDrawComponent> ent, ref MapInitEvent args)
- {
- ent.Comp.NextUpdateTime = Timing.CurTime + ent.Comp.Delay;
- }
-
- private void OnRejuvenate(EntityUid uid, PowerCellSlotComponent component, RejuvenateEvent args)
- {
- if (!_itemSlots.TryGetSlot(uid, component.CellSlotId, out var itemSlot) || !itemSlot.Item.HasValue)
- return;
-
- // charge entity batteries and remove booby traps.
- RaiseLocalEvent(itemSlot.Item.Value, args);
- }
-
- private void OnCellInsertAttempt(EntityUid uid, PowerCellSlotComponent component, ContainerIsInsertingAttemptEvent args)
- {
- if (!component.Initialized)
- return;
-
- if (args.Container.ID != component.CellSlotId)
- return;
-
- if (!HasComp<PowerCellComponent>(args.EntityUid))
- {
- args.Cancel();
- }
- }
-
- private void OnCellInserted(EntityUid uid, PowerCellSlotComponent component, EntInsertedIntoContainerMessage args)
- {
- if (!component.Initialized)
- return;
-
- if (args.Container.ID != component.CellSlotId)
- return;
- _appearance.SetData(uid, PowerCellSlotVisuals.Enabled, true);
- RaiseLocalEvent(uid, new PowerCellChangedEvent(false), false);
- }
-
- protected virtual void OnCellRemoved(EntityUid uid, PowerCellSlotComponent component, EntRemovedFromContainerMessage args)
- {
- if (args.Container.ID != component.CellSlotId)
- return;
- _appearance.SetData(uid, PowerCellSlotVisuals.Enabled, false);
- RaiseLocalEvent(uid, new PowerCellChangedEvent(true), false);
- }
-
- private void OnCellEmpAttempt(Entity<PowerCellComponent> entity, ref EmpAttemptEvent args)
- {
- var parent = Transform(entity).ParentUid;
- // relay the attempt event to the slot so it can cancel it
- if (HasComp<PowerCellSlotComponent>(parent))
- RaiseLocalEvent(parent, ref args);
- }
-
- public void SetDrawEnabled(Entity<PowerCellDrawComponent?> ent, bool enabled)
- {
- if (!Resolve(ent, ref ent.Comp, false) || ent.Comp.Enabled == enabled)
- return;
-
- if (enabled)
- ent.Comp.NextUpdateTime = Timing.CurTime;
-
- ent.Comp.Enabled = enabled;
- Dirty(ent, ent.Comp);
- }
-
- /// <summary>
- /// Returns whether the entity has a slotted battery and <see cref="PowerCellDrawComponent.UseRate"/> charge.
- /// </summary>
- /// <param name="uid"></param>
- /// <param name="battery"></param>
- /// <param name="cell"></param>
- /// <param name="user">Popup to this user with the relevant detail if specified.</param>
- public abstract bool HasActivatableCharge(
- EntityUid uid,
- PowerCellDrawComponent? battery = null,
- PowerCellSlotComponent? cell = null,
- EntityUid? user = null);
-
- /// <summary>
- /// Whether the power cell has any power at all for the draw rate.
- /// </summary>
- public abstract bool HasDrawCharge(
- EntityUid uid,
- PowerCellDrawComponent? battery = null,
- PowerCellSlotComponent? cell = null,
- EntityUid? user = null);
-}
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()
{
private void OnActivateAttempt(Entity<ToggleCellDrawComponent> ent, ref ItemToggleActivateAttemptEvent args)
{
- if (!_cell.HasDrawCharge(ent, user: args.User)
- || !_cell.HasActivatableCharge(ent, user: args.User))
+ if (!_cell.HasDrawCharge(ent.Owner, user: args.User, predicted: true)
+ || !_cell.HasActivatableCharge(ent.Owner, user: args.User, predicted: true))
args.Cancelled = true;
}
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;
/// "brain", legs, modules, and battery. Essentially the master component
/// for borg logic.
/// </summary>
-[RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem)), AutoGenerateComponentState]
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem))]
+[AutoGenerateComponentState, AutoGenerateComponentPause]
public sealed partial class BorgChassisComponent : Component
{
#region Brain
[DataField]
public ProtoId<AlertPrototype> NoBatteryAlert = "BorgBatteryNone";
+ /// <summary>
+ /// The next update time for the battery charge level.
+ /// Used for the alert and borg UI.
+ /// </summary>
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+ [AutoPausedField]
+ public TimeSpan NextBatteryUpdate = TimeSpan.Zero;
+
/// <summary>
/// If the entity can open own UI.
/// </summary>
/// Raised on the entity being inserted whenever checking if an entity can be inserted into an entity storage.
/// </summary>
[ByRefEvent]
-public record struct InsertIntoEntityStorageAttemptEvent(EntityUid ItemToInsert, bool Cancelled = false);
+public record struct InsertIntoEntityStorageAttemptEvent(BaseContainer Container, EntityUid ItemToInsert, bool Cancelled = false);
/// <summary>
/// Raised on the entity storage whenever checking if an entity can be inserted into it.
/// </summary>
[ByRefEvent]
-public record struct EntityStorageInsertedIntoAttemptEvent(EntityUid ItemToInsert, bool Cancelled = false);
+public record struct EntityStorageInsertedIntoAttemptEvent(BaseContainer Container, EntityUid ItemToInsert, bool Cancelled = false);
/// <summary>
/// Raised on the Container's owner whenever an entity storage tries to dump its
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)
-using Content.Shared.PowerCell;
+using Content.Shared.PowerCell.Components;
using Robust.Shared.GameStates;
namespace Content.Shared.UserInterface;
/// <summary>
-/// Specifies that the attached entity requires <see cref="PowerCellDrawComponent"/> power.
+/// Specifies that the attached entity requires <see cref="PowerCellDrawComponent"/> power to open the activatable UI.
/// </summary>
[RegisterComponent, NetworkedComponent]
-public sealed partial class ActivatableUIRequiresPowerCellComponent : Component
-{
-
-}
+public sealed partial class ActivatableUIRequiresPowerCellComponent : Component;
using Content.Shared.Item.ItemToggle;
using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.PowerCell;
-using Robust.Shared.Containers;
+using Content.Shared.Power;
+using Content.Shared.Power.Components;
namespace Content.Shared.UserInterface;
public sealed partial class ActivatableUISystem
{
[Dependency] private readonly ItemToggleSystem _toggle = default!;
- [Dependency] private readonly SharedPowerCellSystem _cell = default!;
+ [Dependency] private readonly PowerCellSystem _cell = default!;
private void InitializePower()
{
- SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, ActivatableUIOpenAttemptEvent>(OnBatteryOpenAttempt);
+ SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, ItemToggledEvent>(OnToggled);
SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, BoundUIOpenedEvent>(OnBatteryOpened);
SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, BoundUIClosedEvent>(OnBatteryClosed);
- SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, ItemToggledEvent>(OnToggled);
+ SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, PredictedBatteryStateChangedEvent>(OnBatteryStateChanged);
+ SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, ActivatableUIOpenAttemptEvent>(OnBatteryOpenAttempt);
}
private void OnToggled(Entity<ActivatableUIRequiresPowerCellComponent> ent, ref ItemToggledEvent args)
{
// only close ui when losing power
- if (!TryComp<ActivatableUIComponent>(ent, out var activatable) || args.Activated)
+ if (args.Activated || !TryComp<ActivatableUIComponent>(ent, out var activatable))
return;
if (activatable.Key == null)
_toggle.TryDeactivate(uid);
}
- /// <summary>
- /// Call if you want to check if the UI should close due to a recent battery usage.
- /// </summary>
- public void CheckUsage(EntityUid uid, ActivatableUIComponent? active = null, ActivatableUIRequiresPowerCellComponent? component = null, PowerCellDrawComponent? draw = null)
+ private void OnBatteryStateChanged(Entity<ActivatableUIRequiresPowerCellComponent> ent, ref PredictedBatteryStateChangedEvent args)
{
- if (!Resolve(uid, ref component, ref draw, ref active, false))
- return;
-
- if (active.Key == null)
- {
- Log.Error($"Encountered null key in activatable ui on entity {ToPrettyString(uid)}");
- return;
- }
-
- if (_cell.HasActivatableCharge(uid))
+ // Deactivate when empty.
+ if (args.NewState != BatteryState.Empty)
return;
- _uiSystem.CloseUi(uid, active.Key);
+ var activatable = Comp<ActivatableUIComponent>(ent);
+ if (activatable.Key != null)
+ _uiSystem.CloseUi(ent.Owner, activatable.Key);
}
private void OnBatteryOpenAttempt(EntityUid uid, ActivatableUIRequiresPowerCellComponent component, ActivatableUIOpenAttemptEvent args)
{
- if (!TryComp<PowerCellDrawComponent>(uid, out var draw))
+ if (args.Cancelled)
return;
// Check if we have the appropriate drawrate / userate to even open it.
- if (args.Cancelled ||
- !_cell.HasActivatableCharge(uid, draw, user: args.User) ||
- !_cell.HasDrawCharge(uid, draw, user: args.User))
+ if (!_cell.HasActivatableCharge(uid, user: args.User, predicted: true) ||
+ !_cell.HasDrawCharge(uid, user: args.User, predicted: true))
{
args.Cancel();
}
+using Content.Shared.Power.Components;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
namespace Content.Shared.Weapons.Ranged.Components;
-public abstract partial class BatteryAmmoProviderComponent : AmmoProviderComponent
+/// <summary>
+/// Ammo provider that uses electric charge from a battery to provide ammunition to a weapon.
+/// This works with both <see cref="BatteryComponent"/> and <see cref="PredictedBatteryComponent"/>
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+[AutoGenerateComponentState(raiseAfterAutoHandleState: true), AutoGenerateComponentPause]
+public sealed partial class BatteryAmmoProviderComponent : AmmoProviderComponent
{
/// <summary>
- /// How much battery it costs to fire once.
+ /// The projectile or hitscan entity to spawn when firing.
+ /// </summary>
+ [DataField("proto", required: true)]
+ public EntProtoId Prototype;
+
+ /// <summary>
+ /// How much charge it costs to fire once, in watts.
/// </summary>
- [DataField("fireCost"), ViewVariables(VVAccess.ReadWrite)]
+ [DataField, AutoNetworkedField]
public float FireCost = 100;
- // Batteries aren't predicted which means we need to track the battery and manually count it ourselves woo!
+ /// <summary>
+ /// Timestamp for the next update for the shot counter and visuals.
+ /// This is the expected time at which the next integer will be reached.
+ /// Null if the charge rate is 0, meaning the shot amount is constant.
+ /// Only used for predicted batteries.
+ /// </summary>
+ /// <remarks>
+ /// Not a datafield since this is refreshed along with the battery's charge rate anyways.
+ /// </remarks>
+ [ViewVariables, AutoNetworkedField, AutoPausedField]
+ public TimeSpan? NextUpdate;
- [ViewVariables(VVAccess.ReadWrite)]
+ /// <summary>
+ /// The time between reaching full charges at the current charge rate.
+ /// Only used for predicted batteries.
+ /// </summary>
+ /// <remarks>
+ /// Not a datafield since this is refreshed along with the battery's charge rate anyways.
+ /// </remarks>
+ [ViewVariables, AutoNetworkedField]
+ public TimeSpan ChargeTime = TimeSpan.Zero;
+
+ /// <summary>
+ /// The current amount of available shots.
+ /// BatteryComponent is not predicted, so we need to manually network this for the ammo indicator and examination text.
+ /// </summary>
+ /// <remarks>
+ /// Not a datafield since this is only cached and refreshed on component startup.
+ /// TODO: If we ever fully predict all batteries then remove this and just read the charge on the client.
+ /// </remarks>
+ [ViewVariables, AutoNetworkedField]
public int Shots;
- [ViewVariables(VVAccess.ReadWrite)]
+ /// <summary>
+ /// The maximum amount of available shots.
+ /// BatteryComponent is not predicted, so we need to manually network this for the ammo indicator and examination text.
+ /// </summary>
+ /// <remarks>
+ /// Not a datafield since this is only cached and refreshed on component startup.
+ /// TODO: If we ever fully predict all batteries then remove this and just read the charge on the client.
+ /// </remarks>
+ [ViewVariables, AutoNetworkedField]
public int Capacity;
}
+++ /dev/null
-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;
-}
+++ /dev/null
-using Robust.Shared.GameStates;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Shared.Weapons.Ranged.Components;
-
-[RegisterComponent, NetworkedComponent]
-public sealed partial class ProjectileBatteryAmmoProviderComponent : BatteryAmmoProviderComponent
-{
- [ViewVariables(VVAccess.ReadWrite), DataField("proto", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
- public string Prototype = default!;
-}
+++ /dev/null
-namespace Content.Shared.Weapons.Ranged.Events;
-
-[ByRefEvent]
-public readonly record struct UpdateClientAmmoEvent();
\ No newline at end of file
[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()
{
_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));
}
}
}
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;
{
protected virtual void InitializeBattery()
{
- // Trying to dump comp references hence the below
- // Hitscan
- SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, ComponentGetState>(OnBatteryGetState);
- SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, ComponentHandleState>(OnBatteryHandleState);
- SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, TakeAmmoEvent>(OnBatteryTakeAmmo);
- SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, GetAmmoCountEvent>(OnBatteryAmmoCount);
- SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, ExaminedEvent>(OnBatteryExamine);
- SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, DamageExamineEvent>(OnBatteryDamageExamine);
-
- // Projectile
- SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, ComponentGetState>(OnBatteryGetState);
- SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, ComponentHandleState>(OnBatteryHandleState);
- SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, TakeAmmoEvent>(OnBatteryTakeAmmo);
- SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, GetAmmoCountEvent>(OnBatteryAmmoCount);
- SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, ExaminedEvent>(OnBatteryExamine);
- SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, DamageExamineEvent>(OnBatteryDamageExamine);
+ SubscribeLocalEvent<BatteryAmmoProviderComponent, ComponentStartup>(OnBatteryStartup);
+ SubscribeLocalEvent<BatteryAmmoProviderComponent, AfterAutoHandleStateEvent>(OnAfterAutoHandleState);
+ SubscribeLocalEvent<BatteryAmmoProviderComponent, TakeAmmoEvent>(OnBatteryTakeAmmo);
+ SubscribeLocalEvent<BatteryAmmoProviderComponent, GetAmmoCountEvent>(OnBatteryAmmoCount);
+ SubscribeLocalEvent<BatteryAmmoProviderComponent, ExaminedEvent>(OnBatteryExamine);
+ SubscribeLocalEvent<BatteryAmmoProviderComponent, DamageExamineEvent>(OnBatteryDamageExamine);
+ SubscribeLocalEvent<BatteryAmmoProviderComponent, PowerCellChangedEvent>(OnPowerCellChanged);
+ SubscribeLocalEvent<BatteryAmmoProviderComponent, PredictedBatteryChargeChangedEvent>(OnPredictedChargeChanged);
+ SubscribeLocalEvent<BatteryAmmoProviderComponent, ChargeChangedEvent>(OnChargeChanged);
}
- private void OnBatteryHandleState(EntityUid uid, BatteryAmmoProviderComponent component, ref ComponentHandleState args)
+ private void OnBatteryExamine(Entity<BatteryAmmoProviderComponent> ent, ref ExaminedEvent args)
{
- if (args.Current is not BatteryAmmoProviderComponentState state)
- return;
-
- component.Shots = state.Shots;
- component.Capacity = state.MaxShots;
- component.FireCost = state.FireCost;
- UpdateAmmoCount(uid, prediction: false);
+ args.PushMarkup(Loc.GetString("gun-battery-examine", ("color", AmmoExamineColor), ("count", ent.Comp.Shots)));
}
- private void OnBatteryGetState(EntityUid uid, BatteryAmmoProviderComponent component, ref ComponentGetState args)
+ private void OnBatteryDamageExamine(Entity<BatteryAmmoProviderComponent> ent, ref DamageExamineEvent args)
{
- args.State = new BatteryAmmoProviderComponentState()
+ var proto = ProtoManager.Index<EntityPrototype>(ent.Comp.Prototype);
+ DamageSpecifier? damageSpec = null;
+ var damageType = string.Empty;
+
+ if (proto.TryGetComponent<ProjectileComponent>(out var projectileComp, Factory))
{
- Shots = component.Shots,
- MaxShots = component.Capacity,
- FireCost = component.FireCost,
- };
- }
+ if (!projectileComp.Damage.Empty)
+ {
+ damageType = Loc.GetString("damage-projectile");
+ damageSpec = projectileComp.Damage * Damageable.UniversalProjectileDamageModifier;
+ }
+ }
+ else if (proto.TryGetComponent<HitscanBasicDamageComponent>(out var hitscanComp, Factory))
+ {
+ if (!hitscanComp.Damage.Empty)
+ {
+ damageType = Loc.GetString("damage-hitscan");
+ damageSpec = hitscanComp.Damage * Damageable.UniversalHitscanDamageModifier;
+ }
+ }
+ if (damageSpec == null)
+ return;
- private void OnBatteryExamine(EntityUid uid, BatteryAmmoProviderComponent component, ExaminedEvent args)
- {
- args.PushMarkup(Loc.GetString("gun-battery-examine", ("color", AmmoExamineColor), ("count", component.Shots)));
+ _damageExamine.AddDamageExamine(args.Message, Damageable.ApplyUniversalAllModifiers(damageSpec), damageType);
}
- private void OnBatteryDamageExamine<T>(Entity<T> entity, ref DamageExamineEvent args) where T : BatteryAmmoProviderComponent
+ private void OnBatteryTakeAmmo(Entity<BatteryAmmoProviderComponent> ent, ref TakeAmmoEvent args)
{
- var damageSpec = GetDamage(entity.Comp);
+ var shots = Math.Min(args.Shots, ent.Comp.Shots);
- if (damageSpec == null)
+ if (shots == 0)
return;
- var damageType = entity.Comp switch
+ for (var i = 0; i < shots; i++)
{
- HitscanBatteryAmmoProviderComponent => Loc.GetString("damage-hitscan"),
- ProjectileBatteryAmmoProviderComponent => Loc.GetString("damage-projectile"),
- _ => throw new ArgumentOutOfRangeException(),
- };
+ args.Ammo.Add(GetShootable(ent, args.Coordinates));
+ }
- _damageExamine.AddDamageExamine(args.Message, Damageable.ApplyUniversalAllModifiers(damageSpec), damageType);
+ TakeCharge(ent, shots);
}
- private DamageSpecifier? GetDamage(BatteryAmmoProviderComponent component)
+ private void OnBatteryAmmoCount(Entity<BatteryAmmoProviderComponent> ent, ref GetAmmoCountEvent args)
{
- if (component is ProjectileBatteryAmmoProviderComponent battery)
- {
- if (ProtoManager.Index<EntityPrototype>(battery.Prototype).Components
- .TryGetValue(Factory.GetComponentName<ProjectileComponent>(), out var projectile))
- {
- var p = (ProjectileComponent) projectile.Component;
-
- if (!p.Damage.Empty)
- {
- return p.Damage * Damageable.UniversalProjectileDamageModifier;
- }
- }
-
- return null;
- }
+ args.Count = ent.Comp.Shots;
+ args.Capacity = ent.Comp.Capacity;
+ }
- if (component is HitscanBatteryAmmoProviderComponent hitscan)
- {
- var dmg = ProtoManager.Index(hitscan.HitscanEntityProto);
- if (!dmg.TryGetComponent<HitscanBasicDamageComponent>(out var basicDamageComp, Factory))
- return null;
+ /// <summary>
+ /// Use up the required amount of battery charge for firing.
+ /// </summary>
+ public void TakeCharge(Entity<BatteryAmmoProviderComponent> ent, int shots = 1)
+ {
+ // Take charge from either the BatteryComponent, PredictedBatteryComponent or PowerCellSlotComponent.
+ var ev = new ChangeChargeEvent(-ent.Comp.FireCost * shots);
+ RaiseLocalEvent(ent, ref ev);
+ // UpdateShots is already called by the resulting PredictedBatteryChargeChangedEvent or ChargeChangedEvent
+ }
- return basicDamageComp.Damage * Damageable.UniversalHitscanDamageModifier;
- }
+ private (EntityUid? Entity, IShootable) GetShootable(BatteryAmmoProviderComponent component, EntityCoordinates coordinates)
+ {
- return null;
+ var ent = Spawn(component.Prototype, coordinates);
+ return (ent, EnsureShootable(ent));
}
- private void OnBatteryTakeAmmo(EntityUid uid, BatteryAmmoProviderComponent component, TakeAmmoEvent args)
+ public void UpdateShots(Entity<BatteryAmmoProviderComponent> ent)
{
- var shots = Math.Min(args.Shots, component.Shots);
+ var oldShots = ent.Comp.Shots;
+ var oldCapacity = ent.Comp.Capacity;
+ (var newShots, var newCapacity) = GetShots(ent);
- // Don't dirty if it's an empty fire.
- if (shots == 0)
+ // Only dirty if necessary.
+ if (oldShots == newShots && oldCapacity == newCapacity)
return;
- for (var i = 0; i < shots; i++)
- {
- args.Ammo.Add(GetShootable(component, args.Coordinates));
- component.Shots--;
- }
+ ent.Comp.Shots = newShots;
+ if (newCapacity > 0) // Don't make the capacity 0 when removing a power cell as this will make it be visualized as full instead of empty.
+ ent.Comp.Capacity = newCapacity;
+
+ // Update the ammo counter predictively if the change was predicted. On the server this does nothing.
+ UpdateAmmoCount(ent.Owner);
- TakeCharge((uid, component));
- UpdateBatteryAppearance(uid, component);
- Dirty(uid, component);
+ Dirty(ent); // Dirtying will update the client's ammo counter in an AfterAutoHandleStateEvent subscription in case it was not predicted.
+
+ if (!TryComp<AppearanceComponent>(ent, out var appearance))
+ return;
+
+ // Update the visuals.
+ Appearance.SetData(ent.Owner, AmmoVisuals.HasAmmo, newShots != 0, appearance);
+ Appearance.SetData(ent.Owner, AmmoVisuals.AmmoCount, newShots, appearance);
+ if (newCapacity > 0) // Don't make the capacity 0 when removing a power cell as this will make it be visualized as full instead of empty.
+ Appearance.SetData(ent.Owner, AmmoVisuals.AmmoMax, newCapacity, appearance);
}
- private void OnBatteryAmmoCount(EntityUid uid, BatteryAmmoProviderComponent component, ref GetAmmoCountEvent args)
+ // For server side changes the client's ammo counter needs to be updated as well.
+ private void OnAfterAutoHandleState(Entity<BatteryAmmoProviderComponent> ent, ref AfterAutoHandleStateEvent args)
{
- args.Count = component.Shots;
- args.Capacity = component.Capacity;
+ UpdateAmmoCount(ent); // Need to have prediction set to true because the state is applied repeatedly while prediction is running.
}
- /// <summary>
- /// Update the battery (server-only) whenever fired.
- /// </summary>
- protected virtual void TakeCharge(Entity<BatteryAmmoProviderComponent> entity)
+ // For when a power cell gets inserted or removed.
+ private void OnPowerCellChanged(Entity<BatteryAmmoProviderComponent> ent, ref PowerCellChangedEvent args)
{
- UpdateAmmoCount(entity, prediction: false);
+ UpdateShots(ent);
}
- protected void UpdateBatteryAppearance(EntityUid uid, BatteryAmmoProviderComponent component)
+ // For predicted batteries.
+ // If the entity is has a PowerCellSlotComponent then this event is relayed from the power cell to the slot entity.
+ private void OnPredictedChargeChanged(Entity<BatteryAmmoProviderComponent> ent, ref PredictedBatteryChargeChangedEvent args)
{
- if (!TryComp<AppearanceComponent>(uid, out var appearance))
- return;
+ // Update the visuals and charge counter UI.
+ UpdateShots(ent);
+ // Queue the update for when the autorecharge reaches enough charge for another shot.
+ UpdateNextUpdate(ent, args.CurrentCharge, args.MaxCharge, args.CurrentChargeRate);
+ }
- Appearance.SetData(uid, AmmoVisuals.HasAmmo, component.Shots != 0, appearance);
- Appearance.SetData(uid, AmmoVisuals.AmmoCount, component.Shots, appearance);
- Appearance.SetData(uid, AmmoVisuals.AmmoMax, component.Capacity, appearance);
+ // For unpredicted batteries.
+ private void OnChargeChanged(Entity<BatteryAmmoProviderComponent> ent, ref ChargeChangedEvent args)
+ {
+ // Update the visuals and charge counter UI.
+ UpdateShots(ent);
+ // No need to queue an update here since unpredicted batteries already update periodically as they charge/discharge.
}
- private (EntityUid? Entity, IShootable) GetShootable(BatteryAmmoProviderComponent component, EntityCoordinates coordinates)
+ private void UpdateNextUpdate(Entity<BatteryAmmoProviderComponent> ent, float currentCharge, float maxCharge, float currentChargeRate)
{
- switch (component)
+ // Don't queue any updates if charge is constant.
+ ent.Comp.NextUpdate = null;
+ // ETA of the next full charge.
+ if (currentChargeRate > 0f && currentCharge != maxCharge)
+ {
+ ent.Comp.NextUpdate = Timing.CurTime + TimeSpan.FromSeconds((ent.Comp.FireCost - (currentCharge % ent.Comp.FireCost)) / currentChargeRate);
+ ent.Comp.ChargeTime = TimeSpan.FromSeconds(ent.Comp.FireCost / currentChargeRate);
+ }
+ else if (currentChargeRate < 0f && currentCharge != 0f)
{
- case ProjectileBatteryAmmoProviderComponent proj:
- var ent = Spawn(proj.Prototype, coordinates);
- return (ent, EnsureShootable(ent));
- case HitscanBatteryAmmoProviderComponent hitscan:
- var hitscanEnt = Spawn(hitscan.HitscanEntityProto);
- return (hitscanEnt, EnsureShootable(hitscanEnt));
- default:
- throw new ArgumentOutOfRangeException();
+ ent.Comp.NextUpdate = Timing.CurTime + TimeSpan.FromSeconds(-(currentCharge % ent.Comp.FireCost) / currentChargeRate);
+ ent.Comp.ChargeTime = TimeSpan.FromSeconds(-ent.Comp.FireCost / currentChargeRate);
}
+ Dirty(ent);
+ }
+
+ // Shots are only chached, not a DataField, so we need to refresh this when the game is loaded.
+ private void OnBatteryStartup(Entity<BatteryAmmoProviderComponent> ent, ref ComponentStartup args)
+ {
+ UpdateShots(ent);
+ }
+
+ /// <summary>
+ /// Gets the current and maximum amount of shots from this entity's battery.
+ /// This works for BatteryComponent, PredictedBatteryComponent and PowercellSlotComponent.
+ /// </summary>
+ public (int, int) GetShots(Entity<BatteryAmmoProviderComponent> ent)
+ {
+ var ev = new GetChargeEvent();
+ RaiseLocalEvent(ent, ref ev);
+ var currentShots = (int)(ev.CurrentCharge / ent.Comp.FireCost);
+ var maxShots = (int)(ev.MaxCharge / ent.Comp.FireCost);
+
+ return (currentShots, maxShots);
}
- [Serializable, NetSerializable]
- private sealed class BatteryAmmoProviderComponentState : ComponentState
+ /// <summary>
+ /// Update loop for refreshing the ammo counter for charging/draining predicted batteries.
+ /// This is not needed for unpredicted batteries since those already raise ChargeChangedEvent periodically.
+ /// </summary>
+ private void UpdateBattery(float frameTime)
{
- public int Shots;
- public int MaxShots;
- public float FireCost;
+ var curTime = Timing.CurTime;
+ var hitscanQuery = EntityQueryEnumerator<BatteryAmmoProviderComponent>();
+ while (hitscanQuery.MoveNext(out var uid, out var provider))
+ {
+ if (provider.NextUpdate == null || curTime < provider.NextUpdate)
+ continue;
+ UpdateShots((uid, provider));
+ provider.NextUpdate += provider.ChargeTime; // Queue another update for when we reach the next full charge.
+ Dirty(uid, provider);
+ // TODO: Stop updating when full or empty.
+ }
}
}
RaiseLocalEvent(uid, ref ammoEv);
return ammoEv.Capacity;
}
+
+ public override void Update(float frameTime)
+ {
+ UpdateBattery(frameTime);
+ }
}
/// <summary>
- type: Transform
pos: 6.5,-7.5
parent: 1
- - type: PowerCellDraw
- canUse: True
- canDraw: True
- type: Physics
canCollide: False
- type: ContainerContainer
amount: 6
whitelist:
components:
- - HitscanBatteryAmmoProvider
+ - BatteryAmmoProvider
blacklist:
components:
- PowerCell
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
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
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
- 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
- type: MovementSpeedModifier
baseWalkSpeed: 5
baseSprintSpeed: 7
- - type: ProjectileBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: WatcherBolt
fireCost: 50
- - type: BatterySelfRecharger
+ - type: PredictedBatterySelfRecharger
autoRechargeRate: 50
- - type: Battery
+ - type: PredictedBattery
maxCharge: 1000
startingCharge: 1000
- type: Gun
radius: 1
energy: 3
color: orangered
- - type: ProjectileBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: WatcherBoltMagmawing
fireCost: 50
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
- 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
onUse: false # above component does the toggling
- type: PowerCellDraw
drawRate: 0
- useRate: 20
+ useCharge: 20
- type: ToggleCellDraw
components:
- type: PowerCellDraw
drawRate: 1
- useRate: 0
+ useCharge: 0
- type: ToggleCellDraw
- type: entity
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
slotId: charger_slot
portable: true
- type: PowerChargerVisuals
- - type: ApcPowerReceiver
- needsPower: false
- powerLoad: 0
- type: StaticPrice
price: 500
- type: Tag
ejectOnInteract: true
whitelist:
components:
- - HitscanBatteryAmmoProvider
- - ProjectileBatteryAmmoProvider
+ - BatteryAmmoProvider
components:
- type: Item
storedRotation: -90
- - type: Battery
+ - type: PredictedBattery
pricePerJoule: 0.15
- type: PowerCell
- type: Explosive
- type: Tag
tags:
- PowerCell
- - type: Appearance
- - type: PowerCellVisuals
- type: Riggable
+ - type: Appearance
+ - type: PredictedBatteryVisuals
+ - type: GenericVisualizer
+ visuals:
+ enum.BatteryVisuals.State:
+ enum.PowerCellVisualLayers.Unshaded:
+ Full: {visible: true, state: o2}
+ Neither: {visible: true, state: o1}
+ Empty: {visible: false}
- type: entity
name: potato battery
- type: Sprite
layers:
- state: potato
- - type: Battery
+ - type: PredictedBattery
maxCharge: 70
startingCharge: 70
- type: Tag
- map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2
shader: unshaded
- - type: Battery
+ - type: PredictedBattery
maxCharge: 360
startingCharge: 360
- type: Tag
state: o2
shader: unshaded
visible: false
- - type: Battery
+ - type: PredictedBattery
maxCharge: 360
startingCharge: 0
name: small-capacity nuclear power cell
description: A self rechargeable power cell, designed for fast recharge rate at the expense of capacity.
components:
- - type: BatterySelfRecharger
+ - type: PredictedBatterySelfRecharger
autoRechargeRate: 36 # 10 seconds to recharge
autoRechargePauseTime: 30
- map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2
shader: unshaded
- - type: Battery
+ - type: PredictedBattery
maxCharge: 720
startingCharge: 720
state: o2
shader: unshaded
visible: false
- - type: Battery
+ - type: PredictedBattery
maxCharge: 720
startingCharge: 0
- map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2
shader: unshaded
- - type: Battery
+ - type: PredictedBattery
maxCharge: 1080
startingCharge: 1080
state: o2
shader: unshaded
visible: false
- - type: Battery
+ - type: PredictedBattery
maxCharge: 1080
startingCharge: 0
- map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2
shader: unshaded
- - type: Battery
+ - type: PredictedBattery
maxCharge: 1800
startingCharge: 1800
state: o2
shader: unshaded
visible: false
- - type: Battery
+ - type: PredictedBattery
maxCharge: 1800
startingCharge: 0
- map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2
shader: unshaded
- - type: Battery
+ - type: PredictedBattery
maxCharge: 720
startingCharge: 720
- - type: BatterySelfRecharger
+ - type: PredictedBatterySelfRecharger
autoRechargeRate: 12 # takes 1 minute to charge itself back to full
- type: entity
state: o2
shader: unshaded
visible: false
- - type: Battery
+ - type: PredictedBattery
startingCharge: 0
- type: entity
- map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2
shader: unshaded
- - type: Battery
+ - type: PredictedBattery
maxCharge: 1200
startingCharge: 1200
- - type: BatterySelfRecharger
+ - type: PredictedBatterySelfRecharger
autoRechargeRate: 40
# Power cage (big heavy power cell for big devices)
- map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2
shader: unshaded
- - type: Battery
+ - type: PredictedBattery
maxCharge: 1400
startingCharge: 1400
- map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2
shader: unshaded
- - type: Battery
+ - type: PredictedBattery
maxCharge: 2700
startingCharge: 2700
- map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2
shader: unshaded
- - type: Battery
+ - type: PredictedBattery
maxCharge: 6200
startingCharge: 6200
state: o2
shader: unshaded
visible: false
- - type: Battery
+ - type: PredictedBattery
maxCharge: 1400
startingCharge: 0
state: o2
shader: unshaded
visible: false
- - type: Battery
+ - type: PredictedBattery
startingCharge: 0
- type: entity
state: o2
shader: unshaded
visible: false
- - type: Battery
+ - type: PredictedBattery
startingCharge: 0
- type: MultiHandedItem
- type: ToggleCellDraw
- type: PowerCellDraw
- useRate: 100
+ useCharge: 100
- type: entity
id: DefibrillatorEmpty
size: Normal
- type: ToggleCellDraw
- type: PowerCellDraw
- useRate: 100
+ useCharge: 100
- type: Defibrillator
zapHeal:
types:
- 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
parent: [ HandheldHealthAnalyzerUnpowered, PowerCellSlotSmallItem]
suffix: ""
components:
+ - type: PredictedBatteryVisuals
- type: PowerCellDraw
drawRate: 1.2 #Calculated for 5 minutes on a small cell
- type: ToggleCellDraw
slots:
cell_slot:
name: power-cell-slot-component-slot-name-default
+ - type: Sprite
+ layers:
+ - state: icon
+ - state: analyzer
+ shader: unshaded
+ visible: false
+ map: [ "enum.PowerDeviceVisualLayers.Powered" ]
components:
- type: PowerCellDraw
drawRate: 1
- useRate: 0
+ useCharge: 0
- type: ToggleCellDraw
- type: entity
components:
- type: PowerCellDraw
drawRate: 1
- useRate: 0
+ useCharge: 0
- type: ToggleCellDraw
- type: entity
- type: ToggleCellDraw
- type: PowerCellDraw
drawRate: 2.4 # ~5 minutes on a medium power cell.
- useRate: 0
+ useCharge: 0
- type: entity
id: MineralScannerEmpty
- type: ToggleCellDraw
- type: PowerCellDraw
drawRate: 1.2 # ~10 minutes on a medium power cell.
- useRate: 0
+ useCharge: 0
- type: entity
id: AdvancedMineralScannerEmpty
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
- SemiAuto
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/laser.ogg
- - type: Battery
+ - type: PredictedBattery
maxCharge: 1000
startingCharge: 1000
- type: StaticPrice
selectedMode: SemiAuto
availableModes:
- SemiAuto
- - type: HitscanBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: RedLaser
fireCost: 62.5
- type: StaticPrice
price: 420
+ - type: Tag
+ tags:
+ - LaserWeapon
- type: entity
id: BaseWeaponPowerCell
- SemiAuto
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/laser.ogg
- - type: HitscanBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: RedLightLaser
fireCost: 50
- type: ItemSlots
magState: mag
steps: 5
zeroVisible: true
+ - type: Tag
+ tags:
+ - Sidearm
+ - LaserWeapon
- type: entity
name: retro laser blaster
shader: unshaded
- type: Item
storedOffset: 0,-5
- - type: HitscanBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: RedMediumLaser
fireCost: 62.5
- type: MagazineVisuals
steps: 5
zeroVisible: true
- type: Appearance
+ - type: Tag
+ tags:
+ - Sidearm
+ - LaserWeapon
- type: entity
name: makeshift laser pistol
- type: Appearance
- type: Clothing
sprite: Objects/Weapons/Guns/Battery/makeshift.rsi
- - type: HitscanBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: RedLaser
fireCost: 62.5
- - type: Battery
+ - type: PredictedBattery
maxCharge: 500
startingCharge: 500
+ - type: Tag
+ tags:
+ - Sidearm
+ - LaserWeapon
- type: entity
name: tesla gun
path: /Audio/Effects/Lightning/lightningshock.ogg
params:
variation: 0.2
- - type: ProjectileBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: TeslaGunBullet
fireCost: 300
- type: MagazineVisuals
components:
- type: StaticPrice
price: 300
- - type: HitscanBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: RedLaserPractice
fireCost: 62.5
- type: PacifismAllowedGun
- SemiAuto
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/laser3.ogg
- - type: HitscanBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: Pulse
fireCost: 200
- - type: Battery
+ - type: PredictedBattery
maxCharge: 2000
startingCharge: 2000
+ - type: Tag
+ tags:
+ - Sidearm
+ - LaserWeapon
- type: entity
name: pulse carbine
- FullAuto
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/laser3.ogg
- - type: HitscanBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: Pulse
fireCost: 200
- - type: Battery
+ - type: PredictedBattery
maxCharge: 5000
startingCharge: 5000
fireRate: 1.5
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/laser3.ogg
- - type: HitscanBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: Pulse
fireCost: 100
- - type: Battery
+ - type: PredictedBattery
maxCharge: 40000
startingCharge: 40000
fireRate: 1.5
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/laser_cannon.ogg
- - type: HitscanBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: RedHeavyLaser
fireCost: 100
- type: Tag
path: /Audio/Weapons/emitter.ogg
params:
pitch: 2
- - type: ProjectileBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: AntiParticlesProjectile
fireCost: 500
- - type: Battery
+ - type: PredictedBattery
maxCharge: 10000
startingCharge: 10000
- type: Gun
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/laser3.ogg
- - type: HitscanBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: XrayLaser
fireCost: 100
- type: MagazineVisuals
projectileSpeed: 35 # any higher and this causes issues in lag
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/taser2.ogg
- - type: ProjectileBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: BulletDisablerPractice
fireCost: 62.5
- type: Tag
slots:
- suitStorage
- Belt
- - type: ProjectileBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: BulletDisabler
fireCost: 62.5
- type: GuideHelp
- FullAuto
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/taser2.ogg
- - type: ProjectileBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: BulletDisablerSmg
fireCost: 25
- type: MagazineVisuals
fireRate: 0.5
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/taser.ogg
- - type: ProjectileBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: BulletTaser
fireCost: 200
- type: MagazineVisuals
path: /Audio/Effects/tesla_collapse.ogg # The wrath of god...
params:
volume: -6
- - type: ProjectileBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: BulletTaserSuper
fireCost: 200
- type: Gun
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/laser_cannon.ogg
- - type: HitscanBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: RedMediumLaser
fireCost: 100
- - type: BatterySelfRecharger
+ - type: PredictedBatterySelfRecharger
autoRechargeRate: 40
- type: MagazineVisuals
magState: mag
- HighRiskItem
- Sidearm
- WeaponAntiqueLaser
+ - LaserWeapon
- type: StaticPrice
price: 750
- type: StealTarget
- type: Gun
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/laser_cannon.ogg
- - type: HitscanBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: RedMediumLaser
fireCost: 100
- - type: BatterySelfRecharger
+ - type: PredictedBatterySelfRecharger
autoRechargeRate: 30
- type: MagazineVisuals
magState: mag
- type: Appearance
- type: StaticPrice
price: 63
+ - type: Tag
+ tags:
+ - Sidearm
+ - LaserWeapon
- type: entity
name: C.H.I.M.P. handcannon
fireRate: 1.5
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/taser2.ogg
- - type: ProjectileBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: AnomalousParticleDeltaStrong
fireCost: 100
- type: BatteryWeaponFireModes
fireRate: 1
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/laser_clown.ogg
- - type: HitscanBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: RedMediumLaser
fireCost: 100
- - type: BatterySelfRecharger
+ - type: PredictedBatterySelfRecharger
autoRechargeRate: 40
- type: StaticPrice
price: 750
+ - type: Tag
+ tags:
+ - Sidearm
+ - LaserWeapon
- type: entity
name: energy shotgun
fireRate: 2
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/laser_cannon.ogg
- - type: ProjectileBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: BulletLaserSpreadNarrow
fireCost: 80
- type: BatteryWeaponFireModes
sprite: Objects/Weapons/Guns/Battery/inhands_64x.rsi
heldPrefix: energy
- type: GunRequiresWield #remove when inaccuracy on spreads is fixed
- - type: Battery
+ - type: PredictedBattery
maxCharge: 480
startingCharge: 480
- type: Gun
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/laser_cannon.ogg
- - type: ProjectileBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: BulletLaserMagnum
fireCost: 150
- type: BatteryWeaponFireModes
- proto: BulletDisabler
fireCost: 62.5
pacifismAllowedMode: true
- - type: BatterySelfRecharger
+ - type: PredictedBatterySelfRecharger
autoRechargeRate: 48
autoRechargePauseTime: 10
fireRate: 1
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/taser2.ogg
- - type: ProjectileBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: BoltTempgunCold
fireCost: 100
- type: BatteryWeaponFireModes
fireCost: 100
- proto: BoltTempgunHot
fireCost: 100
- - type: Battery
+ - type: PredictedBattery
maxCharge: 1000
startingCharge: 1000
- type: StaticPrice
parent: [WeaponAdvancedLaser, BaseXenoborgContraband]
id: XenoborgHeavyLaserGun
components:
- - type: HitscanBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: RedHeavyLaser
- 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
- 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
- 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
fireRate: 1.5
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/taser2.ogg
- - type: ProjectileBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: BulletEnergyTurretLaser
fireCost: 100
- type: Battery
rootTask:
task: EnergyTurretCompound
- type: StaticPrice
- price: 200
\ No newline at end of file
+ price: 200
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
- type: NpcFactionMember
factions:
- AllHostile
- - type: ProjectileBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: BulletEnergyTurretDisabler
fireCost: 100
- type: BatteryWeaponFireModes
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
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
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
- type: Appearance
- type: Charger
slotId: charger_slot
+ - type: ApcPowerReceiver
+ powerLoad: 1 # same as draw given in ChargerComponent.PassiveDraw; increased when it is actually charging something
- type: Anchorable
delay: 1
- type: Destructible
ejectOnInteract: true
whitelist:
components:
- - HitscanBatteryAmmoProvider
- - ProjectileBatteryAmmoProvider
+ - BatteryAmmoProvider
- Stunbaton
- PowerCell
blacklist:
ejectOnInteract: true
whitelist:
components:
- - HitscanBatteryAmmoProvider
- - ProjectileBatteryAmmoProvider
+ - BatteryAmmoProvider
- Stunbaton
- PowerCell
blacklist:
ejectOnInteract: true
whitelist:
components:
- - HitscanBatteryAmmoProvider
- - ProjectileBatteryAmmoProvider
+ - BatteryAmmoProvider
- Stunbaton
blacklist:
tags:
tags:
- PowerCell
- PowerCellSmall
- - type: HitscanBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: RedLightLaser
fireCost: 50
whitelist:
tags:
- PowerCage
- - type: HitscanBatteryAmmoProvider
+ - type: BatteryAmmoProvider
proto: RedShuttleLaser
fireCost: 150
## 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.