using Content.Server._NF.Manufacturing.Components; using Content.Server.Atmos.EntitySystems; using Content.Server.NodeContainer.EntitySystems; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Server.Power.Nodes; using Content.Shared._NF.Power; using Content.Shared.Atmos; using Content.Shared.Atmos.Piping.Unary.Components; using Content.Shared.Examine; using Content.Shared.NodeContainer; using Content.Shared.Power; using Content.Shared.UserInterface; using Robust.Server.GameObjects; using Robust.Shared.Timing; namespace Content.Shared._NF.Manufacturing.EntitySystems; /// /// Consumes large quantities of power, scales excessive overage down to reasonable values. /// Spawns gas regularly depending on the amount of power received. /// public sealed partial class GasSpawnPowerConsumerSystem : EntitySystem { [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly AppearanceSystem _appearance = default!; [Dependency] private readonly AtmosphereSystem _atmos = default!; [Dependency] private readonly NodeContainerSystem _node = default!; [Dependency] private readonly NodeGroupSystem _nodeGroup = default!; [Dependency] private readonly UserInterfaceSystem _ui = default!; private GasMixture _mixture = new(); public override void Initialize() { base.Initialize(); UpdatesAfter.Add(typeof(PowerNetSystem)); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnUIOpen); Subs.BuiEvents( AdjustablePowerDrawUiKey.Key, subs => { subs.Event(HandleSetEnabled); subs.Event(HandleSetLoad); }); } private void OnMapInit(Entity ent, ref MapInitEvent args) { ent.Comp.NextSpawnCheck = _timing.CurTime + ent.Comp.SpawnCheckPeriod; if (TryComp(ent, out PowerConsumerComponent? power)) power.DrawRate = Math.Clamp(power.DrawRate, ent.Comp.MinimumRequestablePower, ent.Comp.MaximumRequestablePower); } private void OnExamined(Entity ent, ref ExaminedEvent args) { if (TryComp(ent, out PowerConsumerComponent? power)) { args.PushMarkup(Loc.GetString("gas-spawn-power-consumer-examine", ("actual", power.ReceivedPower), ("requested", power.DrawRate))); var powered = power.NetworkLoad.Enabled && power.NetworkLoad.ReceivingPower > 0; args.PushMarkup( Loc.GetString("power-receiver-component-on-examine-main", ("stateText", Loc.GetString(powered ? "power-receiver-component-on-examine-powered" : "power-receiver-component-on-examine-unpowered")) ) ); } } public override void Update(float frameTime) { var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var spawn, out var power)) { if (power.NetworkLoad.Enabled) spawn.AccumulatedSpawnCheckEnergy += power.NetworkLoad.ReceivingPower * frameTime; if (_timing.CurTime >= spawn.NextSpawnCheck) { spawn.NextSpawnCheck += spawn.SpawnCheckPeriod; if (!TryComp(uid, out var canister)) { spawn.AccumulatedSpawnCheckEnergy = 0; continue; } if (!float.IsFinite(spawn.AccumulatedSpawnCheckEnergy) || !float.IsPositive(spawn.AccumulatedSpawnCheckEnergy)) { spawn.AccumulatedSpawnCheckEnergy = 0; continue; } // Ensure accumulated energy is never infinite. if (!float.IsFinite(spawn.AccumulatedEnergy) || !float.IsPositive(spawn.AccumulatedEnergy)) spawn.AccumulatedEnergy = 0; // Adjust spawn check energy if (float.IsFinite(spawn.AccumulatedSpawnCheckEnergy) && float.IsPositive(spawn.AccumulatedSpawnCheckEnergy)) { var totalPeriodSeconds = (float)spawn.SpawnCheckPeriod.TotalSeconds; var effectivePower = GetEffectivePower((uid, spawn), spawn.AccumulatedSpawnCheckEnergy / totalPeriodSeconds); spawn.AccumulatedEnergy += effectivePower * totalPeriodSeconds; } spawn.AccumulatedSpawnCheckEnergy = 0; // Require at least enough energy for one mole of gas before actually producing anything. if (spawn.AccumulatedEnergy >= spawn.EnergyPerMole) { _mixture.CopyFrom(spawn.SpawnMixture); // Figure out how many moles we can spawn with the energy we have. var molesToSpawn = spawn.AccumulatedEnergy / spawn.EnergyPerMole; molesToSpawn = MathF.Min(molesToSpawn, spawn.MaximumMolesPerSecond); // Figure out how many moles will fit in the canister. var deltaP = Atmospherics.MaxOutputPressure - canister.Air.Pressure; var maxMoles = deltaP * canister.Air.Volume / (_mixture.Temperature * Atmospherics.R); molesToSpawn = MathF.Min(molesToSpawn, maxMoles); _mixture.Multiply(molesToSpawn / _mixture.TotalMoles); _atmos.Merge(canister.Air, _mixture); spawn.AccumulatedEnergy = 0; } } _appearance.SetData(uid, PowerDeviceVisuals.Powered, power.NetworkLoad.Enabled && power.NetworkLoad.ReceivingPower > 0); } } /// /// Gets the actual effective power in watts for some amount of input power. /// No range check on power. /// /// Input power level, in watts. /// Effective power, in watts. private float GetEffectivePower(Entity ent, float power) { float actualPower; if (power <= ent.Comp.LinearMaxValue) actualPower = power; else actualPower = ent.Comp.LogarithmCoefficient * MathF.Pow(ent.Comp.LogarithmRateBase, MathF.Log10(power) - ent.Comp.LogarithmSubtrahend); return actualPower; } /// /// Gets the expected gas generation rate in moles per second. /// /// Input power level, in watts /// Expected item generation time in seconds public float GetGasSpawnRate(Entity ent, float power) { if (!float.IsFinite(power) || !float.IsPositive(power)) { return 0.0f; } var numMoles = GetEffectivePower(ent, power) / ent.Comp.EnergyPerMole; return MathF.Min(numMoles, ent.Comp.MaximumMolesPerSecond); } private void OnUIOpen(Entity ent, ref AfterActivatableUIOpenEvent args) { if (TryComp(ent, out PowerConsumerComponent? power)) UpdateUI(ent, power); } private void HandleSetEnabled(Entity ent, ref AdjustablePowerDrawSetEnabledMessage args) { if (TryComp(ent, out NodeContainerComponent? node) && _node.TryGetNode(node, ent.Comp.PowerNodeName, out var deviceNode)) { deviceNode.Enabled = args.On; if (deviceNode.Enabled) _nodeGroup.QueueReflood(deviceNode); else _nodeGroup.QueueNodeRemove(deviceNode); if (TryComp(ent, out PowerConsumerComponent? power)) UpdateUI(ent, power); } } private void HandleSetLoad(Entity ent, ref AdjustablePowerDrawSetLoadMessage args) { if (args.Load >= 0 && TryComp(ent, out PowerConsumerComponent? power)) { power.DrawRate = Math.Clamp(args.Load, ent.Comp.MinimumRequestablePower, ent.Comp.MaximumRequestablePower); UpdateUI(ent, power); } } private void UpdateUI(Entity ent, PowerConsumerComponent power) { if (!_ui.IsUiOpen(ent.Owner, AdjustablePowerDrawUiKey.Key)) return; bool nodeEnabled = false; if (TryComp(ent, out NodeContainerComponent? node) && _node.TryGetNode(node, ent.Comp.PowerNodeName, out var deviceNode)) { nodeEnabled = deviceNode.Enabled; } _ui.SetUiState( ent.Owner, AdjustablePowerDrawUiKey.Key, new AdjustablePowerDrawBuiState { On = nodeEnabled, Load = power.DrawRate, Text = Loc.GetString("gas-spawn-power-consumer-value", ("value", GetGasSpawnRate(ent, power.DrawRate))) }); } }