using System.Numerics; using Content.Server._NF.Atmos.Components; using Content.Server.Administration.Logs; using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.Piping.Components; using Content.Server.Audio; using Content.Server.Construction; using Content.Server.Hands.Systems; using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.NodeGroups; using Content.Server.NodeContainer.Nodes; using Content.Server.Power.Components; using Content.Server.Stack; using Content.Shared._NF.Atmos.BUI; using Content.Shared._NF.Atmos.Components; using Content.Shared._NF.Atmos.Events; using Content.Shared._NF.Atmos.Prototypes; using Content.Shared._NF.Atmos.Systems; using Content.Shared._NF.Atmos.Visuals; using Content.Shared._NF.Bank.Components; using Content.Shared.Atmos; using Content.Shared.Atmos.Piping.Binary.Components; using Content.Shared.Coordinates; using Content.Shared.Database; using Content.Shared.Power; using Robust.Server.Audio; using Robust.Server.GameObjects; using Robust.Shared.Prototypes; using Robust.Shared.Random; namespace Content.Server._NF.Atmos.Systems; /// /// System for handling gas deposits and machines for extracting from gas deposits /// public sealed class GasDepositSystem : SharedGasDepositSystem { [Dependency] private readonly AmbientSoundSystem _ambientSound = default!; [Dependency] private readonly AppearanceSystem _appearance = default!; [Dependency] private readonly AtmosphereSystem _atmosphere = default!; [Dependency] private readonly AudioSystem _audio = default!; [Dependency] private readonly IAdminLogManager _adminLog = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly HandsSystem _hands = default!; [Dependency] private readonly NodeContainerSystem _nodeContainer = default!; [Dependency] private readonly StackSystem _stack = default!; [Dependency] private readonly TransformSystem _transform = default!; /// /// The fraction that a deposit's volume should be depleted to before it is considered "low volume". /// private const float LowMoleCoefficient = 0.25f; /// /// The maximum distance to check for nearby gas sale points when selling gas. /// private const double DefaultMaxSalePointDistance = 8.0; /// public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnRandomDepositMapInit); SubscribeLocalEvent(OnExtractorMapInit); SubscribeLocalEvent(OnExtractorUiOpened); SubscribeLocalEvent(OnPowerChanged); SubscribeLocalEvent(OnExtractorUpdate); SubscribeLocalEvent(OnExtractorRefreshParts); SubscribeLocalEvent(OnExtractorUpgradeExamine); SubscribeLocalEvent( OnOutputPressureChangeMessage); SubscribeLocalEvent(OnToggleStatusMessage); SubscribeLocalEvent(OnSalePointUpdate); SubscribeLocalEvent(OnConsoleUiOpened); SubscribeLocalEvent(OnConsoleSell); SubscribeLocalEvent(OnConsoleRefresh); } private void OnExtractorMapInit(Entity ent, ref MapInitEvent args) { UpdateAppearance(ent); } private void OnExtractorUiOpened(Entity ent, ref BoundUIOpenedEvent args) { Dirty(ent); } private void OnPowerChanged(Entity ent, ref PowerChangedEvent args) { UpdateAppearance(ent); } public void OnRandomDepositMapInit(Entity ent, ref MapInitEvent args) { EnsureComp(ent, out var deposit); if (!_prototype.TryIndex(ent.Comp.DepositPrototype, out var depositPrototype)) { if (!_prototype.TryGetRandom(_random, out var randomPrototype)) return; depositPrototype = (GasDepositPrototype)randomPrototype; } for (var i = 0; i < depositPrototype.Gases.Length && i < Atmospherics.TotalNumberOfGases; i++) { var gasRange = depositPrototype.Gases[i]; var gasAmount = gasRange[0] + _random.NextFloat() * (gasRange[1] - gasRange[0]); gasAmount *= ent.Comp.Scale; deposit.Deposit.SetMoles(i, gasAmount); } deposit.LowMoles = deposit.Deposit.TotalMoles * LowMoleCoefficient; } private void OnExtractorUpdate(Entity ent, ref AtmosDeviceUpdateEvent args) { if (!ent.Comp.Enabled || !TryComp(ent.Comp.DepositEntity, out GasDepositComponent? depositComp) || TryComp(ent, out var power) && !power.Powered || !_nodeContainer.TryGetNode(ent.Owner, ent.Comp.PortName, out PipeNode? port)) { _ambientSound.SetAmbience(ent, false); SetDepositState(ent, GasDepositExtractorState.Off); return; } if (depositComp.Deposit.TotalMoles < Atmospherics.GasMinMoles) { _ambientSound.SetAmbience(ent, false); SetDepositState(ent, GasDepositExtractorState.Empty); return; } // Nowhere to pipe gas, say it's blocked. if (port.NodeGroup is not PipeNet { NodeCount: > 1 } net) { _ambientSound.SetAmbience(ent, false); SetDepositState(ent, GasDepositExtractorState.Blocked); return; } var targetPressure = float.Clamp(ent.Comp.TargetPressure, 0, ent.Comp.MaxTargetPressure); // How many moles could we theoretically spawn. Cap by pressure, amount, and extractor limit. var allowableMoles = (targetPressure - net.Air.Pressure) * net.Air.Volume / (ent.Comp.OutputTemperature * Atmospherics.R); allowableMoles = float.Min(allowableMoles, ent.Comp.ExtractionRate * args.dt); if (allowableMoles < Atmospherics.GasMinMoles) { _ambientSound.SetAmbience(ent, false); SetDepositState(ent, GasDepositExtractorState.Blocked); return; } var removed = depositComp.Deposit.Remove(allowableMoles); removed.Temperature = ent.Comp.OutputTemperature; _atmosphere.Merge(net.Air, removed); _ambientSound.SetAmbience(ent, true); if (depositComp.Deposit.TotalMoles <= depositComp.LowMoles) SetDepositState(ent, GasDepositExtractorState.Low); else SetDepositState(ent, GasDepositExtractorState.On); } private void OnExtractorRefreshParts(Entity ent, ref RefreshPartsEvent args) { float componentRate; if (!args.PartRatings.TryGetValue(ent.Comp.ExtractionRateMachinePart, out componentRate)) componentRate = 1.0f; componentRate = MathF.Max(componentRate, 1.0f) - 1.0f; ent.Comp.ExtractionRate = ent.Comp.BaseExtractionRate * MathF.Pow(ent.Comp.ExtractionRateMultiplier, componentRate); } private void OnExtractorUpgradeExamine(Entity ent, ref UpgradeExamineEvent args) { if (ent.Comp.BaseExtractionRate > 0) args.AddPercentageUpgrade("gas-deposit-extraction-rate", ent.Comp.ExtractionRate / ent.Comp.BaseExtractionRate); } private void OnToggleStatusMessage(Entity ent, ref GasPressurePumpToggleStatusMessage args) { ent.Comp.Enabled = args.Enabled; _adminLog.Add(LogType.AtmosPowerChanged, LogImpact.Low, $"{ToPrettyString(args.Actor):player} set the power on {ToPrettyString(ent):device} to {args.Enabled}"); Dirty(ent); } private void OnOutputPressureChangeMessage(Entity ent, ref GasPressurePumpChangeOutputPressureMessage args) { ent.Comp.TargetPressure = Math.Clamp(args.Pressure, 0f, Atmospherics.MaxOutputPressure); _adminLog.Add(LogType.AtmosPressureChanged, LogImpact.Low, $"{ToPrettyString(args.Actor):player} set the pressure on {ToPrettyString(ent):device} to {args.Pressure}kPa"); Dirty(ent); } private void SetDepositState(Entity ent, GasDepositExtractorState newState) { if (newState != ent.Comp.LastState) { ent.Comp.LastState = newState; UpdateAppearance(ent); } } private void UpdateAppearance(Entity ent, AppearanceComponent? appearance = null) { if (!Resolve(ent, ref appearance, false)) return; var pumpOn = ent.Comp.Enabled && (!TryComp(ent, out var power) || power.Powered); if (!pumpOn) _appearance.SetData(ent, GasDepositExtractorVisuals.State, GasDepositExtractorState.Off, appearance); else _appearance.SetData(ent, GasDepositExtractorVisuals.State, ent.Comp.LastState, appearance); } // Atmos update: take any gas from the connecting network and push it into the pump. private void OnSalePointUpdate(Entity ent, ref AtmosDeviceUpdateEvent args) { if (TryComp(ent, out var power) && !power.Powered || !_nodeContainer.TryGetNode(ent.Owner, ent.Comp.InletPipePortName, out PipeNode? port)) return; if (port.Air.TotalMoles > 0) { _atmosphere.Merge(ent.Comp.GasStorage, port.Air); port.Air.Clear(); } } private void OnConsoleUiOpened(Entity ent, ref BoundUIOpenedEvent args) { UpdateConsoleInterface(ent); } private void OnConsoleRefresh(Entity ent, ref GasSaleRefreshMessage args) { UpdateConsoleInterface(ent); } private void OnConsoleSell(Entity ent, ref GasSaleSellMessage args) { var xform = Transform(ent); if (xform.GridUid is not { } gridUid) { UI.SetUiState(ent.Owner, GasSaleConsoleUiKey.Key, new GasSaleConsoleBoundUserInterfaceState(0, new GasMixture(), false)); return; } var amount = 0.0; foreach (var salePoint in GetNearbySalePoints(ent, gridUid)) { amount += _atmosphere.GetPrice(salePoint.Comp.GasStorage); salePoint.Comp.GasStorage.Clear(); } if (TryComp(ent, out var priceMod)) amount *= priceMod.Mod; var stackPrototype = _prototype.Index(ent.Comp.CashType); var stackUid = _stack.Spawn((int)amount, stackPrototype, args.Actor.ToCoordinates()); if (!_hands.TryPickupAnyHand(args.Actor, stackUid)) _transform.SetLocalRotation(stackUid, Angle.Zero); // Orient these to grid north instead of map north _audio.PlayPvs(ent.Comp.ApproveSound, ent); UI.SetUiState(ent.Owner, GasSaleConsoleUiKey.Key, new GasSaleConsoleBoundUserInterfaceState(0, new GasMixture(), false)); } private void UpdateConsoleInterface(Entity ent) { if (Transform(ent).GridUid is not { } gridUid) { UI.SetUiState(ent.Owner, GasSaleConsoleUiKey.Key, new GasSaleConsoleBoundUserInterfaceState(0, new GasMixture(), false)); return; } GetNearbyMixtures(ent, gridUid, out var mixture, out var amount); if (TryComp(ent, out var priceMod)) amount *= priceMod.Mod; UI.SetUiState(ent.Owner, GasSaleConsoleUiKey.Key, new GasSaleConsoleBoundUserInterfaceState((int)amount, mixture, mixture.TotalMoles > 0)); } private void GetNearbyMixtures(EntityUid consoleUid, EntityUid gridUid, out GasMixture mixture, out double value) { mixture = new GasMixture(); value = 0.0; foreach (var salePoint in GetNearbySalePoints(consoleUid, gridUid)) { _atmosphere.Merge(mixture, salePoint.Comp.GasStorage); value += _atmosphere.GetPrice(salePoint.Comp.GasStorage); } } private List> GetNearbySalePoints(EntityUid consoleUid, EntityUid gridUid) { List> ret = new(); var query = AllEntityQuery(); var consolePosition = Transform(consoleUid).Coordinates.Position; var maxSalePointDistance = DefaultMaxSalePointDistance; // Get the mapped checking distance from the console if (TryComp(consoleUid, out var cargoShuttleComponent)) maxSalePointDistance = cargoShuttleComponent.SellPointDistance; while (query.MoveNext(out var uid, out var comp, out var compXform)) { if (compXform.ParentUid != gridUid || !compXform.Anchored || Vector2.Distance(consolePosition, compXform.Coordinates.Position) > maxSalePointDistance) continue; ret.Add((uid, comp)); } return ret; } }