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