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;
}
}