using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Globalization; namespace Barotrauma.Items.Components { partial class PowerContainer : Powered, IDrawableComponent, IServerSerializable, IClientSerializable { //[power/min] private float capacity; private float adjustedCapacity; private float charge, prevCharge; //how fast the battery can be recharged private float maxRechargeSpeed; //how fast it's currently being recharged (can be changed, so that //charging can be slowed down or disabled if there's a shortage of power) private float rechargeSpeed; private float lastSentCharge; //charge indicator description protected Vector2 indicatorPosition, indicatorSize; protected bool isHorizontal; protected override PowerPriority Priority { get { return PowerPriority.Battery; } } private float currPowerOutput; public float CurrPowerOutput { get { return currPowerOutput; } private set { System.Diagnostics.Debug.Assert(value >= 0.0f, $"Tried to set PowerContainer's output to a negative value ({value})"); currPowerOutput = Math.Max(0, value); } } [Serialize("0,0", IsPropertySaveable.Yes, description: "The position of the progress bar indicating the charge of the item. In pixels as an offset from the upper left corner of the sprite.")] public Vector2 IndicatorPosition { get { return indicatorPosition; } set { indicatorPosition = value; } } [Serialize("0,0", IsPropertySaveable.Yes, description: "The size of the progress bar indicating the charge of the item (in pixels).")] public Vector2 IndicatorSize { get { return indicatorSize; } set { indicatorSize = value; } } [Serialize(false, IsPropertySaveable.Yes, description: "Should the progress bar indicating the charge of the item fill up horizontally or vertically.")] public bool IsHorizontal { get { return isHorizontal; } set { isHorizontal = value; } } [Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "Maximum output of the device when fully charged (kW).")] public float MaxOutPut { set; get; } [Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "The maximum capacity of the device (kW * min). For example, a value of 1000 means the device can output 100 kilowatts of power for 10 minutes, or 1000 kilowatts for 1 minute.")] public float Capacity { get => capacity; set { capacity = Math.Max(value, 1.0f); adjustedCapacity = GetCapacity(); } } [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "The current charge of the device.")] public float Charge { get { return charge; } set { if (!MathUtils.IsValid(value)) return; charge = MathHelper.Clamp(value, 0.0f, adjustedCapacity); //send a network event if the charge has changed by more than 5% if (Math.Abs(charge - lastSentCharge) / adjustedCapacity > 0.05f) { #if SERVER if (GameMain.Server != null && (!item.Submarine?.Loading ?? true)) { item.CreateServerEvent(this); } #endif lastSentCharge = charge; } } } public float ChargePercentage => MathUtils.Percentage(Charge, adjustedCapacity); [Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "How fast the device can be recharged. For example, a recharge speed of 100 kW and a capacity of 1000 kW*min would mean it takes 10 minutes to fully charge the device.")] public float MaxRechargeSpeed { get { return maxRechargeSpeed; } set { maxRechargeSpeed = Math.Max(value, 1.0f); } } [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "The current recharge speed of the device.")] public float RechargeSpeed { get { return rechargeSpeed; } set { if (!MathUtils.IsValid(value)) return; rechargeSpeed = MathHelper.Clamp(value, 0.0f, maxRechargeSpeed); rechargeSpeed = MathUtils.RoundTowardsClosest(rechargeSpeed, Math.Max(maxRechargeSpeed * 0.1f, 1.0f)); if (isRunning) { HasBeenTuned = true; } } } [Serialize(false, IsPropertySaveable.Yes, description: "If true, the recharge speed (and power consumption) of the device goes up exponentially as the recharge rate is increased.")] public bool ExponentialRechargeSpeed { get; set; } private float efficiency; [Editable(minValue: 0.0f, maxValue: 1.0f, decimals: 2), Serialize(0.95f, IsPropertySaveable.Yes, description: "The amount of power you can get out of a item relative to the amount of power that's put into it.")] public float Efficiency { get { return efficiency; } set { efficiency = MathHelper.Clamp(value, 0.0f, 1.0f); } } private bool flipIndicator; [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Should the progress bar indicating the charge be flipped to fill from the other side.")] public bool FlipIndicator { get { return flipIndicator; } set { flipIndicator = value; } } public bool OutputDisabled { get; private set; } public float RechargeRatio => RechargeSpeed / MaxRechargeSpeed; public const float aiRechargeTargetRatio = 0.5f; private bool isRunning; public bool HasBeenTuned { get; private set; } public PowerContainer(Item item, ContentXElement element) : base(item, element) { IsActive = true; InitProjSpecific(); prevCharge = Charge; } partial void InitProjSpecific(); public override bool Pick(Character picker) { return picker != null; } public override void Update(float deltaTime, Camera cam) { if (item.Connections == null) { IsActive = false; return; } adjustedCapacity = GetCapacity(); isRunning = true; float chargeRatio = charge / adjustedCapacity; if (chargeRatio > 0.0f) { ApplyStatusEffects(ActionType.OnActive, deltaTime); } float loadReading = 0; if (powerOut != null && powerOut.Grid != null) { loadReading = powerOut.Grid.Load; } item.SendSignal(((int)Math.Round(CurrPowerOutput)).ToString(), "power_value_out"); item.SendSignal(((int)Math.Round(loadReading)).ToString(), "load_value_out"); item.SendSignal(((int)Math.Round(Charge)).ToString(), "charge"); item.SendSignal(((int)Math.Round(Charge / adjustedCapacity * 100)).ToString(), "charge_%"); item.SendSignal(((int)Math.Round(RechargeSpeed / maxRechargeSpeed * 100)).ToString(), "charge_rate"); } /// /// Returns the power consumption if checking the powerIn connection, or a negative value if the output can provide power when checking powerOut. /// Power consumption is proportional to set recharge speed and if there is less than max charge. /// public override float GetCurrentPowerConsumption(Connection connection = null) { if (connection == powerIn) { //Don't draw power if fully charged if (charge >= adjustedCapacity) { charge = adjustedCapacity; return 0; } else { if (item.Condition <= 0.0f) { return 0.0f; } float missingCharge = adjustedCapacity - charge; float targetRechargeSpeed = rechargeSpeed; if (ExponentialRechargeSpeed) { targetRechargeSpeed = MathF.Pow(rechargeSpeed / maxRechargeSpeed, 2) * maxRechargeSpeed; } //For the last kwMin scale the recharge rate linearly to prevent overcharging and to have a smooth cutoff if (missingCharge < 1.0f) { targetRechargeSpeed *= missingCharge; } return MathHelper.Clamp(targetRechargeSpeed, 0, MaxRechargeSpeed); } } else { CurrPowerOutput = 0; return charge > 0 ? -1 : 0; } } /// /// Minimum and maximum output for the queried connection. /// Powerin min max equals CurrPowerConsumption as its abnormal for there to be power out. /// PowerOut min power out is zero and max is the maxout unless below 10% charge where /// the output is scaled relative to the 10% charge. /// /// Connection being queried /// Current grid load /// Minimum and maximum power output for the connection public override PowerRange MinMaxPowerOut(Connection connection, float load = 0) { if (OutputDisabled) { return PowerRange.Zero; } if (connection == powerOut) { float maxOutput; float chargeRatio = prevCharge / adjustedCapacity; if (chargeRatio < 0.1f) { maxOutput = Math.Max(chargeRatio * 10.0f, 0.0f) * MaxOutPut; } else { maxOutput = MaxOutPut; } //Limit max power out to not exceed the charge of the container maxOutput = Math.Min(maxOutput, prevCharge * 60 / UpdateInterval); return new PowerRange(0.0f, maxOutput); } return PowerRange.Zero; } /// /// Finalized power out from the container for the connection, provided the given grid information /// Output power based on the maxpower all batteries can output. So all batteries can /// equally share powerout based on their output capabilities. /// /// /// /// /// /// public override float GetConnectionPowerOut(Connection connection, float power, PowerRange minMaxPower, float load) { if (OutputDisabled) { return 0; } //Only power out connection can provide power and Max poweroutput can't be negative if (connection == powerOut && minMaxPower.Max > 0) { //Set power output based on the relative max power output capabilities and load demand CurrPowerOutput = MathHelper.Clamp((load - power) / minMaxPower.Max, 0, 1) * MinMaxPowerOut(connection, load).Max; return CurrPowerOutput; } return 0.0f; } /// /// When the corresponding grid connection is resolved, adjust the container's charge. /// public override void GridResolved(Connection conn) { if (conn == powerIn) { //Increase charge based on how much power came in from the grid Charge += (CurrPowerConsumption * Voltage) / 60 * UpdateInterval * efficiency; } else { //Decrease charge based on how much power is leaving the device Charge = Math.Clamp(Charge - CurrPowerOutput / 60 * UpdateInterval, 0, adjustedCapacity); prevCharge = Charge; } } public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } if (objective.Override) { HasBeenTuned = false; } if (HasBeenTuned) { return true; } float targetRatio = objective.Option.IsEmpty || objective.Option == "charge" ? aiRechargeTargetRatio : -1; if (targetRatio > 0 || float.TryParse(objective.Option.Value, out targetRatio)) { if (Math.Abs(rechargeSpeed - maxRechargeSpeed * targetRatio) > 0.05f) { #if SERVER item.CreateServerEvent(this); #endif RechargeSpeed = maxRechargeSpeed * targetRatio; #if CLIENT if (rechargeSpeedSlider != null) { rechargeSpeedSlider.BarScroll = RechargeSpeed / Math.Max(maxRechargeSpeed, 1.0f); } #endif if (character.IsOnPlayerTeam) { character.Speak(TextManager.GetWithVariables("DialogChargeBatteries", ("[itemname]", item.Name, FormatCapitals.Yes), ("[rate]", ((int)(rechargeSpeed / maxRechargeSpeed * 100.0f)).ToString(), FormatCapitals.No)).Value, null, 1.0f, "chargebattery".ToIdentifier(), 10.0f); } } } else { if (rechargeSpeed > 0.0f) { #if SERVER item.CreateServerEvent(this); #endif RechargeSpeed = 0.0f; #if CLIENT if (rechargeSpeedSlider != null) { rechargeSpeedSlider.BarScroll = RechargeSpeed / Math.Max(maxRechargeSpeed, 1.0f); } #endif if (character.IsOnPlayerTeam) { character.Speak(TextManager.GetWithVariables("DialogStopChargingBatteries", ("[itemname]", item.Name, FormatCapitals.Yes), ("[rate]", ((int)(rechargeSpeed / maxRechargeSpeed * 100.0f)).ToString(), FormatCapitals.No)).Value, null, 1.0f, "chargebattery".ToIdentifier(), 10.0f); } } } return true; } public override void ReceiveSignal(Signal signal, Connection connection) { if (connection.IsPower) { return; } switch (connection.Name) { case "disable_output": OutputDisabled = signal.value != "0"; break; case "set_rate": if (float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempSpeed)) { if (!MathUtils.IsValid(tempSpeed)) { return; } float rechargeRate = MathHelper.Clamp(tempSpeed / 100.0f, 0.0f, 1.0f); RechargeSpeed = rechargeRate * MaxRechargeSpeed; #if CLIENT if (rechargeSpeedSlider != null) { rechargeSpeedSlider.BarScroll = rechargeRate; } #endif } break; } } public float GetCapacity() => item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.BatteryCapacity, Capacity); } }