6
2026-01-18 12:53:36 +03:00

345 lines
14 KiB
C#

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;
/// <summary>
/// System for handling gas deposits and machines for extracting from gas deposits
/// </summary>
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!;
/// <summary>
/// The fraction that a deposit's volume should be depleted to before it is considered "low volume".
/// </summary>
private const float LowMoleCoefficient = 0.25f;
/// <summary>
/// The maximum distance to check for nearby gas sale points when selling gas.
/// </summary>
private const double DefaultMaxSalePointDistance = 8.0;
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RandomGasDepositComponent, MapInitEvent>(OnRandomDepositMapInit);
SubscribeLocalEvent<GasDepositExtractorComponent, MapInitEvent>(OnExtractorMapInit);
SubscribeLocalEvent<GasDepositExtractorComponent, BoundUIOpenedEvent>(OnExtractorUiOpened);
SubscribeLocalEvent<GasDepositExtractorComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<GasDepositExtractorComponent, AtmosDeviceUpdateEvent>(OnExtractorUpdate);
SubscribeLocalEvent<GasDepositExtractorComponent, RefreshPartsEvent>(OnExtractorRefreshParts);
SubscribeLocalEvent<GasDepositExtractorComponent, UpgradeExamineEvent>(OnExtractorUpgradeExamine);
SubscribeLocalEvent<GasDepositExtractorComponent, GasPressurePumpChangeOutputPressureMessage>(
OnOutputPressureChangeMessage);
SubscribeLocalEvent<GasDepositExtractorComponent, GasPressurePumpToggleStatusMessage>(OnToggleStatusMessage);
SubscribeLocalEvent<GasSalePointComponent, AtmosDeviceUpdateEvent>(OnSalePointUpdate);
SubscribeLocalEvent<GasSaleConsoleComponent, BoundUIOpenedEvent>(OnConsoleUiOpened);
SubscribeLocalEvent<GasSaleConsoleComponent, GasSaleSellMessage>(OnConsoleSell);
SubscribeLocalEvent<GasSaleConsoleComponent, GasSaleRefreshMessage>(OnConsoleRefresh);
}
private void OnExtractorMapInit(Entity<GasDepositExtractorComponent> ent, ref MapInitEvent args)
{
UpdateAppearance(ent);
}
private void OnExtractorUiOpened(Entity<GasDepositExtractorComponent> ent, ref BoundUIOpenedEvent args)
{
Dirty(ent);
}
private void OnPowerChanged(Entity<GasDepositExtractorComponent> ent, ref PowerChangedEvent args)
{
UpdateAppearance(ent);
}
public void OnRandomDepositMapInit(Entity<RandomGasDepositComponent> ent, ref MapInitEvent args)
{
EnsureComp<GasDepositComponent>(ent, out var deposit);
if (!_prototype.TryIndex(ent.Comp.DepositPrototype, out var depositPrototype))
{
if (!_prototype.TryGetRandom<GasDepositPrototype>(_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<GasDepositExtractorComponent> ent, ref AtmosDeviceUpdateEvent args)
{
if (!ent.Comp.Enabled
|| !TryComp(ent.Comp.DepositEntity, out GasDepositComponent? depositComp)
|| TryComp<ApcPowerReceiverComponent>(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<GasDepositExtractorComponent> 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<GasDepositExtractorComponent> 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<GasDepositExtractorComponent> 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<GasDepositExtractorComponent> 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<GasDepositExtractorComponent> ent, GasDepositExtractorState newState)
{
if (newState != ent.Comp.LastState)
{
ent.Comp.LastState = newState;
UpdateAppearance(ent);
}
}
private void UpdateAppearance(Entity<GasDepositExtractorComponent> ent, AppearanceComponent? appearance = null)
{
if (!Resolve(ent, ref appearance, false))
return;
var pumpOn = ent.Comp.Enabled && (!TryComp<ApcPowerReceiverComponent>(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<GasSalePointComponent> ent, ref AtmosDeviceUpdateEvent args)
{
if (TryComp<ApcPowerReceiverComponent>(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<GasSaleConsoleComponent> ent, ref BoundUIOpenedEvent args)
{
UpdateConsoleInterface(ent);
}
private void OnConsoleRefresh(Entity<GasSaleConsoleComponent> ent, ref GasSaleRefreshMessage args)
{
UpdateConsoleInterface(ent);
}
private void OnConsoleSell(Entity<GasSaleConsoleComponent> 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<MarketModifierComponent>(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<GasSaleConsoleComponent> 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<MarketModifierComponent>(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<Entity<GasSalePointComponent>> GetNearbySalePoints(EntityUid consoleUid, EntityUid gridUid)
{
List<Entity<GasSalePointComponent>> ret = new();
var query = AllEntityQuery<GasSalePointComponent, TransformComponent>();
var consolePosition = Transform(consoleUid).Coordinates.Position;
var maxSalePointDistance = DefaultMaxSalePointDistance;
// Get the mapped checking distance from the console
if (TryComp<GasSaleConsoleComponent>(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;
}
}