Atmos pipe rework (#3833)

* Initial

* Cleanup a bunch of things

* some changes dunno

* RequireAnchored

* a

* stuff

* more work

* Lots of progress

* delete pipe visualizer

* a

* b

* pipenet and pipenode cleanup

* Fixes

* Adds GasValve

* Adds GasMiner

* Fix stuff, maybe?

* More fixes

* Ignored components on the client

* Adds thermomachine behavior, change a bunch of stuff

* Remove Anchored

* some work, but it's shitcode

* significantly more ECS

* ECS AtmosDevices

* Cleanup

* fix appearance

* when the pipe direction is sus

* Gas tanks and canisters

* pipe anchoring and stuff

* coding is my passion

* Unsafe pipes take longer to unanchor

* turns out we're no longer using eris canisters

* Gas canister inserted tank appearance, improvements

* Work on a bunch of appearances

* Scrubber appearance

* Reorganize AtmosphereSystem.Piping into a bunch of different systems

* Appearance for vent/scrubber/pump turns off when leaving atmosphere

* ThermoMachine appearance

* Cleanup gas tanks

* Remove passive gate unused imports

* remove old canister UI functionality

* PipeNode environment air, make everything use AssumeAir instead of merging manually

* a

* Reorganize atmos to follow new structure

* ?????

* Canister UI, restructure client

* Restructure shared

* Fix build tho

* listen, at least the canister UI works entirely...

* fix build : )

* Atmos device prototypes have names and descriptions

* gas canister ui slider doesn't jitter

* trinary prototypes

* sprite for miners

* ignore components

* fix YAML

* Fix port system doing useless thing

* Fix build

* fix thinking moment

* fix build again because

* canister direction

* pipenode is a word

* GasTank Air will throw on invalid states

* fix build....

* Unhardcode volume pump thresholds

* Volume pump and filter take time into account

* Rename Join/Leave atmosphere events to AtmosDeviceEnabled/Disabled Event

* Gas tank node volume is set by initial mixtuer

* I love node container
This commit is contained in:
Vera Aguilera Puerto
2021-06-19 13:25:05 +02:00
committed by GitHub
parent cfc3f2e7fc
commit a2b737d945
250 changed files with 3964 additions and 3163 deletions

View File

@@ -1,179 +0,0 @@
#nullable enable
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Atmos;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos
{
[RegisterComponent]
public class AirtightComponent : Component, IMapInit
{
[Dependency] private readonly IMapManager _mapManager = default!;
private (GridId, Vector2i) _lastPosition;
private AtmosphereSystem _atmosphereSystem = default!;
public override string Name => "Airtight";
[DataField("airBlockedDirection", customTypeSerializer: typeof(FlagSerializer<AtmosDirectionFlags>))]
[ViewVariables]
private int _initialAirBlockedDirection = (int) AtmosDirection.All;
[ViewVariables]
private int _currentAirBlockedDirection;
[DataField("airBlocked")]
private bool _airBlocked = true;
[DataField("fixVacuum")]
private bool _fixVacuum = true;
[ViewVariables]
[DataField("rotateAirBlocked")]
private bool _rotateAirBlocked = true;
[ViewVariables]
[DataField("fixAirBlockedDirectionInitialize")]
private bool _fixAirBlockedDirectionInitialize = true;
[ViewVariables]
[DataField("noAirWhenFullyAirBlocked")]
public bool NoAirWhenFullyAirBlocked { get; } = true;
[ViewVariables(VVAccess.ReadWrite)]
public bool AirBlocked
{
get => _airBlocked;
set
{
_airBlocked = value;
UpdatePosition();
}
}
public AtmosDirection AirBlockedDirection
{
get => (AtmosDirection)_currentAirBlockedDirection;
set
{
_currentAirBlockedDirection = (int) value;
_initialAirBlockedDirection = (int)Rotate(AirBlockedDirection, -Owner.Transform.LocalRotation);
UpdatePosition();
}
}
[ViewVariables]
public bool FixVacuum => _fixVacuum;
public override void Initialize()
{
base.Initialize();
_atmosphereSystem = EntitySystem.Get<AtmosphereSystem>();
if (_fixAirBlockedDirectionInitialize)
RotateEvent(new RotateEvent(Owner, Angle.Zero, Owner.Transform.WorldRotation));
// Adding this component will immediately anchor the entity, because the atmos system
// requires airtight entities to be anchored for performance.
Owner.Transform.Anchored = true;
UpdatePosition();
}
public void RotateEvent(RotateEvent ev)
{
if (!_rotateAirBlocked || ev.Sender != Owner || _initialAirBlockedDirection == (int)AtmosDirection.Invalid)
return;
_currentAirBlockedDirection = (int) Rotate((AtmosDirection)_initialAirBlockedDirection, ev.NewRotation);
}
private AtmosDirection Rotate(AtmosDirection myDirection, Angle myAngle)
{
var newAirBlockedDirs = AtmosDirection.Invalid;
if (myAngle == Angle.Zero)
return myDirection;
// TODO ATMOS MULTIZ When we make multiZ atmos, special case this.
for (var i = 0; i < Atmospherics.Directions; i++)
{
var direction = (AtmosDirection) (1 << i);
if (!myDirection.IsFlagSet(direction)) continue;
var angle = direction.ToAngle();
angle += myAngle;
newAirBlockedDirs |= angle.ToAtmosDirectionCardinal();
}
return newAirBlockedDirs;
}
/// <inheritdoc />
public void MapInit()
{
if (Owner.Transform.Anchored)
{
var grid = _mapManager.GetGrid(Owner.Transform.GridID);
_lastPosition = (Owner.Transform.GridID, grid.TileIndicesFor(Owner.Transform.Coordinates));
}
UpdatePosition();
}
/// <inheritdoc />
protected override void Shutdown()
{
base.Shutdown();
_airBlocked = false;
UpdatePosition(_lastPosition.Item1, _lastPosition.Item2);
if (_fixVacuum)
{
_atmosphereSystem.GetGridAtmosphere(_lastPosition.Item1)?.FixVacuum(_lastPosition.Item2);
}
}
public void OnTransformMove()
{
UpdatePosition(_lastPosition.Item1, _lastPosition.Item2);
UpdatePosition();
if (Owner.Transform.Anchored)
{
var grid = _mapManager.GetGrid(Owner.Transform.GridID);
_lastPosition = (Owner.Transform.GridID, grid.TileIndicesFor(Owner.Transform.Coordinates));
}
}
private void UpdatePosition()
{
if (Owner.Transform.Anchored)
{
if (!Owner.Transform.GridID.IsValid())
return;
var grid = _mapManager.GetGrid(Owner.Transform.GridID);
UpdatePosition(Owner.Transform.GridID, grid.TileIndicesFor(Owner.Transform.Coordinates));
}
}
private void UpdatePosition(GridId gridId, Vector2i pos)
{
var gridAtmos = _atmosphereSystem.GetGridAtmosphere(gridId);
gridAtmos?.UpdateAdjacentBits(pos);
gridAtmos?.Invalidate(pos);
}
}
}

View File

@@ -1,45 +0,0 @@
#nullable enable
using Content.Server.Atmos;
using Content.Server.Temperature.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos
{
/// <summary>
/// Represents that entity can be exposed to Atmos
/// </summary>
[RegisterComponent]
public class AtmosExposedComponent
: Component
{
public override string Name => "AtmosExposed";
[ViewVariables]
[ComponentDependency] private readonly TemperatureComponent? _temperatureComponent = null;
[ViewVariables]
[ComponentDependency] private readonly BarotraumaComponent? _barotraumaComponent = null;
[ViewVariables]
[ComponentDependency] private readonly FlammableComponent? _flammableComponent = null;
public void Update(TileAtmosphere tile, float frameDelta)
{
if (_temperatureComponent != null)
{
if (tile.Air != null)
{
var temperatureDelta = tile.Air.Temperature - _temperatureComponent.CurrentTemperature;
var heat = temperatureDelta * (tile.Air.HeatCapacity * _temperatureComponent.HeatCapacity / (tile.Air.HeatCapacity + _temperatureComponent.HeatCapacity));
_temperatureComponent.ReceiveHeat(heat);
}
_temperatureComponent.Update();
}
_barotraumaComponent?.Update(tile.Air?.Pressure ?? 0);
_flammableComponent?.Update(tile);
}
}
}

View File

@@ -1,90 +0,0 @@
using System;
using System.Runtime.CompilerServices;
using Content.Server.Alert;
using Content.Server.Pressure;
using Content.Shared.Alert;
using Content.Shared.Atmos;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.Components.Atmos
{
/// <summary>
/// Barotrauma: injury because of changes in air pressure.
/// </summary>
[RegisterComponent]
public class BarotraumaComponent : Component
{
public override string Name => "Barotrauma";
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Update(float airPressure)
{
if (!Owner.TryGetComponent(out IDamageableComponent? damageable)) return;
var status = Owner.GetComponentOrNull<ServerAlertsComponent>();
var highPressureMultiplier = 1f;
var lowPressureMultiplier = 1f;
foreach (var protection in Owner.GetAllComponents<IPressureProtection>())
{
highPressureMultiplier *= protection.HighPressureMultiplier;
lowPressureMultiplier *= protection.LowPressureMultiplier;
}
var pressure = MathF.Max(airPressure, 1f);
switch (pressure)
{
// Low pressure.
case var p when p <= Atmospherics.WarningLowPressure:
pressure *= lowPressureMultiplier;
if(pressure > Atmospherics.WarningLowPressure)
goto default;
damageable.ChangeDamage(DamageType.Blunt, Atmospherics.LowPressureDamage, false, Owner);
if (status == null) break;
if (pressure <= Atmospherics.HazardLowPressure)
{
status.ShowAlert(AlertType.LowPressure, 2);
break;
}
status.ShowAlert(AlertType.LowPressure, 1);
break;
// High pressure.
case var p when p >= Atmospherics.WarningHighPressure:
pressure *= highPressureMultiplier;
if(pressure < Atmospherics.WarningHighPressure)
goto default;
var damage = (int) MathF.Min((pressure / Atmospherics.HazardHighPressure) * Atmospherics.PressureDamageCoefficient, Atmospherics.MaxHighPressureDamage);
damageable.ChangeDamage(DamageType.Blunt, damage, false, Owner);
if (status == null) break;
if (pressure >= Atmospherics.HazardHighPressure)
{
status.ShowAlert(AlertType.HighPressure, 2);
break;
}
status.ShowAlert(AlertType.HighPressure, 1);
break;
// Normal pressure.
default:
status?.ClearAlertCategory(AlertCategory.Pressure);
break;
}
}
}
}

View File

@@ -1,61 +0,0 @@
#nullable enable
using Content.Server.Body.Respiratory;
using Content.Shared.Inventory;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.GameObjects.Components.Atmos
{
/// <summary>
/// Used in internals as breath tool.
/// </summary>
[RegisterComponent]
public class BreathToolComponent : Component, IEquipped, IUnequipped
{
/// <summary>
/// Tool is functional only in allowed slots
/// </summary>
[DataField("allowedSlots")]
private EquipmentSlotDefines.SlotFlags _allowedSlots = EquipmentSlotDefines.SlotFlags.MASK;
public override string Name => "BreathMask";
public bool IsFunctional { get; private set; }
public IEntity? ConnectedInternalsEntity { get; private set; }
protected override void Shutdown()
{
base.Shutdown();
DisconnectInternals();
}
void IEquipped.Equipped(EquippedEventArgs eventArgs)
{
if ((EquipmentSlotDefines.SlotMasks[eventArgs.Slot] & _allowedSlots) != _allowedSlots) return;
IsFunctional = true;
if (eventArgs.User.TryGetComponent(out InternalsComponent? internals))
{
ConnectedInternalsEntity = eventArgs.User;
internals.ConnectBreathTool(Owner);
}
}
void IUnequipped.Unequipped(UnequippedEventArgs eventArgs)
{
DisconnectInternals();
}
public void DisconnectInternals()
{
var old = ConnectedInternalsEntity;
ConnectedInternalsEntity = null;
if (old != null && old.TryGetComponent<InternalsComponent>(out var internalsComponent))
{
internalsComponent.DisconnectBreathTool();
}
IsFunctional = false;
}
}
}

View File

@@ -1,119 +0,0 @@
#nullable enable
using Robust.Shared.GameObjects;
using Content.Server.Atmos;
using Content.Server.Doors;
using Content.Server.Doors.Components;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Doors;
using Content.Shared.Interaction;
using Content.Shared.Notification;
using Content.Shared.Notification.Managers;
using Robust.Shared.Localization;
namespace Content.Server.GameObjects.Components.Atmos
{
/// <summary>
/// Companion component to ServerDoorComponent that handles firelock-specific behavior -- primarily prying, and not being openable on open-hand click.
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(IDoorCheck))]
public class FirelockComponent : Component, IDoorCheck
{
public override string Name => "Firelock";
[ComponentDependency]
private readonly ServerDoorComponent? _doorComponent = null;
public bool EmergencyPressureStop()
{
if (_doorComponent != null && _doorComponent.State == SharedDoorComponent.DoorState.Open && _doorComponent.CanCloseGeneric())
{
_doorComponent.Close();
if (Owner.TryGetComponent(out AirtightComponent? airtight))
{
airtight.AirBlocked = true;
}
return true;
}
return false;
}
bool IDoorCheck.OpenCheck()
{
return !IsHoldingFire() && !IsHoldingPressure();
}
bool IDoorCheck.DenyCheck() => false;
float? IDoorCheck.GetPryTime()
{
if (IsHoldingFire() || IsHoldingPressure())
{
return 1.5f;
}
return null;
}
bool IDoorCheck.BlockActivate(ActivateEventArgs eventArgs) => true;
void IDoorCheck.OnStartPry(InteractUsingEventArgs eventArgs)
{
if (_doorComponent == null || _doorComponent.State != SharedDoorComponent.DoorState.Closed)
{
return;
}
if (IsHoldingPressure())
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("A gush of air blows in your face... Maybe you should reconsider."));
}
else if (IsHoldingFire())
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("A gush of warm air blows in your face... Maybe you should reconsider."));
}
}
public bool IsHoldingPressure(float threshold = 20)
{
var atmosphereSystem = EntitySystem.Get<AtmosphereSystem>();
var gridAtmosphere = atmosphereSystem.GetGridAtmosphere(Owner.Transform.Coordinates);
var minMoles = float.MaxValue;
var maxMoles = 0f;
foreach (var (_, adjacent) in gridAtmosphere.GetAdjacentTiles(Owner.Transform.Coordinates))
{
// includeAirBlocked remains false, and therefore Air must be present
var moles = adjacent.Air!.TotalMoles;
if (moles < minMoles)
minMoles = moles;
if (moles > maxMoles)
maxMoles = moles;
}
return (maxMoles - minMoles) > threshold;
}
public bool IsHoldingFire()
{
var atmosphereSystem = EntitySystem.Get<AtmosphereSystem>();
if (!Owner.Transform.Coordinates.TryGetTileAtmosphere(out var tileAtmos))
return false;
if (tileAtmos.Hotspot.Valid)
return true;
var gridAtmosphere = atmosphereSystem.GetGridAtmosphere(Owner.Transform.Coordinates);
foreach (var (_, adjacent) in gridAtmosphere.GetAdjacentTiles(tileAtmos.GridIndices))
{
if (adjacent.Hotspot.Valid)
return true;
}
return false;
}
}
}

View File

@@ -1,224 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.Alert;
using Content.Server.Atmos;
using Content.Server.Stunnable.Components;
using Content.Server.Temperature.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.Alert;
using Content.Shared.Atmos;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.GameObjects.Components.Atmos;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Notification.Managers;
using Content.Shared.Temperature;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos
{
[RegisterComponent]
public class FlammableComponent : SharedFlammableComponent, IStartCollide, IFireAct, IInteractUsing
{
private bool _resisting = false;
private readonly List<EntityUid> _collided = new();
[ViewVariables(VVAccess.ReadWrite)]
public bool OnFire { get; private set; }
[ViewVariables(VVAccess.ReadWrite)]
public float FireStacks { get; private set; }
[ViewVariables(VVAccess.ReadWrite)]
[DataField("fireSpread")]
public bool FireSpread { get; private set; } = false;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("canResistFire")]
public bool CanResistFire { get; private set; } = false;
public void Ignite()
{
if (FireStacks > 0 && !OnFire)
{
OnFire = true;
}
UpdateAppearance();
}
public void Extinguish()
{
if (!OnFire) return;
OnFire = false;
FireStacks = 0;
_collided.Clear();
UpdateAppearance();
}
public void AdjustFireStacks(float relativeFireStacks)
{
FireStacks = MathF.Min(MathF.Max(-10f, FireStacks + relativeFireStacks), 20f);
if (OnFire && FireStacks <= 0)
Extinguish();
UpdateAppearance();
}
public void Update(TileAtmosphere tile)
{
// Slowly dry ourselves off if wet.
if (FireStacks < 0)
{
FireStacks = MathF.Min(0, FireStacks + 1);
}
Owner.TryGetComponent(out ServerAlertsComponent? status);
if (!OnFire)
{
status?.ClearAlert(AlertType.Fire);
return;
}
status?.ShowAlert(AlertType.Fire);
if (FireStacks > 0)
{
if (Owner.TryGetComponent(out TemperatureComponent? temp))
{
temp.ReceiveHeat(200 * FireStacks);
}
if (Owner.TryGetComponent(out IDamageableComponent? damageable))
{
// TODO ATMOS Fire resistance from armor
var damage = Math.Min((int) (FireStacks * 2.5f), 10);
damageable.ChangeDamage(DamageClass.Burn, damage, false);
}
AdjustFireStacks(-0.1f * (_resisting ? 10f : 1f));
}
else
{
Extinguish();
return;
}
// If we're in an oxygenless environment, put the fire out.
if (tile.Air?.GetMoles(Gas.Oxygen) < 1f)
{
Extinguish();
return;
}
tile.HotspotExpose(700, 50, true);
var physics = Owner.GetComponent<IPhysBody>();
foreach (var uid in _collided.ToArray())
{
if (!uid.IsValid() || !Owner.EntityManager.EntityExists(uid))
{
_collided.Remove(uid);
continue;
}
var entity = Owner.EntityManager.GetEntity(uid);
var otherPhysics = entity.GetComponent<IPhysBody>();
if (!physics.GetWorldAABB().Intersects(otherPhysics.GetWorldAABB()))
{
_collided.Remove(uid);
}
}
}
void IStartCollide.CollideWith(Fixture ourFixture, Fixture otherFixture, in Manifold manifold)
{
if (!otherFixture.Body.Owner.TryGetComponent(out FlammableComponent? otherFlammable))
return;
if (!FireSpread || !otherFlammable.FireSpread)
return;
if (OnFire)
{
if (otherFlammable.OnFire)
{
var fireSplit = (FireStacks + otherFlammable.FireStacks) / 2;
FireStacks = fireSplit;
otherFlammable.FireStacks = fireSplit;
}
else
{
FireStacks /= 2;
otherFlammable.FireStacks += FireStacks;
otherFlammable.Ignite();
}
} else if (otherFlammable.OnFire)
{
otherFlammable.FireStacks /= 2;
FireStacks += otherFlammable.FireStacks;
Ignite();
}
}
private void UpdateAppearance()
{
if (Owner.Deleted || !Owner.TryGetComponent(out AppearanceComponent? appearanceComponent)) return;
appearanceComponent.SetData(FireVisuals.OnFire, OnFire);
appearanceComponent.SetData(FireVisuals.FireStacks, FireStacks);
}
public void FireAct(float temperature, float volume)
{
AdjustFireStacks(3);
Ignite();
}
// This needs some improvements...
public void Resist()
{
if (!OnFire || !EntitySystem.Get<ActionBlockerSystem>().CanInteract(Owner) || _resisting || !Owner.TryGetComponent(out StunnableComponent? stunnable)) return;
_resisting = true;
Owner.PopupMessage(Loc.GetString("You stop, drop, and roll!"));
stunnable.Paralyze(2f);
Owner.SpawnTimer(2000, () =>
{
_resisting = false;
FireStacks -= 3f;
UpdateAppearance();
});
}
public async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
{
foreach (var hotItem in eventArgs.Using.GetAllComponents<IHotItem>())
{
if (hotItem.IsCurrentlyHot())
{
Ignite();
return true;
}
}
return false;
}
}
}

View File

@@ -1,290 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Hands.Components;
using Content.Server.UserInterface;
using Content.Shared.Atmos;
using Content.Shared.DragDrop;
using Content.Shared.GameObjects.Components;
using Content.Shared.GameObjects.Components.Atmos;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Notification;
using Content.Shared.Notification.Managers;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Players;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos
{
[RegisterComponent]
public class GasAnalyzerComponent : SharedGasAnalyzerComponent, IAfterInteract, IDropped, IUse
{
private GasAnalyzerDanger _pressureDanger;
private float _timeSinceSync;
private const float TimeBetweenSyncs = 2f;
private bool _checkPlayer = false; // Check at the player pos or at some other tile?
private EntityCoordinates? _position; // The tile that we scanned
private AppearanceComponent? _appearance;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(GasAnalyzerUiKey.Key);
public override void Initialize()
{
base.Initialize();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += UserInterfaceOnReceiveMessage;
UserInterface.OnClosed += UserInterfaceOnClose;
}
Owner.TryGetComponent(out _appearance);
}
public override ComponentState GetComponentState(ICommonSession player)
{
return new GasAnalyzerComponentState(_pressureDanger);
}
/// <summary>
/// Call this from other components to open the gas analyzer UI.
/// Uses the player position.
/// </summary>
/// <param name="session">The session to open the ui for</param>
public void OpenInterface(IPlayerSession session)
{
_checkPlayer = true;
_position = null;
UserInterface?.Open(session);
UpdateUserInterface();
UpdateAppearance(true);
Resync();
}
/// <summary>
/// Call this from other components to open the gas analyzer UI.
/// Uses a given position.
/// </summary>
/// <param name="session">The session to open the ui for</param>
/// <param name="pos">The position to analyze the gas</param>
public void OpenInterface(IPlayerSession session, EntityCoordinates pos)
{
_checkPlayer = false;
_position = pos;
UserInterface?.Open(session);
UpdateUserInterface();
UpdateAppearance(true);
Resync();
}
public void ToggleInterface(IPlayerSession session)
{
if (UserInterface == null)
return;
if (UserInterface.SessionHasOpen(session))
CloseInterface(session);
else
OpenInterface(session);
}
public void CloseInterface(IPlayerSession session)
{
_position = null;
UserInterface?.Close(session);
// Our OnClose will do the appearance stuff
Resync();
}
private void UserInterfaceOnClose(IPlayerSession obj)
{
UpdateAppearance(false);
}
private void UpdateAppearance(bool open)
{
_appearance?.SetData(GasAnalyzerVisuals.VisualState,
open ? GasAnalyzerVisualState.Working : GasAnalyzerVisualState.Off);
}
public void Update(float frameTime)
{
_timeSinceSync += frameTime;
if (_timeSinceSync > TimeBetweenSyncs)
{
Resync();
UpdateUserInterface();
}
}
private void Resync()
{
// Already get the pressure before Dirty(), because we can't get the EntitySystem in that thread or smth
var pressure = 0f;
var gam = EntitySystem.Get<AtmosphereSystem>().GetGridAtmosphere(Owner.Transform.Coordinates);
var tile = gam?.GetTile(Owner.Transform.Coordinates)?.Air;
if (tile != null)
{
pressure = tile.Pressure;
}
if (pressure >= Atmospherics.HazardHighPressure || pressure <= Atmospherics.HazardLowPressure)
{
_pressureDanger = GasAnalyzerDanger.Hazard;
}
else if (pressure >= Atmospherics.WarningHighPressure || pressure <= Atmospherics.WarningLowPressure)
{
_pressureDanger = GasAnalyzerDanger.Warning;
}
else
{
_pressureDanger = GasAnalyzerDanger.Nominal;
}
Dirty();
_timeSinceSync = 0f;
}
private void UpdateUserInterface()
{
if (UserInterface == null)
{
return;
}
string? error = null;
// Check if the player is still holding the gas analyzer => if not, don't update
foreach (var session in UserInterface.SubscribedSessions)
{
if (session.AttachedEntity == null)
return;
if (!session.AttachedEntity.TryGetComponent(out IHandsComponent? handsComponent))
return;
var activeHandEntity = handsComponent?.GetActiveHand?.Owner;
if (activeHandEntity == null || !activeHandEntity.TryGetComponent(out GasAnalyzerComponent? gasAnalyzer))
{
return;
}
}
var pos = Owner.Transform.Coordinates;
if (!_checkPlayer && _position.HasValue)
{
// Check if position is out of range => don't update
if (!_position.Value.InRange(Owner.EntityManager, pos, SharedInteractionSystem.InteractionRange))
return;
pos = _position.Value;
}
var atmosSystem = EntitySystem.Get<AtmosphereSystem>();
var gam = atmosSystem.GetGridAtmosphere(pos);
var tile = gam.GetTile(pos)?.Air;
if (tile == null)
{
error = "No Atmosphere!";
UserInterface.SetState(
new GasAnalyzerBoundUserInterfaceState(
0,
0,
null,
error));
return;
}
var gases = new List<GasEntry>();
for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
{
var gas = atmosSystem.GetGas(i);
if (tile.Gases[i] <= Atmospherics.GasMinMoles) continue;
gases.Add(new GasEntry(gas.Name, tile.Gases[i], gas.Color));
}
UserInterface.SetState(
new GasAnalyzerBoundUserInterfaceState(
tile.Pressure,
tile.Temperature,
gases.ToArray(),
error));
}
private void UserInterfaceOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg)
{
var message = serverMsg.Message;
switch (message)
{
case GasAnalyzerRefreshMessage msg:
var player = serverMsg.Session.AttachedEntity;
if (player == null)
{
return;
}
if (!player.TryGetComponent(out IHandsComponent? handsComponent))
{
Owner.PopupMessage(player, Loc.GetString("You have no hands."));
return;
}
var activeHandEntity = handsComponent.GetActiveHand?.Owner;
if (activeHandEntity == null || !activeHandEntity.TryGetComponent(out GasAnalyzerComponent? gasAnalyzer))
{
serverMsg.Session.AttachedEntity?.PopupMessage(Loc.GetString("You need a Gas Analyzer in your hand!"));
return;
}
UpdateUserInterface();
Resync();
break;
}
}
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (!eventArgs.CanReach)
{
eventArgs.User.PopupMessage(Loc.GetString("You can't reach there!"));
return true;
}
if (eventArgs.User.TryGetComponent(out ActorComponent? actor))
{
OpenInterface(actor.PlayerSession, eventArgs.ClickLocation);
}
return true;
}
void IDropped.Dropped(DroppedEventArgs eventArgs)
{
if (eventArgs.User.TryGetComponent(out ActorComponent? actor))
{
CloseInterface(actor.PlayerSession);
}
}
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
{
if (eventArgs.User.TryGetComponent(out ActorComponent? actor))
{
ToggleInterface(actor.PlayerSession);
return true;
}
return false;
}
}
}

View File

@@ -1,320 +0,0 @@
#nullable enable
using System;
using System.Linq;
using Content.Server.Atmos;
using Content.Server.GameObjects.Components.Atmos.Piping;
using Content.Server.Interfaces;
using Content.Server.UserInterface;
using Content.Shared.ActionBlocker;
using Content.Shared.Atmos;
using Content.Shared.GameObjects.Components.Atmos;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos
{
/// <summary>
/// Component that manages gas mixtures temperature, pressure and output.
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
public class GasCanisterComponent : Component, IGasMixtureHolder, IActivate
{
[Dependency] private readonly IMapManager _mapManager = default!;
public override string Name => "GasCanister";
private const int MaxLabelLength = 32;
[ViewVariables(VVAccess.ReadWrite)]
public string Label { get; set; } = "Gas Canister";
[ViewVariables(VVAccess.ReadWrite)]
public bool ValveOpened { get; set; } = false;
/// <summary>
/// What <see cref="GasMixture"/> the canister contains.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("gasMixture")]
public GasMixture Air { get; set; } = new (DefaultVolume);
[ViewVariables]
public bool Anchored => !Owner.TryGetComponent<IPhysBody>(out var physics) || physics.BodyType == BodyType.Static;
/// <summary>
/// The floor connector port that the canister is attached to.
/// </summary>
[ViewVariables]
public GasCanisterPortComponent? ConnectedPort { get; private set; }
[ViewVariables]
public bool ConnectedToPort => ConnectedPort != null;
public const float DefaultVolume = 10;
[ViewVariables(VVAccess.ReadWrite)] public float ReleasePressure { get; set; }
/// <summary>
/// The user interface bound to the canister.
/// </summary>
private BoundUserInterface? UserInterface => Owner.GetUIOrNull(SharedGasCanisterComponent.GasCanisterUiKey.Key);
/// <summary>
/// Stores the last ui state after it's been casted into <see cref="GasCanisterBoundUserInterface"/>
/// </summary>
private GasCanisterBoundUserInterfaceState? _lastUiState;
private AppearanceComponent? _appearance;
public override void Initialize()
{
base.Initialize();
if (Owner.TryGetComponent<IPhysBody>(out var physics))
{
AnchorUpdate();
}
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += OnUiReceiveMessage;
}
// Init some variables
Label = Owner.Name;
Owner.TryGetComponent(out _appearance);
UpdateUserInterface();
UpdateAppearance();
}
#region Connector port methods
public override void OnRemove()
{
base.OnRemove();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage -= OnUiReceiveMessage;
}
DisconnectFromPort();
}
public void TryConnectToPort()
{
if (!Owner.Transform.Anchored) return;
var grid = _mapManager.GetGrid(Owner.Transform.GridID);
var coords = Owner.Transform.Coordinates;
var port = grid.GetLocal(coords)
.Select(entity => Owner.EntityManager.ComponentManager.TryGetComponent<GasCanisterPortComponent>(entity, out var port) ? port : null)
.Where(port => port != null)
.Where(port => !port!.ConnectedToCanister)
.FirstOrDefault();
if (port == null) return;
ConnectedPort = port;
ConnectedPort.ConnectGasCanister(this);
}
public void DisconnectFromPort()
{
ConnectedPort?.DisconnectGasCanister();
ConnectedPort = null;
}
public void AnchorUpdate()
{
if (Anchored)
{
TryConnectToPort();
}
else
{
DisconnectFromPort();
}
UpdateAppearance();
}
#endregion
void IActivate.Activate(ActivateEventArgs eventArgs)
{
if (!eventArgs.User.TryGetComponent(out ActorComponent? actor))
return;
UserInterface?.Open(actor.PlayerSession);
}
private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj)
{
if (obj.Session.AttachedEntity == null)
{
return;
}
if (!PlayerCanUse(obj.Session.AttachedEntity))
{
return;
}
// If the label has been changed by a client
if (obj.Message is CanisterLabelChangedMessage canLabelMessage)
{
var newLabel = canLabelMessage.NewLabel;
if (newLabel.Length > MaxLabelLength)
newLabel = newLabel.Substring(0, MaxLabelLength);
Label = newLabel;
Owner.Name = Label;
UpdateUserInterface();
return;
}
// If the release pressure has been adjusted by the client on the gas canister
if (obj.Message is ReleasePressureButtonPressedMessage rPMessage)
{
ReleasePressure += rPMessage.ReleasePressure;
ReleasePressure = Math.Clamp(ReleasePressure, 0, 1000);
ReleasePressure = MathF.Round(ReleasePressure, 2);
UpdateUserInterface();
return;
}
if (obj.Message is UiButtonPressedMessage btnPressedMessage)
{
switch (btnPressedMessage.Button)
{
case UiButton.ValveToggle:
ToggleValve();
break;
}
}
}
/// <summary>
/// Update the user interface if relevant
/// </summary>
private void UpdateUserInterface()
{
var state = GetUserInterfaceState();
if (_lastUiState != null && _lastUiState.Equals(state))
{
return;
}
_lastUiState = state;
UserInterface?.SetState(state);
}
/// <summary>
/// Update the canister's sprite
/// </summary>
private void UpdateAppearance()
{
_appearance?.SetData(GasCanisterVisuals.ConnectedState, ConnectedToPort);
// The Eris canisters are being used, so best to use the Eris light logic unless someone else has a better idea.
// https://github.com/discordia-space/CEV-Eris/blob/fdd6ee7012f46838a6711adb1737cd90c48ae448/code/game/machinery/atmoalter/canister.dm#L129
if (Air.Pressure < 10)
{
_appearance?.SetData(GasCanisterVisuals.PressureState, 0);
}
else if (Air.Pressure < Atmospherics.OneAtmosphere)
{
_appearance?.SetData(GasCanisterVisuals.PressureState, 1);
}
else if (Air.Pressure < (15 * Atmospherics.OneAtmosphere))
{
_appearance?.SetData(GasCanisterVisuals.PressureState, 2);
}
else
{
_appearance?.SetData(GasCanisterVisuals.PressureState, 3);
}
}
/// <summary>
/// Get the current interface state from server data
/// </summary>
/// <returns>The state</returns>
private GasCanisterBoundUserInterfaceState GetUserInterfaceState()
{
// We round the pressure for ease of reading
return new GasCanisterBoundUserInterfaceState(Label,
MathF.Round(Air.Pressure, 2),
ReleasePressure,
ValveOpened);
}
public void AirWasUpdated()
{
UpdateUserInterface();
UpdateAppearance();
}
#region Check methods
private bool PlayerCanUse(IEntity? player)
{
if (player == null)
{
return false;
}
var actionBlocker = EntitySystem.Get<ActionBlockerSystem>();
if (!actionBlocker.CanInteract(player) ||
!actionBlocker.CanUse(player))
{
return false;
}
return true;
}
#endregion
/// <summary>
/// Called when the canister's valve is toggled
/// </summary>
private void ToggleValve()
{
ValveOpened = !ValveOpened;
UpdateUserInterface();
}
/// <summary>
/// Called every frame
/// </summary>
/// <param name="frameTime"></param>
public void Update(in float frameTime)
{
if (ValveOpened)
{
var tileAtmosphere = Owner.Transform.Coordinates.GetTileAtmosphere();
if (tileAtmosphere != null)
{
// If tileAtmosphere.Air is null, then we're airblocked, so DON'T release
if (tileAtmosphere.Air != null)
{
Air.ReleaseGasTo(tileAtmosphere.Air, ReleasePressure);
tileAtmosphere.Invalidate();
}
}
else
{
Air.ReleaseGasTo(null, ReleasePressure);
}
AirWasUpdated();
}
}
}
}

View File

@@ -1,18 +0,0 @@
using Content.Server.Atmos;
using Content.Server.Interfaces;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos
{
[RegisterComponent]
public class GasMixtureHolderComponent : Component, IGasMixtureHolder
{
public override string Name => "GasMixtureHolder";
[ViewVariables] [DataField("air")] public GasMixture Air { get; set; } = new GasMixture();
}
}

View File

@@ -1,372 +0,0 @@
#nullable enable
#nullable disable warnings
using System;
using Content.Server.Atmos;
using Content.Server.Body.Respiratory;
using Content.Server.Explosion;
using Content.Server.Interfaces;
using Content.Server.UserInterface;
using Content.Shared.ActionBlocker;
using Content.Shared.Actions;
using Content.Shared.Actions.Behaviors.Item;
using Content.Shared.Actions.Components;
using Content.Shared.Atmos;
using Content.Shared.Audio;
using Content.Shared.DragDrop;
using Content.Shared.Examine;
using Content.Shared.GameObjects.Components.Atmos.GasTank;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Player;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos
{
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
public class GasTankComponent : SharedGasTankComponent, IExamine, IGasMixtureHolder, IUse, IDropped, IActivate
{
private const float MaxExplosionRange = 14f;
private const float DefaultOutputPressure = Atmospherics.OneAtmosphere;
private int _integrity = 3;
[ComponentDependency] private readonly ItemActionsComponent? _itemActions = null;
[ViewVariables] private BoundUserInterface? _userInterface;
[DataField("air")] [ViewVariables] public GasMixture? Air { get; set; } = new();
/// <summary>
/// Distributed pressure.
/// </summary>
[DataField("outputPressure")]
[ViewVariables]
public float OutputPressure { get; private set; } = DefaultOutputPressure;
/// <summary>
/// Tank is connected to internals.
/// </summary>
[ViewVariables] public bool IsConnected { get; set; }
/// <summary>
/// Represents that tank is functional and can be connected to internals.
/// </summary>
public bool IsFunctional => GetInternalsComponent() != null;
/// <summary>
/// Pressure at which tanks start leaking.
/// </summary>
[DataField("tankLeakPressure")]
public float TankLeakPressure { get; set; } = 30 * Atmospherics.OneAtmosphere;
/// <summary>
/// Pressure at which tank spills all contents into atmosphere.
/// </summary>
[DataField("tankRupturePressure")]
public float TankRupturePressure { get; set; } = 40 * Atmospherics.OneAtmosphere;
/// <summary>
/// Base 3x3 explosion.
/// </summary>
[DataField("tankFragmentPressure")]
public float TankFragmentPressure { get; set; } = 50 * Atmospherics.OneAtmosphere;
/// <summary>
/// Increases explosion for each scale kPa above threshold.
/// </summary>
[DataField("tankFragmentScale")]
public float TankFragmentScale { get; set; } = 10 * Atmospherics.OneAtmosphere;
public override void Initialize()
{
base.Initialize();
_userInterface = Owner.GetUIOrNull(SharedGasTankUiKey.Key);
if (_userInterface != null)
{
_userInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
}
}
public void OpenInterface(IPlayerSession session)
{
_userInterface?.Open(session);
UpdateUserInterface(true);
}
public void Examine(FormattedMessage message, bool inDetailsRange)
{
message.AddMarkup(Loc.GetString("gas-tank-examine", ("pressure", Math.Round(Air?.Pressure ?? 0))));
if (IsConnected)
{
message.AddMarkup(Loc.GetString("gas-tank-connected"));
}
}
protected override void Shutdown()
{
base.Shutdown();
DisconnectFromInternals();
}
public void Update()
{
Air?.React(this);
CheckStatus();
UpdateUserInterface();
}
public GasMixture? RemoveAir(float amount)
{
var gas = Air?.Remove(amount);
CheckStatus();
return gas;
}
public GasMixture RemoveAirVolume(float volume)
{
if (Air == null)
return new GasMixture(volume);
var tankPressure = Air.Pressure;
if (tankPressure < OutputPressure)
{
OutputPressure = tankPressure;
UpdateUserInterface();
}
var molesNeeded = OutputPressure * volume / (Atmospherics.R * Air.Temperature);
var air = RemoveAir(molesNeeded);
if (air != null)
air.Volume = volume;
else
return new GasMixture(volume);
return air;
}
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
{
if (!eventArgs.User.TryGetComponent(out ActorComponent? actor)) return false;
OpenInterface(actor.PlayerSession);
return true;
}
void IActivate.Activate(ActivateEventArgs eventArgs)
{
if (!eventArgs.User.TryGetComponent(out ActorComponent? actor)) return;
OpenInterface(actor.PlayerSession);
}
public void ConnectToInternals()
{
if (IsConnected || !IsFunctional) return;
var internals = GetInternalsComponent();
if (internals == null) return;
IsConnected = internals.TryConnectTank(Owner);
UpdateUserInterface();
}
public void DisconnectFromInternals(IEntity? owner = null)
{
if (!IsConnected) return;
IsConnected = false;
GetInternalsComponent(owner)?.DisconnectTank();
UpdateUserInterface();
}
private void UpdateUserInterface(bool initialUpdate = false)
{
var internals = GetInternalsComponent();
_userInterface?.SetState(
new GasTankBoundUserInterfaceState
{
TankPressure = Air?.Pressure ?? 0,
OutputPressure = initialUpdate ? OutputPressure : (float?) null,
InternalsConnected = IsConnected,
CanConnectInternals = IsFunctional && internals != null
});
if (internals == null) return;
_itemActions?.GrantOrUpdate(ItemActionType.ToggleInternals, IsFunctional, IsConnected);
}
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message)
{
switch (message.Message)
{
case GasTankSetPressureMessage msg:
OutputPressure = msg.Pressure;
break;
case GasTankToggleInternalsMessage _:
ToggleInternals();
break;
}
}
internal void ToggleInternals()
{
var user = GetInternalsComponent()?.Owner;
if (user == null || !EntitySystem.Get<ActionBlockerSystem>().CanUse(user))
return;
if (IsConnected)
{
DisconnectFromInternals();
return;
}
ConnectToInternals();
}
private InternalsComponent? GetInternalsComponent(IEntity? owner = null)
{
if (Owner.Deleted) return null;
if (owner != null) return owner.GetComponentOrNull<InternalsComponent>();
return Owner.TryGetContainer(out var container)
? container.Owner.GetComponentOrNull<InternalsComponent>()
: null;
}
public void AssumeAir(GasMixture giver)
{
Air?.Merge(giver);
CheckStatus();
}
private void CheckStatus()
{
if (Air == null)
return;
var pressure = Air.Pressure;
if (pressure > TankFragmentPressure)
{
// Give the gas a chance to build up more pressure.
for (var i = 0; i < 3; i++)
{
Air.React(this);
}
pressure = Air.Pressure;
var range = (pressure - TankFragmentPressure) / TankFragmentScale;
// Let's cap the explosion, yeah?
if (range > MaxExplosionRange)
{
range = MaxExplosionRange;
}
Owner.SpawnExplosion((int) (range * 0.25f), (int) (range * 0.5f), (int) (range * 1.5f), 1);
Owner.QueueDelete();
return;
}
if (pressure > TankRupturePressure)
{
if (_integrity <= 0)
{
var tileAtmos = Owner.Transform.Coordinates.GetTileAtmosphere();
tileAtmos?.AssumeAir(Air);
SoundSystem.Play(Filter.Pvs(Owner), "Audio/Effects/spray.ogg", Owner.Transform.Coordinates,
AudioHelpers.WithVariation(0.125f));
Owner.QueueDelete();
return;
}
_integrity--;
return;
}
if (pressure > TankLeakPressure)
{
if (_integrity <= 0)
{
var tileAtmos = Owner.Transform.Coordinates.GetTileAtmosphere();
if (tileAtmos == null)
return;
var leakedGas = Air.RemoveRatio(0.25f);
tileAtmos.AssumeAir(leakedGas);
}
else
{
_integrity--;
}
return;
}
if (_integrity < 3)
_integrity++;
}
void IDropped.Dropped(DroppedEventArgs eventArgs)
{
DisconnectFromInternals(eventArgs.User);
}
/// <summary>
/// Open interaction window
/// </summary>
[Verb]
private sealed class ControlVerb : Verb<GasTankComponent>
{
public override bool RequireInteractionRange => true;
protected override void GetData(IEntity user, GasTankComponent component, VerbData data)
{
data.Visibility = VerbVisibility.Invisible;
if (!user.HasComponent<ActorComponent>())
{
return;
}
data.Visibility = VerbVisibility.Visible;
data.Text = "Open Control Panel";
}
protected override void Activate(IEntity user, GasTankComponent component)
{
if (!user.TryGetComponent<ActorComponent>(out var actor))
{
return;
}
component.OpenInterface(actor.PlayerSession);
}
}
}
[UsedImplicitly]
[DataDefinition]
public class ToggleInternalsAction : IToggleItemAction
{
public bool DoToggleAction(ToggleItemActionEventArgs args)
{
if (!args.Item.TryGetComponent<GasTankComponent>(out var gasTankComponent)) return false;
// no change
if (gasTankComponent.IsConnected == args.ToggledOn) return false;
gasTankComponent.ToggleInternals();
// did we successfully toggle to the desired status?
return gasTankComponent.IsConnected == args.ToggledOn;
}
}
}

View File

@@ -1,944 +0,0 @@
#nullable enable
// ReSharper disable once RedundantUsingDirective
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Content.Server.Atmos;
using Content.Server.GameObjects.Components.Atmos.Piping;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.GameObjects.EntitySystems.Atmos;
using Content.Server.NodeContainer.NodeGroups;
using Content.Shared.Atmos;
using Content.Shared.Maps;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
using Dependency = Robust.Shared.IoC.DependencyAttribute;
namespace Content.Server.GameObjects.Components.Atmos
{
/// <summary>
/// This is our SSAir equivalent.
/// </summary>
[ComponentReference(typeof(IGridAtmosphereComponent))]
[RegisterComponent, Serializable]
public class GridAtmosphereComponent : Component, IGridAtmosphereComponent, ISerializationHooks
{
[Dependency] private IMapManager _mapManager = default!;
[Dependency] private ITileDefinitionManager _tileDefinitionManager = default!;
[Dependency] private IServerEntityManager _serverEntityManager = default!;
public GridTileLookupSystem GridTileLookupSystem { get; private set; } = default!;
internal GasTileOverlaySystem GasTileOverlaySystem { get; private set; } = default!;
public AtmosphereSystem AtmosphereSystem { get; private set; } = default!;
/// <summary>
/// Check current execution time every n instances processed.
/// </summary>
private const int LagCheckIterations = 30;
public override string Name => "GridAtmosphere";
private bool _paused;
private float _timer;
private Stopwatch _stopwatch = new();
private GridId _gridId;
[ComponentDependency] private IMapGridComponent? _mapGridComponent;
[ViewVariables]
public int UpdateCounter { get; private set; } = 0;
[ViewVariables]
private double _tileEqualizeLastProcess;
[ViewVariables]
private readonly HashSet<ExcitedGroup> _excitedGroups = new(1000);
[ViewVariables]
private int ExcitedGroupCount => _excitedGroups.Count;
[ViewVariables]
private double _excitedGroupLastProcess;
[DataField("uniqueMixes")]
private List<GasMixture>? _uniqueMixes;
[DataField("tiles")]
private Dictionary<Vector2i, int>? _tiles;
[ViewVariables]
protected readonly Dictionary<Vector2i, TileAtmosphere> Tiles = new(1000);
[ViewVariables]
private readonly HashSet<TileAtmosphere> _activeTiles = new(1000);
[ViewVariables]
private int ActiveTilesCount => _activeTiles.Count;
[ViewVariables]
private double _activeTilesLastProcess;
[ViewVariables]
private readonly HashSet<TileAtmosphere> _hotspotTiles = new(1000);
[ViewVariables]
private int HotspotTilesCount => _hotspotTiles.Count;
[ViewVariables]
private double _hotspotsLastProcess;
[ViewVariables]
private readonly HashSet<TileAtmosphere> _superconductivityTiles = new(1000);
[ViewVariables]
private int SuperconductivityTilesCount => _superconductivityTiles.Count;
[ViewVariables]
private double _superconductivityLastProcess;
[ViewVariables]
private readonly HashSet<Vector2i> _invalidatedCoords = new(1000);
[ViewVariables]
private int InvalidatedCoordsCount => _invalidatedCoords.Count;
[ViewVariables]
private HashSet<TileAtmosphere> _highPressureDelta = new(1000);
[ViewVariables]
private int HighPressureDeltaCount => _highPressureDelta.Count;
[ViewVariables]
private double _highPressureDeltaLastProcess;
[ViewVariables]
private readonly HashSet<IPipeNet> _pipeNets = new();
[ViewVariables]
private double _pipeNetLastProcess;
[ViewVariables]
private readonly HashSet<PipeNetDeviceComponent> _pipeNetDevices = new();
[ViewVariables]
private double _pipeNetDevicesLastProcess;
[ViewVariables]
private Queue<TileAtmosphere> _currentRunTiles = new();
[ViewVariables]
private Queue<ExcitedGroup> _currentRunExcitedGroups = new();
[ViewVariables]
private Queue<IPipeNet> _currentRunPipeNet = new();
[ViewVariables]
private Queue<PipeNetDeviceComponent> _currentRunPipeNetDevice = new();
[ViewVariables]
private ProcessState _state = ProcessState.TileEqualize;
public GridAtmosphereComponent()
{
_paused = false;
}
private enum ProcessState
{
TileEqualize,
ActiveTiles,
ExcitedGroups,
HighPressureDelta,
Hotspots,
Superconductivity,
PipeNet,
PipeNetDevices,
}
/// <inheritdoc />
public virtual void PryTile(Vector2i indices)
{
if (IsSpace(indices) || IsAirBlocked(indices)) return;
indices.PryTile(_gridId, _mapManager, _tileDefinitionManager, _serverEntityManager);
}
void ISerializationHooks.BeforeSerialization()
{
var uniqueMixes = new List<GasMixture>();
var uniqueMixHash = new Dictionary<GasMixture, int>();
var tiles = new Dictionary<Vector2i, int>();
foreach (var (indices, tile) in Tiles)
{
if (tile.Air == null) continue;
if (uniqueMixHash.TryGetValue(tile.Air, out var index))
{
tiles[indices] = index;
continue;
}
uniqueMixes.Add(tile.Air);
var newIndex = uniqueMixes.Count - 1;
uniqueMixHash[tile.Air] = newIndex;
tiles[indices] = newIndex;
}
if (uniqueMixes.Count == 0) uniqueMixes = null;
if (tiles.Count == 0) tiles = null;
_uniqueMixes = uniqueMixes;
_tiles = tiles;
}
public override void Initialize()
{
base.Initialize();
Tiles.Clear();
if (_tiles != null && Owner.TryGetComponent(out IMapGridComponent? mapGrid))
{
foreach (var (indices, mix) in _tiles)
{
try
{
Tiles.Add(indices, new TileAtmosphere(this, mapGrid.GridIndex, indices, (GasMixture) _uniqueMixes![mix].Clone()));
}
catch (ArgumentOutOfRangeException)
{
Logger.Error($"Error during atmos serialization! Tile at {indices} points to an unique mix ({mix}) out of range!");
throw;
}
Invalidate(indices);
}
}
GridTileLookupSystem = EntitySystem.Get<GridTileLookupSystem>();
GasTileOverlaySystem = EntitySystem.Get<GasTileOverlaySystem>();
AtmosphereSystem = EntitySystem.Get<AtmosphereSystem>();
RepopulateTiles();
}
public override void OnAdd()
{
base.OnAdd();
if (Owner.TryGetComponent(out IMapGridComponent? mapGrid))
_gridId = mapGrid.GridIndex;
}
public virtual void RepopulateTiles()
{
if (!Owner.TryGetComponent(out IMapGridComponent? mapGrid)) return;
foreach (var tile in mapGrid.Grid.GetAllTiles())
{
if(!Tiles.ContainsKey(tile.GridIndices))
Tiles.Add(tile.GridIndices, new TileAtmosphere(this, tile.GridIndex, tile.GridIndices, new GasMixture(GetVolumeForCells(1), AtmosphereSystem){Temperature = Atmospherics.T20C}));
Invalidate(tile.GridIndices);
}
foreach (var (_, tile) in Tiles.ToArray())
{
tile.UpdateAdjacent();
tile.UpdateVisuals();
}
}
/// <inheritdoc />
public virtual void Invalidate(Vector2i indices)
{
_invalidatedCoords.Add(indices);
}
protected virtual void Revalidate()
{
foreach (var indices in _invalidatedCoords)
{
var tile = GetTile(indices);
if (tile == null)
{
tile = new TileAtmosphere(this, _gridId, indices, new GasMixture(GetVolumeForCells(1), AtmosphereSystem){Temperature = Atmospherics.T20C});
Tiles[indices] = tile;
}
var isAirBlocked = IsAirBlocked(indices);
if (IsSpace(indices) && !isAirBlocked)
{
tile.Air = new GasMixture(GetVolumeForCells(1), AtmosphereSystem);
tile.Air.MarkImmutable();
Tiles[indices] = tile;
} else if (isAirBlocked)
{
var nullAir = false;
foreach (var airtight in GetObstructingComponents(indices))
{
if (airtight.NoAirWhenFullyAirBlocked)
{
nullAir = true;
break;
}
}
if(nullAir)
tile.Air = null;
}
else
{
if (tile.Air == null && NeedsVacuumFixing(indices))
{
FixVacuum(tile.GridIndices);
}
// Tile used to be space, but isn't anymore.
if (tile.Air?.Immutable ?? false)
{
tile.Air = null;
}
tile.Air ??= new GasMixture(GetVolumeForCells(1), AtmosphereSystem){Temperature = Atmospherics.T20C};
}
// By removing the active tile, we effectively remove its excited group, if any.
RemoveActiveTile(tile);
// Then we activate the tile again.
AddActiveTile(tile);
tile.BlockedAirflow = GetBlockedDirections(indices);
// TODO ATMOS: Query all the contents of this tile (like walls) and calculate the correct thermal conductivity
tile.ThermalConductivity = tile.Tile?.Tile.GetContentTileDefinition().ThermalConductivity ?? 0.5f;
tile.UpdateAdjacent();
GasTileOverlaySystem.Invalidate(_gridId, indices);
for (var i = 0; i < Atmospherics.Directions; i++)
{
var direction = (AtmosDirection) (1 << i);
var otherIndices = indices.Offset(direction.ToDirection());
var otherTile = GetTile(otherIndices);
if (otherTile != null) AddActiveTile(otherTile);
}
}
_invalidatedCoords.Clear();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void UpdateAdjacentBits(Vector2i indices)
{
GetTile(indices)?.UpdateAdjacent();
}
/// <inheritdoc />
public virtual void FixVacuum(Vector2i indices)
{
var tile = GetTile(indices);
if (tile?.GridIndex != _gridId) return;
// includeAirBlocked is false, therefore all tiles in this have Air != null.
var adjacent = GetAdjacentTiles(indices);
tile.Air = new GasMixture(GetVolumeForCells(1), AtmosphereSystem){Temperature = Atmospherics.T20C};
Tiles[indices] = tile;
var ratio = 1f / adjacent.Count;
foreach (var (_, adj) in adjacent)
{
var mix = adj.Air!.RemoveRatio(ratio);
tile.Air.Merge(mix);
adj.Air.Merge(mix);
}
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual void AddActiveTile(TileAtmosphere tile)
{
if (tile?.GridIndex != _gridId || tile.Air == null) return;
tile.Excited = true;
_activeTiles.Add(tile);
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual void RemoveActiveTile(TileAtmosphere tile, bool disposeGroup = true)
{
_activeTiles.Remove(tile);
tile.Excited = false;
if(disposeGroup)
tile.ExcitedGroup?.Dispose();
else
tile.ExcitedGroup?.RemoveTile(tile);
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual void AddHotspotTile(TileAtmosphere tile)
{
if (tile?.GridIndex != _gridId || tile?.Air == null) return;
_hotspotTiles.Add(tile);
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual void RemoveHotspotTile(TileAtmosphere tile)
{
_hotspotTiles.Remove(tile);
}
public virtual void AddSuperconductivityTile(TileAtmosphere tile)
{
if (tile?.GridIndex != _gridId || !AtmosphereSystem.Superconduction) return;
_superconductivityTiles.Add(tile);
}
public virtual void RemoveSuperconductivityTile(TileAtmosphere tile)
{
_superconductivityTiles.Remove(tile);
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual void AddHighPressureDelta(TileAtmosphere tile)
{
if (tile.GridIndex != _gridId) return;
_highPressureDelta.Add(tile);
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual bool HasHighPressureDelta(TileAtmosphere tile)
{
return _highPressureDelta.Contains(tile);
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual void AddExcitedGroup(ExcitedGroup excitedGroup)
{
_excitedGroups.Add(excitedGroup);
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual void RemoveExcitedGroup(ExcitedGroup excitedGroup)
{
_excitedGroups.Remove(excitedGroup);
}
public virtual void AddPipeNet(IPipeNet pipeNet)
{
_pipeNets.Add(pipeNet);
}
public virtual void RemovePipeNet(IPipeNet pipeNet)
{
_pipeNets.Remove(pipeNet);
}
public virtual void AddPipeNetDevice(PipeNetDeviceComponent pipeNetDevice)
{
_pipeNetDevices.Add(pipeNetDevice);
}
public virtual void RemovePipeNetDevice(PipeNetDeviceComponent pipeNetDevice)
{
_pipeNetDevices.Remove(pipeNetDevice);
}
/// <inheritdoc />
public virtual TileAtmosphere? GetTile(EntityCoordinates coordinates, bool createSpace = true)
{
return GetTile(coordinates.ToVector2i(_serverEntityManager, _mapManager), createSpace);
}
/// <inheritdoc />
public virtual TileAtmosphere? GetTile(Vector2i indices, bool createSpace = true)
{
if (Tiles.TryGetValue(indices, out var tile)) return tile;
// We don't have that tile!
if (IsSpace(indices) && createSpace)
{
return new TileAtmosphere(this, _gridId, indices, new GasMixture(GetVolumeForCells(1), AtmosphereSystem){Temperature = Atmospherics.TCMB}, true);
}
return null;
}
/// <inheritdoc />
public bool IsAirBlocked(Vector2i indices, AtmosDirection direction = AtmosDirection.All)
{
var directions = AtmosDirection.Invalid;
foreach (var obstructingComponent in GetObstructingComponents(indices))
{
if (!obstructingComponent.AirBlocked)
continue;
// We set the directions that are air-blocked so far,
// as you could have a full obstruction with only 4 directional air blockers.
directions |= obstructingComponent.AirBlockedDirection;
if (directions.IsFlagSet(direction))
return true;
}
return false;
}
/// <inheritdoc />
public virtual bool IsSpace(Vector2i indices)
{
if (_mapGridComponent == null) return default;
return _mapGridComponent.Grid.GetTileRef(indices).IsSpace();
}
public Dictionary<AtmosDirection, TileAtmosphere> GetAdjacentTiles(EntityCoordinates coordinates, bool includeAirBlocked = false)
{
return GetAdjacentTiles(coordinates.ToVector2i(_serverEntityManager, _mapManager), includeAirBlocked);
}
public Dictionary<AtmosDirection, TileAtmosphere> GetAdjacentTiles(Vector2i indices, bool includeAirBlocked = false)
{
var sides = new Dictionary<AtmosDirection, TileAtmosphere>();
for (var i = 0; i < Atmospherics.Directions; i++)
{
var direction = (AtmosDirection) (1 << i);
var side = indices.Offset(direction.ToDirection());
var tile = GetTile(side);
if (tile != null && (tile.Air != null || includeAirBlocked))
sides[direction] = tile;
}
return sides;
}
public long EqualizationQueueCycleControl { get; set; }
/// <inheritdoc />
public float GetVolumeForCells(int cellCount)
{
if (_mapGridComponent == null) return default;
return _mapGridComponent.Grid.TileSize * cellCount * Atmospherics.CellVolume;
}
/// <inheritdoc />
public virtual void Update(float frameTime)
{
_timer += frameTime;
var atmosTime = 1f/AtmosphereSystem.AtmosTickRate;
if (_invalidatedCoords.Count != 0)
Revalidate();
if (_timer < atmosTime)
return;
// We subtract it so it takes lost time into account.
_timer -= atmosTime;
var maxProcessTime = AtmosphereSystem.AtmosMaxProcessTime;
switch (_state)
{
case ProcessState.TileEqualize:
if (!ProcessTileEqualize(_paused, maxProcessTime))
{
_paused = true;
return;
}
_paused = false;
_state = ProcessState.ActiveTiles;
return;
case ProcessState.ActiveTiles:
if (!ProcessActiveTiles(_paused, maxProcessTime))
{
_paused = true;
return;
}
_paused = false;
_state = ProcessState.ExcitedGroups;
return;
case ProcessState.ExcitedGroups:
if (!ProcessExcitedGroups(_paused, maxProcessTime))
{
_paused = true;
return;
}
_paused = false;
_state = ProcessState.HighPressureDelta;
return;
case ProcessState.HighPressureDelta:
if (!ProcessHighPressureDelta(_paused, maxProcessTime))
{
_paused = true;
return;
}
_paused = false;
_state = ProcessState.Hotspots;
break;
case ProcessState.Hotspots:
if (!ProcessHotspots(_paused, maxProcessTime))
{
_paused = true;
return;
}
_paused = false;
// Next state depends on whether superconduction is enabled or not.
// Note: We do this here instead of on the tile equalization step to prevent ending it early.
// Therefore, a change to this CVar might only be applied after that step is over.
_state = AtmosphereSystem.Superconduction ? ProcessState.Superconductivity : ProcessState.PipeNet;
break;
case ProcessState.Superconductivity:
if (!ProcessSuperconductivity(_paused, maxProcessTime))
{
_paused = true;
return;
}
_paused = false;
_state = ProcessState.PipeNet;
break;
case ProcessState.PipeNet:
if (!ProcessPipeNets(_paused, maxProcessTime))
{
_paused = true;
return;
}
_paused = false;
_state = ProcessState.PipeNetDevices;
break;
case ProcessState.PipeNetDevices:
if (!ProcessPipeNetDevices(_paused, maxProcessTime))
{
_paused = true;
return;
}
_paused = false;
// Next state depends on whether monstermos equalization is enabled or not.
// Note: We do this here instead of on the tile equalization step to prevent ending it early.
// Therefore, a change to this CVar might only be applied after that step is over.
_state = AtmosphereSystem.MonstermosEqualization ? ProcessState.TileEqualize : ProcessState.ActiveTiles;
break;
}
UpdateCounter++;
}
public virtual bool ProcessTileEqualize(bool resumed = false, float lagCheck = 5f)
{
_stopwatch.Restart();
if(!resumed)
_currentRunTiles = new Queue<TileAtmosphere>(_activeTiles);
var number = 0;
while (_currentRunTiles.Count > 0)
{
var tile = _currentRunTiles.Dequeue();
tile.EqualizePressureInZone(UpdateCounter);
if (number++ < LagCheckIterations) continue;
number = 0;
// Process the rest next time.
if (_stopwatch.Elapsed.TotalMilliseconds >= lagCheck)
{
_tileEqualizeLastProcess = _stopwatch.Elapsed.TotalMilliseconds;
return false;
}
}
_tileEqualizeLastProcess = _stopwatch.Elapsed.TotalMilliseconds;
return true;
}
public virtual bool ProcessActiveTiles(bool resumed = false, float lagCheck = 5f)
{
_stopwatch.Restart();
var spaceWind = AtmosphereSystem.SpaceWind;
if(!resumed)
_currentRunTiles = new Queue<TileAtmosphere>(_activeTiles);
var number = 0;
while (_currentRunTiles.Count > 0)
{
var tile = _currentRunTiles.Dequeue();
tile.ProcessCell(UpdateCounter, spaceWind);
if (number++ < LagCheckIterations) continue;
number = 0;
// Process the rest next time.
if (_stopwatch.Elapsed.TotalMilliseconds >= lagCheck)
{
_activeTilesLastProcess = _stopwatch.Elapsed.TotalMilliseconds;
return false;
}
}
_activeTilesLastProcess = _stopwatch.Elapsed.TotalMilliseconds;
return true;
}
public virtual bool ProcessExcitedGroups(bool resumed = false, float lagCheck = 5f)
{
_stopwatch.Restart();
var spaceIsAllConsuming = AtmosphereSystem.ExcitedGroupsSpaceIsAllConsuming;
if(!resumed)
_currentRunExcitedGroups = new Queue<ExcitedGroup>(_excitedGroups);
var number = 0;
while (_currentRunExcitedGroups.Count > 0)
{
var excitedGroup = _currentRunExcitedGroups.Dequeue();
excitedGroup.BreakdownCooldown++;
excitedGroup.DismantleCooldown++;
if(excitedGroup.BreakdownCooldown > Atmospherics.ExcitedGroupBreakdownCycles)
excitedGroup.SelfBreakdown(spaceIsAllConsuming);
else if(excitedGroup.DismantleCooldown > Atmospherics.ExcitedGroupsDismantleCycles)
excitedGroup.Dismantle();
if (number++ < LagCheckIterations) continue;
number = 0;
// Process the rest next time.
if (_stopwatch.Elapsed.TotalMilliseconds >= lagCheck)
{
_excitedGroupLastProcess = _stopwatch.Elapsed.TotalMilliseconds;
return false;
}
}
_excitedGroupLastProcess = _stopwatch.Elapsed.TotalMilliseconds;
return true;
}
public virtual bool ProcessHighPressureDelta(bool resumed = false, float lagCheck = 5f)
{
_stopwatch.Restart();
if(!resumed)
_currentRunTiles = new Queue<TileAtmosphere>(_highPressureDelta);
var number = 0;
while (_currentRunTiles.Count > 0)
{
var tile = _currentRunTiles.Dequeue();
tile.HighPressureMovements();
tile.PressureDifference = 0f;
tile.PressureSpecificTarget = null;
_highPressureDelta.Remove(tile);
if (number++ < LagCheckIterations) continue;
number = 0;
// Process the rest next time.
if (_stopwatch.Elapsed.TotalMilliseconds >= lagCheck)
{
_highPressureDeltaLastProcess = _stopwatch.Elapsed.TotalMilliseconds;
return false;
}
}
_highPressureDeltaLastProcess = _stopwatch.Elapsed.TotalMilliseconds;
return true;
}
protected virtual bool ProcessHotspots(bool resumed = false, float lagCheck = 5f)
{
_stopwatch.Restart();
if(!resumed)
_currentRunTiles = new Queue<TileAtmosphere>(_hotspotTiles);
var number = 0;
while (_currentRunTiles.Count > 0)
{
var hotspot = _currentRunTiles.Dequeue();
hotspot.ProcessHotspot();
if (number++ < LagCheckIterations) continue;
number = 0;
// Process the rest next time.
if (_stopwatch.Elapsed.TotalMilliseconds >= lagCheck)
{
_hotspotsLastProcess = _stopwatch.Elapsed.TotalMilliseconds;
return false;
}
}
_hotspotsLastProcess = _stopwatch.Elapsed.TotalMilliseconds;
return true;
}
protected virtual bool ProcessSuperconductivity(bool resumed = false, float lagCheck = 5f)
{
_stopwatch.Restart();
if(!resumed)
_currentRunTiles = new Queue<TileAtmosphere>(_superconductivityTiles);
var number = 0;
while (_currentRunTiles.Count > 0)
{
var superconductivity = _currentRunTiles.Dequeue();
superconductivity.Superconduct();
if (number++ < LagCheckIterations) continue;
number = 0;
// Process the rest next time.
if (_stopwatch.Elapsed.TotalMilliseconds >= lagCheck)
{
_superconductivityLastProcess = _stopwatch.Elapsed.TotalMilliseconds;
return false;
}
}
_superconductivityLastProcess = _stopwatch.Elapsed.TotalMilliseconds;
return true;
}
protected virtual bool ProcessPipeNets(bool resumed = false, float lagCheck = 5f)
{
_stopwatch.Restart();
if(!resumed)
_currentRunPipeNet = new Queue<IPipeNet>(_pipeNets);
var number = 0;
while (_currentRunPipeNet.Count > 0)
{
var pipenet = _currentRunPipeNet.Dequeue();
pipenet.Update();
if (number++ < LagCheckIterations) continue;
number = 0;
// Process the rest next time.
if (_stopwatch.Elapsed.TotalMilliseconds >= lagCheck)
{
_pipeNetLastProcess = _stopwatch.Elapsed.TotalMilliseconds;
return false;
}
}
_pipeNetLastProcess = _stopwatch.Elapsed.TotalMilliseconds;
return true;
}
protected virtual bool ProcessPipeNetDevices(bool resumed = false, float lagCheck = 5f)
{
_stopwatch.Restart();
if(!resumed)
_currentRunPipeNetDevice = new Queue<PipeNetDeviceComponent>(_pipeNetDevices);
var number = 0;
while (_currentRunPipeNetDevice.Count > 0)
{
var device = _currentRunPipeNetDevice.Dequeue();
device.Update();
if (number++ < LagCheckIterations) continue;
number = 0;
// Process the rest next time.
if (_stopwatch.Elapsed.TotalMilliseconds >= lagCheck)
{
_pipeNetDevicesLastProcess = _stopwatch.Elapsed.TotalMilliseconds;
return false;
}
}
_pipeNetDevicesLastProcess = _stopwatch.Elapsed.TotalMilliseconds;
return true;
}
protected virtual IEnumerable<AirtightComponent> GetObstructingComponents(Vector2i indices)
{
var gridLookup = EntitySystem.Get<GridTileLookupSystem>();
foreach (var v in gridLookup.GetEntitiesIntersecting(_gridId, indices))
{
if (v.TryGetComponent<AirtightComponent>(out var ac))
yield return ac;
}
}
private bool NeedsVacuumFixing(Vector2i indices)
{
var value = false;
foreach (var airtightComponent in GetObstructingComponents(indices))
{
value |= airtightComponent.FixVacuum;
}
return value;
}
private AtmosDirection GetBlockedDirections(Vector2i indices)
{
var value = AtmosDirection.Invalid;
foreach (var airtightComponent in GetObstructingComponents(indices))
{
if(airtightComponent.AirBlocked)
value |= airtightComponent.AirBlockedDirection;
}
return value;
}
public void Dispose()
{
}
public IEnumerator<TileAtmosphere> GetEnumerator()
{
return Tiles.Values.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <inheritdoc />
public virtual void BurnTile(Vector2i gridIndices)
{
// TODO ATMOS
}
}
}

View File

@@ -1,128 +0,0 @@
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Atmos;
using Content.Shared.MobState;
using Content.Shared.Physics;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos
{
[RegisterComponent]
public class MovedByPressureComponent : Component
{
[Dependency] private readonly IRobustRandom _robustRandom = default!;
public override string Name => "MovedByPressure";
private const float MoveForcePushRatio = 1f;
private const float MoveForceForcePushRatio = 1f;
private const float ProbabilityOffset = 25f;
private const float ProbabilityBasePercent = 10f;
private const float ThrowForce = 100f;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("enabled")]
public bool Enabled { get; set; } = true;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("pressureResistance")]
public float PressureResistance { get; set; } = 1f;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("moveResist")]
public float MoveResist { get; set; } = 100f;
[ViewVariables(VVAccess.ReadWrite)]
public int LastHighPressureMovementAirCycle { get; set; } = 0;
public void ExperiencePressureDifference(int cycle, float pressureDifference, AtmosDirection direction,
float pressureResistanceProbDelta, EntityCoordinates throwTarget)
{
if (!Owner.TryGetComponent(out PhysicsComponent? physics))
return;
physics.WakeBody();
// TODO ATMOS stuns?
var transform = physics.Owner.Transform;
var maxForce = MathF.Sqrt(pressureDifference) * 2.25f;
var moveProb = 100f;
if (PressureResistance > 0)
moveProb = MathF.Abs((pressureDifference / PressureResistance * ProbabilityBasePercent) -
ProbabilityOffset);
if (moveProb > ProbabilityOffset && _robustRandom.Prob(MathF.Min(moveProb / 100f, 1f))
&& !float.IsPositiveInfinity(MoveResist)
&& (physics.BodyType != BodyType.Static
&& (maxForce >= (MoveResist * MoveForcePushRatio)))
|| (physics.BodyType == BodyType.Static && (maxForce >= (MoveResist * MoveForceForcePushRatio))))
{
if (physics.Owner.HasComponent<IMobStateComponent>())
{
physics.BodyStatus = BodyStatus.InAir;
foreach (var fixture in physics.Fixtures)
{
fixture.CollisionMask &= ~(int) CollisionGroup.VaultImpassable;
}
Owner.SpawnTimer(2000, () =>
{
if (Deleted || !Owner.TryGetComponent(out PhysicsComponent? physicsComponent)) return;
// Uhh if you get race conditions good luck buddy.
if (physicsComponent.Owner.HasComponent<IMobStateComponent>())
{
physicsComponent.BodyStatus = BodyStatus.OnGround;
}
foreach (var fixture in physics.Fixtures)
{
fixture.CollisionMask |= (int) CollisionGroup.VaultImpassable;
}
});
}
if (maxForce > ThrowForce)
{
// Vera please fix ;-;
if (throwTarget != EntityCoordinates.Invalid)
{
var moveForce = maxForce * MathHelper.Clamp(moveProb, 0, 100) / 15f;
var pos = ((throwTarget.Position - transform.Coordinates.Position).Normalized + direction.ToDirection().ToVec()).Normalized;
physics.ApplyLinearImpulse(pos * moveForce);
}
else
{
var moveForce = MathF.Min(maxForce * MathHelper.Clamp(moveProb, 0, 100) / 2500f, 20f);
physics.ApplyLinearImpulse(direction.ToDirection().ToVec() * moveForce);
}
LastHighPressureMovementAirCycle = cycle;
}
}
}
}
public static class MovedByPressureExtensions
{
public static bool IsMovedByPressure(this IEntity entity)
{
return entity.IsMovedByPressure(out _);
}
public static bool IsMovedByPressure(this IEntity entity, [NotNullWhen(true)] out MovedByPressureComponent? moved)
{
return entity.TryGetComponent(out moved) &&
moved.Enabled;
}
}
}

View File

@@ -1,102 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos.Piping
{
[RegisterComponent]
public class GasCanisterPortComponent : Component
{
public override string Name => "GasCanisterPort";
[Dependency] private readonly IMapManager _mapManager = default!;
[ViewVariables]
public GasCanisterComponent? ConnectedCanister { get; private set; }
[ViewVariables]
public bool ConnectedToCanister => ConnectedCanister != null;
[ViewVariables]
private PipeNode? _gasPort;
public override void Initialize()
{
base.Initialize();
Owner.EnsureComponentWarn<PipeNetDeviceComponent>();
SetGasPort();
if (Owner.Transform.Anchored)
{
var grid = _mapManager.GetGrid(Owner.Transform.GridID);
var coords = Owner.Transform.Coordinates;
var entities = grid.GetLocal(coords);
foreach (var entity in entities)
{
if (Owner.EntityManager.ComponentManager.TryGetComponent<GasCanisterComponent>(entity, out var canister) && canister.Anchored && !canister.ConnectedToPort)
{
canister.TryConnectToPort();
break;
}
}
}
}
public override void OnRemove()
{
base.OnRemove();
ConnectedCanister?.DisconnectFromPort();
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case PipeNetUpdateMessage:
Update();
break;
}
}
public void Update()
{
if (_gasPort == null || ConnectedCanister == null)
return;
ConnectedCanister.Air.Share(_gasPort.Air, 1);
ConnectedCanister.AirWasUpdated();
}
public void ConnectGasCanister(GasCanisterComponent gasCanister)
{
ConnectedCanister = gasCanister;
}
public void DisconnectGasCanister()
{
ConnectedCanister = null;
}
private void SetGasPort()
{
if (!Owner.TryGetComponent<NodeContainerComponent>(out var container))
{
Logger.Warning($"{nameof(GasCanisterPortComponent)} on {Owner?.Prototype?.ID}, Uid {Owner?.Uid} did not have a {nameof(NodeContainerComponent)}.");
return;
}
_gasPort = container.Nodes.OfType<PipeNode>().FirstOrDefault();
if (_gasPort == null)
{
Logger.Warning($"{nameof(GasCanisterPortComponent)} on {Owner?.Prototype?.ID}, Uid {Owner?.Uid} could not find compatible {nameof(PipeNode)}s on its {nameof(NodeContainerComponent)}.");
return;
}
}
}
}

View File

@@ -1,170 +0,0 @@
#nullable enable
using System;
using System.Linq;
using Content.Server.Atmos;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using Content.Shared.GameObjects.Components.Atmos;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos.Piping
{
[RegisterComponent]
public class GasFilterComponent : Component
{
public override string Name => "GasFilter";
/// <summary>
/// If the filter is currently filtering.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool FilterEnabled
{
get => _filterEnabled;
set
{
_filterEnabled = value;
UpdateAppearance();
}
}
private bool _filterEnabled;
[ViewVariables(VVAccess.ReadWrite)]
public Gas GasToFilter
{
get => _gasToFilter;
set
{
_gasToFilter = value;
UpdateAppearance();
}
}
[DataField("gasToFilter")] private Gas _gasToFilter = Gas.Plasma;
[ViewVariables(VVAccess.ReadWrite)]
public int VolumeFilterRate
{
get => _volumeFilterRate;
set => _volumeFilterRate = Math.Clamp(value, 0, MaxVolumeFilterRate);
}
[DataField("startingVolumePumpRate")]
private int _volumeFilterRate;
[ViewVariables(VVAccess.ReadWrite)]
public int MaxVolumeFilterRate
{
get => _maxVolumeFilterRate;
set => Math.Max(value, 0);
}
[DataField("maxVolumePumpRate")] private int _maxVolumeFilterRate = 100;
[DataField("inletDirection")] [ViewVariables]
private PipeDirection _initialInletDirection = PipeDirection.None;
/// <summary>
/// The direction the filtered-out gas goes.
/// </summary>
[DataField("filterOutletDirection")] [ViewVariables]
private PipeDirection _initialFilterOutletDirection = PipeDirection.None;
/// <summary>
/// The direction the rest of the gas goes.
/// </summary>
[DataField("outletDirection")] [ViewVariables]
private PipeDirection _initialOutletDirection = PipeDirection.None;
[ViewVariables]
private PipeNode? _inletPipe;
[ViewVariables]
private PipeNode? _filterOutletPipe;
[ViewVariables]
private PipeNode? _outletPipe;
[ComponentDependency]
private readonly AppearanceComponent? _appearance = default;
public override void Initialize()
{
base.Initialize();
Owner.EnsureComponent<PipeNetDeviceComponent>();
SetPipes();
UpdateAppearance();
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case PipeNetUpdateMessage:
Update();
break;
}
}
public void Update()
{
if (!FilterEnabled)
return;
if (_inletPipe == null || _inletPipe.Air == null ||
_filterOutletPipe == null || _filterOutletPipe.Air == null ||
_outletPipe == null || _outletPipe.Air == null)
return;
FilterGas(_inletPipe.Air, _filterOutletPipe.Air, _outletPipe.Air);
}
private void FilterGas(GasMixture inletGas, GasMixture filterOutletGas, GasMixture outletGas)
{
var volumeRatio = Math.Clamp(VolumeFilterRate / inletGas.Volume, 0, 1);
var gas = inletGas.RemoveRatio(volumeRatio);
var molesToSeperate = gas.GetMoles(GasToFilter);
gas.SetMoles(GasToFilter, 0);
filterOutletGas.AdjustMoles(GasToFilter, molesToSeperate);
outletGas.Merge(gas);
}
private void UpdateAppearance()
{
_appearance?.SetData(FilterVisuals.VisualState, new FilterVisualState(FilterEnabled));
}
private void SetPipes()
{
_inletPipe = null;
_filterOutletPipe = null;
_outletPipe = null;
if (!Owner.TryGetComponent<NodeContainerComponent>(out var container))
{
Logger.Warning($"{typeof(GasFilterComponent)} on {Owner?.Prototype?.ID}, Uid {Owner?.Uid} did not have a {nameof(NodeContainerComponent)}.");
return;
}
var pipeNodes = container.Nodes.OfType<PipeNode>();
_inletPipe = pipeNodes.Where(pipe => pipe.PipeDirection == _initialInletDirection).FirstOrDefault();
_filterOutletPipe = pipeNodes.Where(pipe => pipe.PipeDirection == _initialFilterOutletDirection).FirstOrDefault();
_outletPipe = pipeNodes.Where(pipe => pipe.PipeDirection == _initialOutletDirection).FirstOrDefault();
if (_inletPipe == null || _filterOutletPipe == null || _outletPipe == null)
{
Logger.Warning($"{nameof(GasFilterComponent)} on {Owner?.Prototype?.ID}, Uid {Owner?.Uid} could not find compatible {nameof(PipeNode)}s on its {nameof(NodeContainerComponent)}.");
return;
}
}
}
}

View File

@@ -1,99 +0,0 @@
#nullable enable
using System.Linq;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos.Piping
{
/// <summary>
/// Generates gas in the attached pipe.
/// </summary>
[RegisterComponent]
public class GasGeneratorComponent : Component
{
public override string Name => "GasGenerator";
/// <summary>
/// If the generator is producing gas.
/// </summary>
[DataField("generatorEnabled")]
[ViewVariables(VVAccess.ReadWrite)]
public bool GeneratorEnabled { get; set; } = true;
/// <summary>
/// What gas is being generated.
/// </summary>
[DataField("generatedGas")]
[ViewVariables(VVAccess.ReadWrite)]
public Gas GeneratedGas { get; set; } = Gas.Oxygen;
/// <summary>
/// Molar rate of gas generation.
/// </summary>
[DataField("gasGenerationRate")]
[ViewVariables(VVAccess.ReadWrite)]
public float GasGenerationRate { get; set; } = 10;
/// <summary>
/// The pipe pressure above which the generator stops producing gas.
/// </summary>
[DataField("generatorPressureCap")]
[ViewVariables(VVAccess.ReadWrite)]
public float GeneratorPressureCap { get; set; } = 10;
/// <summary>
/// The pipe to which generated gas is added.
/// </summary>
[ViewVariables]
private PipeNode? Pipe { get; set; }
public override void Initialize()
{
base.Initialize();
Owner.EnsureComponentWarn<PipeNetDeviceComponent>();
SetPipes();
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case PipeNetUpdateMessage:
Update();
break;
}
}
private void Update()
{
if (!GeneratorEnabled)
return;
if (Pipe == null || Pipe.Air.Pressure > GeneratorPressureCap)
return;
Pipe.Air.AdjustMoles(GeneratedGas, GasGenerationRate);
}
private void SetPipes()
{
if (!Owner.TryGetComponent<NodeContainerComponent>(out var container))
{
Logger.Warning($"{nameof(GasGeneratorComponent)} on {Owner?.Prototype?.ID}, Uid {Owner?.Uid} did not have a {nameof(NodeContainerComponent)}.");
return;
}
Pipe = container.Nodes.OfType<PipeNode>().FirstOrDefault();
if (Pipe == null)
{
Logger.Warning($"{nameof(GasGeneratorComponent)} on {Owner?.Prototype?.ID}, Uid {Owner?.Uid} could not find compatible {nameof(PipeNode)}s on its {nameof(NodeContainerComponent)}.");
return;
}
}
}
}

View File

@@ -1,66 +0,0 @@
#nullable enable
using System.Linq;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos.Piping
{
/// <summary>
/// Placeholder component for adjusting the temperature of gas in pipes.
/// </summary>
[RegisterComponent]
public class PipeHeaterComponent : Component
{
public override string Name => "PipeHeater";
[ViewVariables]
private PipeNode? _heaterPipe;
[ViewVariables(VVAccess.ReadWrite)]
private float TargetTemperature { get; set; }
public override void Initialize()
{
base.Initialize();
Owner.EnsureComponentWarn<PipeNetDeviceComponent>();
SetPipe();
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case PipeNetUpdateMessage:
Update();
break;
}
}
public void Update()
{
if (_heaterPipe == null)
return;
_heaterPipe.Air.Temperature = TargetTemperature;
}
private void SetPipe()
{
if (!Owner.TryGetComponent<NodeContainerComponent>(out var container))
{
Logger.Warning($"{nameof(PipeHeaterComponent)} on {Owner?.Prototype?.ID}, Uid {Owner?.Uid} did not have a {nameof(NodeContainerComponent)}.");
return;
}
_heaterPipe = container.Nodes.OfType<PipeNode>().FirstOrDefault();
if (_heaterPipe == null)
{
Logger.Warning($"{nameof(PipeHeaterComponent)} on {Owner?.Prototype?.ID}, Uid {Owner?.Uid} could not find compatible {nameof(PipeNode)}s on its {nameof(NodeContainerComponent)}.");
return;
}
}
}
}

View File

@@ -1,57 +0,0 @@
#nullable enable
using Content.Server.Atmos;
using Content.Server.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.Components.Atmos.Piping
{
/// <summary>
/// Adds itself to a <see cref="IGridAtmosphereComponent"/> to be updated by.
/// TODO: Make compatible with unanchoring/anchoring. Currently assumes that the Owner does not move.
/// </summary>
[RegisterComponent]
public class PipeNetDeviceComponent : Component
{
public override string Name => "PipeNetDevice";
private IGridAtmosphereComponent? JoinedGridAtmos { get; set; }
private PipeNetUpdateMessage _cachedUpdateMessage = new();
public override void Initialize()
{
base.Initialize();
JoinGridAtmos();
}
public override void OnRemove()
{
base.OnRemove();
LeaveGridAtmos();
}
public void Update()
{
SendMessage(_cachedUpdateMessage);
}
private void JoinGridAtmos()
{
var gridAtmos = EntitySystem.Get<AtmosphereSystem>()
.GetGridAtmosphere(Owner.Transform.Coordinates);
JoinedGridAtmos = gridAtmos;
JoinedGridAtmos.AddPipeNetDevice(this);
}
private void LeaveGridAtmos()
{
JoinedGridAtmos?.RemovePipeNetDevice(this);
JoinedGridAtmos = null;
}
}
public class PipeNetUpdateMessage : ComponentMessage
{
}
}

View File

@@ -1,124 +0,0 @@
#nullable enable
using System.Linq;
using Content.Server.Atmos;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.GameObjects.Components.Atmos;
using Content.Shared.Interaction;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos.Piping.Pumps
{
/// <summary>
/// Transfer gas from one <see cref="PipeNode"/> to another.
/// </summary>
public abstract class BasePumpComponent : Component, IActivate
{
/// <summary>
/// If the pump is currently pumping.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool PumpEnabled
{
get => _pumpEnabled;
set
{
_pumpEnabled = value;
UpdateAppearance();
}
}
[DataField("pumpEnabled")]
private bool _pumpEnabled;
/// <summary>
/// Needs to be same <see cref="PipeDirection"/> as that of a <see cref="PipeNode"/> on this entity.
/// </summary>
[ViewVariables]
[DataField("initialInletDirection", required: true)]
private PipeDirection _initialInletDirection = PipeDirection.None;
/// <summary>
/// Needs to be same <see cref="PipeDirection"/> as that of a <see cref="PipeNode"/> on this entity.
/// </summary>
[ViewVariables]
[DataField("initialOutletDirection", required: true)]
private PipeDirection _initialOutletDirection = PipeDirection.None;
[ViewVariables]
private PipeNode? _inletPipe;
[ViewVariables]
private PipeNode? _outletPipe;
private AppearanceComponent? _appearance;
public override void Initialize()
{
base.Initialize();
Owner.EnsureComponentWarn<PipeNetDeviceComponent>();
SetPipes();
Owner.TryGetComponent(out _appearance);
UpdateAppearance();
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case PipeNetUpdateMessage:
Update();
break;
}
}
public void Update()
{
if (!PumpEnabled)
return;
if (_inletPipe == null || _outletPipe == null)
return;
PumpGas(_inletPipe.Air, _outletPipe.Air);
}
protected abstract void PumpGas(GasMixture inletGas, GasMixture outletGas);
private void UpdateAppearance()
{
if (_inletPipe == null || _outletPipe == null) return;
_appearance?.SetData(PumpVisuals.VisualState, new PumpVisualState(_initialInletDirection, _initialOutletDirection, PumpEnabled));
}
void IActivate.Activate(ActivateEventArgs eventArgs)
{
PumpEnabled = !PumpEnabled;
}
private void SetPipes()
{
_inletPipe = null;
_outletPipe = null;
if (!Owner.TryGetComponent<NodeContainerComponent>(out var container))
{
Logger.Warning($"{nameof(BasePumpComponent)} on {Owner?.Prototype?.ID}, Uid {Owner?.Uid} did not have a {nameof(NodeContainerComponent)}.");
return;
}
var pipeNodes = container.Nodes.OfType<PipeNode>();
_inletPipe = pipeNodes.Where(pipe => pipe.PipeDirection == _initialInletDirection).FirstOrDefault();
_outletPipe = pipeNodes.Where(pipe => pipe.PipeDirection == _initialOutletDirection).FirstOrDefault();
if (_inletPipe == null || _outletPipe == null)
{
Logger.Warning($"{nameof(BasePumpComponent)} on {Owner?.Prototype?.ID}, Uid {Owner?.Uid} could not find compatible {nameof(PipeNode)}s on its {nameof(NodeContainerComponent)}.");
return;
}
}
}
}

View File

@@ -1,66 +0,0 @@
#nullable enable
using System;
using Content.Server.Atmos;
using Content.Shared.Interaction;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos.Piping.Pumps
{
[RegisterComponent]
[ComponentReference(typeof(BasePumpComponent))]
[ComponentReference(typeof(IActivate))]
public class PressurePumpComponent : BasePumpComponent
{
public override string Name => "PressurePump";
/// <summary>
/// The pressure this pump will try to bring its oulet too.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public int PressurePumpTarget
{
get => _pressurePumpTarget;
set => _pressurePumpTarget = Math.Clamp(value, 0, MaxPressurePumpTarget);
}
[DataField("startingPressurePumpTarget")]
private int _pressurePumpTarget;
/// <summary>
/// Max value <see cref="PressurePumpTarget"/> can be set to.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public int MaxPressurePumpTarget
{
get => _maxPressurePumpTarget;
set => Math.Max(value, 0);
}
[DataField("maxPressurePumpTarget")]
private int _maxPressurePumpTarget = 100;
/// <summary>
/// Every update, this pump will only increase the outlet pressure by this fraction of the amount needed to reach the <see cref="PressurePumpTarget"/>.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float TransferRatio
{
get => _transferRatio;
set => _transferRatio = Math.Clamp(value, 0, 1);
}
[DataField("transferRatio")]
private float _transferRatio = 0.5f;
protected override void PumpGas(GasMixture inletGas, GasMixture outletGas)
{
var goalDiff = PressurePumpTarget - outletGas.Pressure;
var realGoalPressureDiff = goalDiff * TransferRatio;
var realTargetPressure = outletGas.Pressure + realGoalPressureDiff;
var realCappedTargetPressure = Math.Max(realTargetPressure, outletGas.Pressure); //no lowering the outlet's pressure
inletGas.PumpGasTo(outletGas, realCappedTargetPressure);
}
}
}

View File

@@ -1,44 +0,0 @@
#nullable enable
using System;
using Content.Server.Atmos;
using Content.Shared.Interaction;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos.Piping.Pumps
{
[RegisterComponent]
[ComponentReference(typeof(BasePumpComponent))]
[ComponentReference(typeof(IActivate))]
public class VolumePumpComponent : BasePumpComponent
{
[ViewVariables(VVAccess.ReadWrite)]
public int VolumePumpRate
{
get => _volumePumpRate;
set => _volumePumpRate = Math.Clamp(value, 0, MaxVolumePumpRate);
}
[DataField("startingVolumePumpRate")]
private int _volumePumpRate;
[ViewVariables(VVAccess.ReadWrite)]
public int MaxVolumePumpRate
{
get => _maxVolumePumpRate;
set => Math.Max(value, 0);
}
[DataField("maxVolumePumpRate")]
private int _maxVolumePumpRate = 100;
public override string Name => "VolumePump";
protected override void PumpGas(GasMixture inletGas, GasMixture outletGas)
{
var volumeRatio = Math.Clamp(VolumePumpRate / inletGas.Volume, 0, 1);
outletGas.Merge(inletGas.RemoveRatio(volumeRatio));
}
}
}

View File

@@ -1,97 +0,0 @@
#nullable enable
using System.Linq;
using Content.Server.Atmos;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.GameObjects.Components.Atmos;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos.Piping.Scrubbers
{
/// <summary>
/// Transfers gas from the tile it is on to a <see cref="PipeNode"/>.
/// </summary>
public abstract class BaseSiphonComponent : Component
{
[ViewVariables]
private PipeNode? _scrubberOutlet;
private AtmosphereSystem? _atmosSystem;
[ViewVariables(VVAccess.ReadWrite)]
public bool SiphonEnabled
{
get => _siphonEnabled;
set
{
_siphonEnabled = value;
UpdateAppearance();
}
}
private bool _siphonEnabled = true;
private AppearanceComponent? _appearance;
public override void Initialize()
{
base.Initialize();
Owner.EnsureComponentWarn<PipeNetDeviceComponent>();
_atmosSystem = EntitySystem.Get<AtmosphereSystem>();
SetOutlet();
Owner.TryGetComponent(out _appearance);
UpdateAppearance();
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case PipeNetUpdateMessage:
Update();
break;
}
}
public void Update()
{
if (!SiphonEnabled)
return;
var tileAtmos = Owner.Transform.Coordinates.GetTileAtmosphere();
if (_scrubberOutlet == null || tileAtmos == null || tileAtmos.Air == null)
return;
ScrubGas(tileAtmos.Air, _scrubberOutlet.Air);
tileAtmos.Invalidate();
}
protected abstract void ScrubGas(GasMixture inletGas, GasMixture outletGas);
private void SetOutlet()
{
if (!Owner.TryGetComponent<NodeContainerComponent>(out var container))
{
Logger.Warning($"{nameof(BaseSiphonComponent)} on {Owner?.Prototype?.ID}, Uid {Owner?.Uid} did not have a {nameof(NodeContainerComponent)}.");
return;
}
_scrubberOutlet = container.Nodes.OfType<PipeNode>().FirstOrDefault();
if (_scrubberOutlet == null)
{
Logger.Warning($"{nameof(BaseSiphonComponent)} on {Owner?.Prototype?.ID}, Uid {Owner?.Uid} could not find compatible {nameof(PipeNode)}s on its {nameof(NodeContainerComponent)}.");
return;
}
}
private void UpdateAppearance()
{
_appearance?.SetData(SiphonVisuals.VisualState, new SiphonVisualState(SiphonEnabled));
}
}
}

View File

@@ -1,55 +0,0 @@
#nullable enable
using System;
using Content.Server.Atmos;
using Content.Shared.Atmos;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos.Piping.Scrubbers
{
[RegisterComponent]
[ComponentReference(typeof(BaseSiphonComponent))]
public class PressureSiphonComponent : BaseSiphonComponent
{
public override string Name => "PressureSiphon";
/// <summary>
/// The pressure this siphon will try to bring its inlet too.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float SiphonPressureTarget
{
get => _siphonPressureTarget;
set => _siphonPressureTarget = Math.Max(value, 0);
}
[DataField("startingSiphonPressureTarget")]
private float _siphonPressureTarget = Atmospherics.OneAtmosphere * 2;
/// <summary>
/// Every update, this siphon will only decrease the inlet pressure by this fraction of the amount needed to reach the <see cref="SiphonPressureTarget"/>.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float TransferRatio
{
get => _transferRatio;
set => _transferRatio = Math.Clamp(value, 0, 1);
}
[DataField("transferRatio")]
private float _transferRatio = 0.5f;
protected override void ScrubGas(GasMixture inletGas, GasMixture outletGas)
{
var goalDiff = SiphonPressureTarget - inletGas.Pressure;
var realGoalPressureDiff = goalDiff * TransferRatio;
var moleRatioToTransfer = 1 - (inletGas.Pressure + realGoalPressureDiff) / inletGas.Pressure;
moleRatioToTransfer = Math.Clamp(moleRatioToTransfer, 0, 1);
var transferedGas = inletGas.RemoveRatio(moleRatioToTransfer);
outletGas.Merge(transferedGas);
}
}
}

View File

@@ -1,97 +0,0 @@
#nullable enable
using System.Linq;
using Content.Server.Atmos;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.GameObjects.Components.Atmos;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos.Piping.Vents
{
/// <summary>
/// Transfers gas from a <see cref="PipeNode"/> to the tile it is on.
/// </summary>
public abstract class BaseVentComponent : Component
{
[ViewVariables]
private PipeNode? _ventInlet;
private AtmosphereSystem? _atmosSystem;
[ViewVariables(VVAccess.ReadWrite)]
public bool VentEnabled
{
get => _ventEnabled;
set
{
_ventEnabled = value;
UpdateAppearance();
}
}
private bool _ventEnabled = true;
private AppearanceComponent? _appearance;
public override void Initialize()
{
base.Initialize();
Owner.EnsureComponentWarn<PipeNetDeviceComponent>();
_atmosSystem = EntitySystem.Get<AtmosphereSystem>();
SetInlet();
Owner.TryGetComponent(out _appearance);
UpdateAppearance();
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case PipeNetUpdateMessage:
Update();
break;
}
}
public void Update()
{
if (!VentEnabled)
return;
var tileAtmos = Owner.Transform.Coordinates.GetTileAtmosphere();
if (_ventInlet == null || tileAtmos == null || tileAtmos.Air == null)
return;
VentGas(_ventInlet.Air, tileAtmos.Air);
tileAtmos.Invalidate();
}
protected abstract void VentGas(GasMixture inletGas, GasMixture outletGas);
private void SetInlet()
{
if (!Owner.TryGetComponent<NodeContainerComponent>(out var container))
{
Logger.Warning($"{nameof(BaseVentComponent)} on {Owner?.Prototype?.ID}, Uid {Owner?.Uid} did not have a {nameof(NodeContainerComponent)}.");
return;
}
_ventInlet = container.Nodes.OfType<PipeNode>().FirstOrDefault();
if (_ventInlet == null)
{
Logger.Warning($"{nameof(BaseVentComponent)} on {Owner?.Prototype?.ID}, Uid {Owner?.Uid} could not find compatible {nameof(PipeNode)}s on its {nameof(NodeContainerComponent)}.");
return;
}
}
private void UpdateAppearance()
{
_appearance?.SetData(VentVisuals.VisualState, new VentVisualState(VentEnabled));
}
}
}

View File

@@ -1,64 +0,0 @@
#nullable enable
using Content.Server.Atmos;
using Content.Shared.Atmos;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
using System;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.GameObjects.Components.Atmos.Piping.Vents
{
[RegisterComponent]
[ComponentReference(typeof(BaseVentComponent))]
public class PressureVentComponent : BaseVentComponent
{
public override string Name => "PressureVent";
/// <summary>
/// The pressure this vent will try to bring its oulet to.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float VentPressureTarget
{
get => _ventPressureTarget;
set => _ventPressureTarget = Math.Clamp(value, 0, MaxVentPressureTarget);
}
[DataField("startingVentPressureTarget")]
private float _ventPressureTarget = Atmospherics.OneAtmosphere;
/// <summary>
/// Max value <see cref="VentPressureTarget"/> can be set to.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float MaxVentPressureTarget
{
get => _maxVentPressureTarget;
set => Math.Max(value, 0);
}
[DataField("maxVentPressureTarget")]
private float _maxVentPressureTarget = Atmospherics.OneAtmosphere * 2;
/// <summary>
/// Every update, this vent will only increase the outlet pressure by this fraction of the amount needed to reach the <see cref="VentPressureTarget"/>.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float TransferRatio
{
get => _transferRatio;
set => _transferRatio = Math.Clamp(value, 0, 1);
}
[DataField("transferRatio")]
private float _transferRatio = 0.5f;
protected override void VentGas(GasMixture inletGas, GasMixture outletGas)
{
var goalDiff = VentPressureTarget - outletGas.Pressure;
var realGoalPressureDiff = goalDiff * TransferRatio;
var realTargetPressure = outletGas.Pressure + realGoalPressureDiff;
var realCappedTargetPressure = Math.Max(realTargetPressure, outletGas.Pressure); //no lowering the outlet's pressure
inletGas.PumpGasTo(outletGas, realCappedTargetPressure);
}
}
}

View File

@@ -1,23 +0,0 @@
using Content.Server.Pressure;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos
{
[RegisterComponent]
public class PressureProtectionComponent : Component, IPressureProtection
{
public override string Name => "PressureProtection";
[ViewVariables]
[DataField("highPressureMultiplier")]
public float HighPressureMultiplier { get; private set; } = 1f;
[ViewVariables]
[DataField("lowPressureMultiplier")]
public float LowPressureMultiplier { get; private set; } = 1f;
}
}

View File

@@ -1,34 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Content.Server.Atmos;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Content.Server.GameObjects.Components.Atmos
{
[RegisterComponent]
[ComponentReference(typeof(IGridAtmosphereComponent))]
public class SpaceGridAtmosphereComponent : UnsimulatedGridAtmosphereComponent
{
public override string Name => "SpaceGridAtmosphere";
public override void RepopulateTiles() { }
public override bool IsSpace(Vector2i indices)
{
return true;
}
public override TileAtmosphere GetTile(Vector2i indices, bool createSpace = true)
{
return new(this, GridId.Invalid, indices, new GasMixture(2500, AtmosphereSystem), true);
}
protected override IEnumerable<AirtightComponent> GetObstructingComponents(Vector2i indices)
{
return Enumerable.Empty<AirtightComponent>();
}
}
}

View File

@@ -1,112 +0,0 @@
#nullable enable
using System;
using Content.Server.Atmos;
using Content.Server.GameObjects.Components.Atmos.Piping;
using Content.Server.NodeContainer.NodeGroups;
using Content.Shared.Atmos;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
namespace Content.Server.GameObjects.Components.Atmos
{
[RegisterComponent]
[ComponentReference(typeof(IGridAtmosphereComponent))]
[ComponentReference(typeof(GridAtmosphereComponent))]
[Serializable]
public class UnsimulatedGridAtmosphereComponent : GridAtmosphereComponent, IGridAtmosphereComponent
{
public override string Name => "UnsimulatedGridAtmosphere";
public override void PryTile(Vector2i indices) { }
public override void RepopulateTiles()
{
if (!Owner.TryGetComponent(out IMapGridComponent? mapGrid)) return;
foreach (var tile in mapGrid.Grid.GetAllTiles())
{
if(!Tiles.ContainsKey(tile.GridIndices))
Tiles.Add(tile.GridIndices, new TileAtmosphere(this, tile.GridIndex, tile.GridIndices, new GasMixture(GetVolumeForCells(1)){Temperature = Atmospherics.T20C}));
}
}
public override void Invalidate(Vector2i indices) { }
protected override void Revalidate() { }
public override void FixVacuum(Vector2i indices) { }
public override void AddActiveTile(TileAtmosphere? tile) { }
public override void RemoveActiveTile(TileAtmosphere? tile, bool disposeGroup = true) { }
public override void AddHotspotTile(TileAtmosphere? tile) { }
public override void RemoveHotspotTile(TileAtmosphere? tile) { }
public override void AddSuperconductivityTile(TileAtmosphere? tile) { }
public override void RemoveSuperconductivityTile(TileAtmosphere? tile) { }
public override void AddHighPressureDelta(TileAtmosphere? tile) { }
public override bool HasHighPressureDelta(TileAtmosphere tile)
{
return false;
}
public override void AddExcitedGroup(ExcitedGroup excitedGroup) { }
public override void RemoveExcitedGroup(ExcitedGroup excitedGroup) { }
public override void AddPipeNet(IPipeNet pipeNet) { }
public override void RemovePipeNet(IPipeNet pipeNet) { }
public override void AddPipeNetDevice(PipeNetDeviceComponent pipeNetDevice) { }
public override void RemovePipeNetDevice(PipeNetDeviceComponent pipeNetDevice) { }
public override void Update(float frameTime) { }
public override bool ProcessTileEqualize(bool resumed = false, float lagCheck = 5f)
{
return false;
}
public override bool ProcessActiveTiles(bool resumed = false, float lagCheck = 5f)
{
return false;
}
public override bool ProcessExcitedGroups(bool resumed = false, float lagCheck = 5f)
{
return false;
}
public override bool ProcessHighPressureDelta(bool resumed = false, float lagCheck = 5f)
{
return false;
}
protected override bool ProcessHotspots(bool resumed = false, float lagCheck = 5f)
{
return false;
}
protected override bool ProcessSuperconductivity(bool resumed = false, float lagCheck = 5f)
{
return false;
}
protected override bool ProcessPipeNets(bool resumed = false, float lagCheck = 5f)
{
return false;
}
protected override bool ProcessPipeNetDevices(bool resumed = false, float lagCheck = 5f)
{
return false;
}
}
}

View File

@@ -1,106 +0,0 @@
using Content.Shared.GameObjects.Components.Atmos;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components
{
[RegisterComponent]
public sealed class AtmosPlaqueComponent : Component, IMapInit
{
public override string Name => "AtmosPlaque";
[DataField("plaqueType")]
private PlaqueType _type = PlaqueType.Unset;
[ViewVariables(VVAccess.ReadWrite)]
public PlaqueType Type
{
get => _type;
set
{
_type = value;
UpdateSign();
}
}
public void MapInit()
{
var random = IoCManager.Resolve<IRobustRandom>();
var rand = random.Next(100);
// Let's not pat ourselves on the back too hard.
// 1% chance of zumos
if (rand == 0) Type = PlaqueType.Zumos;
// 9% FEA
else if (rand <= 10) Type = PlaqueType.Fea;
// 45% ZAS
else if (rand <= 55) Type = PlaqueType.Zas;
// 45% LINDA
else Type = PlaqueType.Linda;
}
protected override void Startup()
{
base.Startup();
UpdateSign();
}
private void UpdateSign()
{
if (!Running)
{
return;
}
Owner.Description = _type switch
{
PlaqueType.Zumos =>
"This plaque commemorates the rise of the Atmos ZUM division. May they carry the torch that the Atmos ZAS, LINDA and FEA divisions left behind.",
PlaqueType.Fea =>
"This plaque commemorates the fall of the Atmos FEA division. For all the charred, dizzy, and brittle men who have died in its hands.",
PlaqueType.Linda =>
"This plaque commemorates the fall of the Atmos LINDA division. For all the charred, dizzy, and brittle men who have died in its hands.",
PlaqueType.Zas =>
"This plaque commemorates the fall of the Atmos ZAS division. For all the charred, dizzy, and brittle men who have died in its hands.",
PlaqueType.Unset => "Uhm",
_ => "Uhm",
};
Owner.Name = _type switch
{
PlaqueType.Zumos =>
"ZUM Atmospherics Division plaque",
PlaqueType.Fea =>
"FEA Atmospherics Division plaque",
PlaqueType.Linda =>
"LINDA Atmospherics Division plaque",
PlaqueType.Zas =>
"ZAS Atmospherics Division plaque",
PlaqueType.Unset => "Uhm",
_ => "Uhm",
};
if (Owner.TryGetComponent(out AppearanceComponent? appearance))
{
var state = _type == PlaqueType.Zumos ? "zumosplaque" : "atmosplaque";
appearance.SetData(AtmosPlaqueVisuals.State, state);
}
}
public enum PlaqueType
{
Unset = 0,
Zumos,
Fea,
Linda,
Zas
}
}
}
// If you get the ZUM plaque it means your round will be blessed with good engineering luck.

View File

@@ -1,139 +0,0 @@
using Content.Server.Power.Components;
using Content.Server.UserInterface;
using Content.Shared.ActionBlocker;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Notification.Managers;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components
{
/// <summary>
/// This component is used as a base class for classes like SolarControlConsoleComponent.
/// These components operate the server-side logic for the "primary UI" of a computer.
/// That means showing the UI when a user activates it, for example.
/// </summary>
public abstract class BaseComputerUserInterfaceComponent : Component
{
protected readonly object UserInterfaceKey;
[ViewVariables] protected BoundUserInterface? UserInterface => Owner.GetUIOrNull(UserInterfaceKey);
[ViewVariables] public bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
public BaseComputerUserInterfaceComponent(object key)
{
UserInterfaceKey = key;
}
public override void Initialize()
{
base.Initialize();
if (UserInterface != null)
UserInterface.OnReceiveMessage += OnReceiveUIMessageCallback;
}
/// <summary>
/// Internal callback used to grab session and session attached entity before any more work is done.
/// This is so that sessionEntity is always available to checks up and down the line.
/// </summary>
private void OnReceiveUIMessageCallback(ServerBoundUserInterfaceMessage obj)
{
var session = obj.Session;
var sessionEntity = session.AttachedEntity;
if (sessionEntity == null)
return; // No session entity, so we're probably not able to touch this.
OnReceiveUnfilteredUserInterfaceMessage(obj, sessionEntity);
}
/// <summary>
/// Override this to handle messages from the UI before filtering them.
/// Calling base is necessary if you want this class to have any meaning.
/// </summary>
protected void OnReceiveUnfilteredUserInterfaceMessage(ServerBoundUserInterfaceMessage obj, IEntity sessionEntity)
{
// "Across all computers" "anti-cheats" ought to be put here or at some parent level (BaseDeviceUserInterfaceComponent?)
// Determine some facts about the session.
// Powered?
if (!Powered)
{
sessionEntity.PopupMessageCursor(Loc.GetString("base-computer-ui-component-not-powered"));
return; // Not powered, so this computer should probably do nothing.
}
// Can we interact?
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(sessionEntity))
{
sessionEntity.PopupMessageCursor(Loc.GetString("base-computer-ui-component-cannot-interact"));
return;
}
// Good to go!
OnReceiveUserInterfaceMessage(obj);
}
/// <summary>
/// Override this to handle messages from the UI.
/// Calling base is unnecessary.
/// These messages will automatically be blocked if the user shouldn't be able to access this computer, or if the computer has lost power.
/// </summary>
protected virtual void OnReceiveUserInterfaceMessage(ServerBoundUserInterfaceMessage obj)
{
// Nothing!
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case PowerChangedMessage powerChanged:
PowerReceiverOnOnPowerStateChanged(powerChanged);
break;
}
}
private void PowerReceiverOnOnPowerStateChanged(PowerChangedMessage e)
{
if (!e.Powered)
{
// We need to kick off users who are using it when it loses power.
UserInterface?.CloseAll();
// Now alert subclass.
ComputerLostPower();
}
}
/// <summary>
/// Override this if you want the computer to do something when it loses power (i.e. reset state)
/// All UIs should have been closed by the time this is called.
/// Calling base is unnecessary.
/// </summary>
public virtual void ComputerLostPower()
{
}
/// <summary>
/// This is called from ComputerUIActivatorSystem.
/// Override this to add additional activation conditions of some sort.
/// Calling base runs standard activation logic.
/// *This remains inside the component for overridability.*
/// </summary>
public virtual void ActivateThunk(ActivateInWorldEvent eventArgs)
{
if (!eventArgs.User.TryGetComponent(out ActorComponent? actor))
{
return;
}
if (!Powered)
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("base-computer-ui-component-not-powered"));
return;
}
UserInterface?.Open(actor.PlayerSession);
}
}
}

View File

@@ -1,171 +0,0 @@
#nullable enable
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Atmos;
using Content.Server.Atmos;
using Content.Shared;
using Content.Shared.Atmos;
using Content.Shared.CCVar;
using Content.Shared.GameObjects.EntitySystems.Atmos;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
namespace Content.Server.GameObjects.EntitySystems.Atmos
{
[UsedImplicitly]
public sealed class AtmosDebugOverlaySystem : SharedAtmosDebugOverlaySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
/// <summary>
/// Players allowed to see the atmos debug overlay.
/// To modify it see <see cref="AddObserver"/> and
/// <see cref="RemoveObserver"/>.
/// </summary>
private readonly HashSet<IPlayerSession> _playerObservers = new();
/// <summary>
/// Overlay update ticks per second.
/// </summary>
private float _updateCooldown;
public override void Initialize()
{
base.Initialize();
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
}
public override void Shutdown()
{
base.Shutdown();
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
}
public bool AddObserver(IPlayerSession observer)
{
return _playerObservers.Add(observer);
}
public bool HasObserver(IPlayerSession observer)
{
return _playerObservers.Contains(observer);
}
public bool RemoveObserver(IPlayerSession observer)
{
if (!_playerObservers.Remove(observer))
{
return false;
}
var message = new AtmosDebugOverlayDisableMessage();
RaiseNetworkEvent(message, observer.ConnectedClient);
return true;
}
/// <summary>
/// Adds the given observer if it doesn't exist, removes it otherwise.
/// </summary>
/// <param name="observer">The observer to toggle.</param>
/// <returns>true if added, false if removed.</returns>
public bool ToggleObserver(IPlayerSession observer)
{
if (HasObserver(observer))
{
RemoveObserver(observer);
return false;
}
else
{
AddObserver(observer);
return true;
}
}
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.NewStatus != SessionStatus.InGame)
{
RemoveObserver(e.Session);
}
}
private AtmosDebugOverlayData ConvertTileToData(TileAtmosphere? tile)
{
var gases = new float[Atmospherics.TotalNumberOfGases];
if (tile?.Air == null)
{
return new AtmosDebugOverlayData(0, gases, AtmosDirection.Invalid, false, tile?.BlockedAirflow ?? AtmosDirection.Invalid);
}
else
{
for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
{
gases[i] = tile.Air.GetMoles(i);
}
return new AtmosDebugOverlayData(tile.Air.Temperature, gases, tile.PressureDirectionForDebugOverlay, tile.ExcitedGroup != null, tile.BlockedAirflow);
}
}
public override void Update(float frameTime)
{
AccumulatedFrameTime += frameTime;
_updateCooldown = 1 / _configManager.GetCVar(CCVars.NetAtmosDebugOverlayTickRate);
if (AccumulatedFrameTime < _updateCooldown)
{
return;
}
// This is the timer from GasTileOverlaySystem
AccumulatedFrameTime -= _updateCooldown;
var currentTick = _gameTiming.CurTick;
// Now we'll go through each player, then through each chunk in range of that player checking if the player is still in range
// If they are, check if they need the new data to send (i.e. if there's an overlay for the gas).
// Afterwards we reset all the chunk data for the next time we tick.
foreach (var session in _playerObservers)
{
if (session.AttachedEntity == null) continue;
var entity = session.AttachedEntity;
var worldBounds = Box2.CenteredAround(entity.Transform.WorldPosition,
new Vector2(LocalViewRange, LocalViewRange));
foreach (var grid in _mapManager.FindGridsIntersecting(entity.Transform.MapID, worldBounds))
{
if (!EntityManager.TryGetEntity(grid.GridEntityId, out var gridEnt)) continue;
if (!gridEnt.TryGetComponent<GridAtmosphereComponent>(out var gam)) continue;
var entityTile = grid.GetTileRef(entity.Transform.Coordinates).GridIndices;
var baseTile = new Vector2i(entityTile.X - (LocalViewRange / 2), entityTile.Y - (LocalViewRange / 2));
var debugOverlayContent = new AtmosDebugOverlayData[LocalViewRange * LocalViewRange];
var index = 0;
for (var y = 0; y < LocalViewRange; y++)
{
for (var x = 0; x < LocalViewRange; x++)
{
var Vector2i = new Vector2i(baseTile.X + x, baseTile.Y + y);
debugOverlayContent[index++] = ConvertTileToData(gam.GetTile(Vector2i));
}
}
RaiseNetworkEvent(new AtmosDebugOverlayMessage(grid.Index, baseTile, debugOverlayContent), session.ConnectedClient);
}
}
}
}
}

View File

@@ -1,491 +0,0 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Content.Server.GameObjects.Components.Atmos;
using Content.Shared;
using Content.Shared.Atmos;
using Content.Shared.CCVar;
using Content.Shared.GameObjects.EntitySystems.Atmos;
using Content.Shared.GameTicking;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
// ReSharper disable once RedundantUsingDirective
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Dependency = Robust.Shared.IoC.DependencyAttribute;
namespace Content.Server.GameObjects.EntitySystems.Atmos
{
[UsedImplicitly]
internal sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem, IResettingEntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
/// <summary>
/// The tiles that have had their atmos data updated since last tick
/// </summary>
private readonly Dictionary<GridId, HashSet<Vector2i>> _invalidTiles = new();
private readonly Dictionary<IPlayerSession, PlayerGasOverlay> _knownPlayerChunks =
new();
/// <summary>
/// Gas data stored in chunks to make PVS / bubbling easier.
/// </summary>
private readonly Dictionary<GridId, Dictionary<Vector2i, GasOverlayChunk>> _overlay =
new();
/// <summary>
/// How far away do we update gas overlays (minimum; due to chunking further away tiles may also be updated).
/// </summary>
private float _updateRange;
// Because the gas overlay updates aren't run every tick we need to avoid the pop-in that might occur with
// the regular PVS range.
private const float RangeOffset = 6.0f;
/// <summary>
/// Overlay update ticks per second.
/// </summary>
private float _updateCooldown;
private AtmosphereSystem _atmosphereSystem = default!;
public override void Initialize()
{
base.Initialize();
_atmosphereSystem = Get<AtmosphereSystem>();
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
_mapManager.OnGridRemoved += OnGridRemoved;
}
public override void Shutdown()
{
base.Shutdown();
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
_mapManager.OnGridRemoved -= OnGridRemoved;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Invalidate(GridId gridIndex, Vector2i indices)
{
if (!_invalidTiles.TryGetValue(gridIndex, out var existing))
{
existing = new HashSet<Vector2i>();
_invalidTiles[gridIndex] = existing;
}
existing.Add(indices);
}
private GasOverlayChunk GetOrCreateChunk(GridId gridIndex, Vector2i indices)
{
if (!_overlay.TryGetValue(gridIndex, out var chunks))
{
chunks = new Dictionary<Vector2i, GasOverlayChunk>();
_overlay[gridIndex] = chunks;
}
var chunkIndices = GetGasChunkIndices(indices);
if (!chunks.TryGetValue(chunkIndices, out var chunk))
{
chunk = new GasOverlayChunk(gridIndex, chunkIndices);
chunks[chunkIndices] = chunk;
}
return chunk;
}
private void OnGridRemoved(MapId mapId, GridId gridId)
{
if (_overlay.ContainsKey(gridId))
{
_overlay.Remove(gridId);
}
}
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.NewStatus != SessionStatus.InGame)
{
if (_knownPlayerChunks.ContainsKey(e.Session))
{
_knownPlayerChunks.Remove(e.Session);
}
return;
}
if (!_knownPlayerChunks.ContainsKey(e.Session))
{
_knownPlayerChunks[e.Session] = new PlayerGasOverlay();
}
}
/// <summary>
/// Checks whether the overlay-relevant data for a gas tile has been updated.
/// </summary>
/// <param name="gam"></param>
/// <param name="oldTile"></param>
/// <param name="indices"></param>
/// <param name="overlayData"></param>
/// <returns>true if updated</returns>
private bool TryRefreshTile(GridAtmosphereComponent gam, GasOverlayData oldTile, Vector2i indices, out GasOverlayData overlayData)
{
var tile = gam.GetTile(indices);
if (tile == null)
{
overlayData = default;
return false;
}
var tileData = new List<GasData>();
if(tile.Air != null)
for (byte i = 0; i < Atmospherics.TotalNumberOfGases; i++)
{
var gas = _atmosphereSystem.GetGas(i);
var overlay = _atmosphereSystem.GetOverlay(i);
if (overlay == null) continue;
var moles = tile.Air.Gases[i];
if (moles < gas.GasMolesVisible) continue;
var data = new GasData(i, (byte) (MathHelper.Clamp01(moles / gas.GasMolesVisibleMax) * 255));
tileData.Add(data);
}
overlayData = new GasOverlayData(tile!.Hotspot.State, tile.Hotspot.Temperature, tileData.Count == 0 ? Array.Empty<GasData>() : tileData.ToArray());
if (overlayData.Equals(oldTile))
{
return false;
}
return true;
}
/// <summary>
/// Get every chunk in range of our entity that exists, including on other grids.
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
private List<GasOverlayChunk> GetChunksInRange(IEntity entity)
{
var inRange = new List<GasOverlayChunk>();
// This is the max in any direction that we can get a chunk (e.g. max 2 chunks away of data).
var (maxXDiff, maxYDiff) = ((int) (_updateRange / ChunkSize) + 1, (int) (_updateRange / ChunkSize) + 1);
var worldBounds = Box2.CenteredAround(entity.Transform.WorldPosition,
new Vector2(_updateRange, _updateRange));
foreach (var grid in _mapManager.FindGridsIntersecting(entity.Transform.MapID, worldBounds))
{
if (!_overlay.TryGetValue(grid.Index, out var chunks))
{
continue;
}
var entityTile = grid.GetTileRef(entity.Transform.Coordinates).GridIndices;
for (var x = -maxXDiff; x <= maxXDiff; x++)
{
for (var y = -maxYDiff; y <= maxYDiff; y++)
{
var chunkIndices = GetGasChunkIndices(new Vector2i(entityTile.X + x * ChunkSize, entityTile.Y + y * ChunkSize));
if (!chunks.TryGetValue(chunkIndices, out var chunk)) continue;
// Now we'll check if it's in range and relevant for us
// (e.g. if we're on the very edge of a chunk we may need more chunks).
var (xDiff, yDiff) = (chunkIndices.X - entityTile.X, chunkIndices.Y - entityTile.Y);
if (xDiff > 0 && xDiff > _updateRange ||
yDiff > 0 && yDiff > _updateRange ||
xDiff < 0 && Math.Abs(xDiff + ChunkSize) > _updateRange ||
yDiff < 0 && Math.Abs(yDiff + ChunkSize) > _updateRange) continue;
inRange.Add(chunk);
}
}
}
return inRange;
}
public override void Update(float frameTime)
{
AccumulatedFrameTime += frameTime;
_updateCooldown = 1 / _configManager.GetCVar(CCVars.NetGasOverlayTickRate);
if (AccumulatedFrameTime < _updateCooldown)
{
return;
}
_updateRange = _configManager.GetCVar(CVars.NetMaxUpdateRange) + RangeOffset;
// TODO: So in the worst case scenario we still have to send a LOT of tile data per tick if there's a fire.
// If we go with say 15 tile radius then we have up to 900 tiles to update per tick.
// In a saltern fire the worst you'll normally see is around 650 at the moment.
// Need a way to fake this more because sending almost 2,000 tile updates per second to even 50 players is... yikes
// I mean that's as big as it gets so larger maps will have the same but still, that's a lot of data.
// Some ways to do this are potentially: splitting fire and gas update data so they don't update at the same time
// (gives the illusion of more updates happening), e.g. if gas updates are 3 times a second and fires are 1.6 times a second or something.
// Could also look at updating tiles close to us more frequently (e.g. within 1 chunk every tick).
// Stuff just out of our viewport we need so when we move it doesn't pop in but it doesn't mean we need to update it every tick.
AccumulatedFrameTime -= _updateCooldown;
var gridAtmosComponents = new Dictionary<GridId, GridAtmosphereComponent>();
var updatedTiles = new Dictionary<GasOverlayChunk, HashSet<Vector2i>>();
// So up to this point we've been caching the updated tiles for multiple ticks.
// Now we'll go through and check whether the update actually matters for the overlay or not,
// and if not then we won't bother sending the data.
foreach (var (gridId, indices) in _invalidTiles)
{
if (!_mapManager.TryGetGrid(gridId, out var grid))
{
return;
}
var gridEntityId = grid.GridEntityId;
if (!EntityManager.GetEntity(gridEntityId).TryGetComponent(out GridAtmosphereComponent? gam))
{
continue;
}
// If it's being invalidated it should have this right?
// At any rate we'll cache it for here + the AddChunk
if (!gridAtmosComponents.ContainsKey(gridId))
{
gridAtmosComponents[gridId] = gam;
}
foreach (var invalid in indices.ToArray())
{
var chunk = GetOrCreateChunk(gridId, invalid);
if (!TryRefreshTile(gam, chunk.GetData(invalid), invalid, out var data)) continue;
if (!updatedTiles.TryGetValue(chunk, out var tiles))
{
tiles = new HashSet<Vector2i>();
updatedTiles[chunk] = tiles;
}
updatedTiles[chunk].Add(invalid);
chunk.Update(data, invalid);
}
}
var currentTick = _gameTiming.CurTick;
// Set the LastUpdate for chunks.
foreach (var (chunk, _) in updatedTiles)
{
chunk.Dirty(currentTick);
}
// Now we'll go through each player, then through each chunk in range of that player checking if the player is still in range
// If they are, check if they need the new data to send (i.e. if there's an overlay for the gas).
// Afterwards we reset all the chunk data for the next time we tick.
foreach (var (session, overlay) in _knownPlayerChunks)
{
if (session.AttachedEntity == null) continue;
// Get chunks in range and update if we've moved around or the chunks have new overlay data
var chunksInRange = GetChunksInRange(session.AttachedEntity);
var knownChunks = overlay.GetKnownChunks();
var chunksToRemove = new List<GasOverlayChunk>();
var chunksToAdd = new List<GasOverlayChunk>();
foreach (var chunk in chunksInRange)
{
if (!knownChunks.Contains(chunk))
{
chunksToAdd.Add(chunk);
}
}
foreach (var chunk in knownChunks)
{
if (!chunksInRange.Contains(chunk))
{
chunksToRemove.Add(chunk);
}
}
foreach (var chunk in chunksToAdd)
{
var message = overlay.AddChunk(currentTick, chunk);
if (message != null)
{
RaiseNetworkEvent(message, session.ConnectedClient);
}
}
foreach (var chunk in chunksToRemove)
{
overlay.RemoveChunk(chunk);
}
var clientInvalids = new Dictionary<GridId, List<(Vector2i, GasOverlayData)>>();
// Check for any dirty chunks in range and bundle the data to send to the client.
foreach (var chunk in chunksInRange)
{
if (!updatedTiles.TryGetValue(chunk, out var invalids)) continue;
if (!clientInvalids.TryGetValue(chunk.GridIndices, out var existingData))
{
existingData = new List<(Vector2i, GasOverlayData)>();
clientInvalids[chunk.GridIndices] = existingData;
}
chunk.GetData(existingData, invalids);
}
foreach (var (grid, data) in clientInvalids)
{
RaiseNetworkEvent(overlay.UpdateClient(grid, data), session.ConnectedClient);
}
}
// Cleanup
_invalidTiles.Clear();
}
private sealed class PlayerGasOverlay
{
private readonly Dictionary<GridId, Dictionary<Vector2i, GasOverlayChunk>> _data =
new();
private readonly Dictionary<GasOverlayChunk, GameTick> _lastSent =
new();
public GasOverlayMessage UpdateClient(GridId grid, List<(Vector2i, GasOverlayData)> data)
{
return new(grid, data);
}
public void Reset()
{
_data.Clear();
_lastSent.Clear();
}
public List<GasOverlayChunk> GetKnownChunks()
{
var known = new List<GasOverlayChunk>();
foreach (var (_, chunks) in _data)
{
foreach (var (_, chunk) in chunks)
{
known.Add(chunk);
}
}
return known;
}
public GasOverlayMessage? AddChunk(GameTick currentTick, GasOverlayChunk chunk)
{
if (!_data.TryGetValue(chunk.GridIndices, out var chunks))
{
chunks = new Dictionary<Vector2i, GasOverlayChunk>();
_data[chunk.GridIndices] = chunks;
}
if (_lastSent.TryGetValue(chunk, out var last) && last >= chunk.LastUpdate)
{
return null;
}
_lastSent[chunk] = currentTick;
var message = ChunkToMessage(chunk);
return message;
}
public void RemoveChunk(GasOverlayChunk chunk)
{
// Don't need to sync to client as they can manage it themself.
if (!_data.TryGetValue(chunk.GridIndices, out var chunks))
{
return;
}
if (chunks.ContainsKey(chunk.Vector2i))
{
chunks.Remove(chunk.Vector2i);
}
}
/// <summary>
/// Retrieve a whole chunk as a message, only getting the relevant tiles for the gas overlay.
/// </summary>
/// <param name="chunk"></param>
/// <returns></returns>
private GasOverlayMessage? ChunkToMessage(GasOverlayChunk chunk)
{
// Chunk data should already be up to date.
// Only send relevant tiles to client.
var tileData = new List<(Vector2i, GasOverlayData)>();
for (var x = 0; x < ChunkSize; x++)
{
for (var y = 0; y < ChunkSize; y++)
{
// TODO: Check could be more robust I think.
var data = chunk.TileData[x, y];
if ((data.Gas == null || data.Gas.Length == 0) && data.FireState == 0 && data.FireTemperature == 0.0f)
{
continue;
}
var indices = new Vector2i(chunk.Vector2i.X + x, chunk.Vector2i.Y + y);
tileData.Add((indices, data));
}
}
if (tileData.Count == 0)
{
return null;
}
return new GasOverlayMessage(chunk.GridIndices, tileData);
}
}
public void Reset()
{
_invalidTiles.Clear();
_overlay.Clear();
foreach (var (_, data) in _knownPlayerChunks)
{
data.Reset();
}
}
}
}

View File

@@ -1,32 +0,0 @@
using Content.Server.Atmos;
using Content.Server.GameObjects.Components.Atmos;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class AtmosExposedSystem
: EntitySystem
{
private const float UpdateDelay = 1f;
private float _lastUpdate;
public override void Update(float frameTime)
{
_lastUpdate += frameTime;
if (_lastUpdate < UpdateDelay) return;
// creadth: everything exposable by atmos should be updated as well
foreach (var atmosExposedComponent in EntityManager.ComponentManager.EntityQuery<AtmosExposedComponent>(true))
{
var tile = atmosExposedComponent.Owner.Transform.Coordinates.GetTileAtmosphere();
if (tile == null) continue;
atmosExposedComponent.Update(tile, _lastUpdate);
}
_lastUpdate = 0;
}
}
}

View File

@@ -1,205 +0,0 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Server.Atmos;
using Content.Server.Atmos.Reactions;
using Content.Server.GameObjects.Components.Atmos;
using Content.Shared;
using Content.Shared.Atmos;
using Content.Shared.CCVar;
using Content.Shared.GameObjects.EntitySystems.Atmos;
using Content.Shared.Maps;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class AtmosphereSystem : SharedAtmosphereSystem
{
[Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPauseManager _pauseManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private GasReactionPrototype[] _gasReactions = Array.Empty<GasReactionPrototype>();
private GridTileLookupSystem? _gridTileLookup = null;
/// <summary>
/// List of gas reactions ordered by priority.
/// </summary>
public IEnumerable<GasReactionPrototype> GasReactions => _gasReactions!;
private float[] _gasSpecificHeats = new float[Atmospherics.TotalNumberOfGases];
public float[] GasSpecificHeats => _gasSpecificHeats;
public GridTileLookupSystem GridTileLookupSystem => _gridTileLookup ??= Get<GridTileLookupSystem>();
public override void Initialize()
{
base.Initialize();
_gasReactions = _protoMan.EnumeratePrototypes<GasReactionPrototype>().ToArray();
Array.Sort(_gasReactions, (a, b) => b.Priority.CompareTo(a.Priority));
_mapManager.MapCreated += OnMapCreated;
_mapManager.TileChanged += OnTileChanged;
Array.Resize(ref _gasSpecificHeats, MathHelper.NextMultipleOf(Atmospherics.TotalNumberOfGases, 4));
for (var i = 0; i < GasPrototypes.Length; i++)
{
_gasSpecificHeats[i] = GasPrototypes[i].SpecificHeat;
}
// Required for airtight components.
SubscribeLocalEvent<RotateEvent>(RotateEvent);
SubscribeLocalEvent<AirtightComponent, SnapGridPositionChangedEvent>(HandleSnapGridMove);
_cfg.OnValueChanged(CCVars.SpaceWind, OnSpaceWindChanged, true);
_cfg.OnValueChanged(CCVars.MonstermosEqualization, OnMonstermosEqualizationChanged, true);
_cfg.OnValueChanged(CCVars.Superconduction, OnSuperconductionChanged, true);
_cfg.OnValueChanged(CCVars.AtmosMaxProcessTime, OnAtmosMaxProcessTimeChanged, true);
_cfg.OnValueChanged(CCVars.AtmosTickRate, OnAtmosTickRateChanged, true);
_cfg.OnValueChanged(CCVars.ExcitedGroupsSpaceIsAllConsuming, OnExcitedGroupsSpaceIsAllConsumingChanged, true);
}
private static void HandleSnapGridMove(EntityUid uid, AirtightComponent component, SnapGridPositionChangedEvent args)
{
component.OnTransformMove();
}
public bool SpaceWind { get; private set; }
public bool MonstermosEqualization { get; private set; }
public bool Superconduction { get; private set; }
public bool ExcitedGroupsSpaceIsAllConsuming { get; private set; }
public float AtmosMaxProcessTime { get; private set; }
public float AtmosTickRate { get; private set; }
private void OnExcitedGroupsSpaceIsAllConsumingChanged(bool obj)
{
ExcitedGroupsSpaceIsAllConsuming = obj;
}
private void OnAtmosTickRateChanged(float obj)
{
AtmosTickRate = obj;
}
private void OnAtmosMaxProcessTimeChanged(float obj)
{
AtmosMaxProcessTime = obj;
}
private void OnMonstermosEqualizationChanged(bool obj)
{
MonstermosEqualization = obj;
}
private void OnSuperconductionChanged(bool obj)
{
Superconduction = obj;
}
private void OnSpaceWindChanged(bool obj)
{
SpaceWind = obj;
}
public override void Shutdown()
{
base.Shutdown();
_mapManager.MapCreated -= OnMapCreated;
}
private void RotateEvent(RotateEvent ev)
{
if (ev.Sender.TryGetComponent(out AirtightComponent? airtight))
{
airtight.RotateEvent(ev);
}
}
public IGridAtmosphereComponent? GetGridAtmosphere(GridId gridId)
{
if (!gridId.IsValid())
return null;
if (!_mapManager.TryGetGrid(gridId, out var grid))
return null;
return ComponentManager.TryGetComponent(grid.GridEntityId, out IGridAtmosphereComponent? gridAtmosphere)
? gridAtmosphere : null;
}
public IGridAtmosphereComponent GetGridAtmosphere(EntityCoordinates coordinates)
{
return GetGridAtmosphere(coordinates.ToMap(EntityManager));
}
public IGridAtmosphereComponent GetGridAtmosphere(MapCoordinates coordinates)
{
if (coordinates.MapId == MapId.Nullspace)
{
throw new ArgumentException($"Coordinates cannot be in nullspace!", nameof(coordinates));
}
if (_mapManager.TryFindGridAt(coordinates, out var grid))
{
if (ComponentManager.TryGetComponent(grid.GridEntityId, out IGridAtmosphereComponent? atmos))
{
return atmos;
}
}
return _mapManager.GetMapEntity(coordinates.MapId).GetComponent<IGridAtmosphereComponent>();
}
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var (mapGridComponent, gridAtmosphereComponent) in EntityManager.ComponentManager.EntityQuery<IMapGridComponent, IGridAtmosphereComponent>(true))
{
if (_pauseManager.IsGridPaused(mapGridComponent.GridIndex)) continue;
gridAtmosphereComponent.Update(frameTime);
}
}
private void OnTileChanged(object? sender, TileChangedEventArgs eventArgs)
{
// When a tile changes, we want to update it only if it's gone from
// space -> not space or vice versa. So if the old tile is the
// same as the new tile in terms of space-ness, ignore the change
if (eventArgs.NewTile.IsSpace() == eventArgs.OldTile.IsSpace())
{
return;
}
GetGridAtmosphere(eventArgs.NewTile.GridPosition(_mapManager))?.Invalidate(eventArgs.NewTile.GridIndices);
}
private void OnMapCreated(object? sender, MapEventArgs e)
{
if (e.Map == MapId.Nullspace)
return;
var map = _mapManager.GetMapEntity(e.Map);
if (!map.HasComponent<IGridAtmosphereComponent>())
map.AddComponent<SpaceGridAtmosphereComponent>();
}
}
}

View File

@@ -1,27 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Content.Shared.Interaction;
using Content.Server.GameObjects.Components;
using Content.Shared.GameTicking;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using JetBrains.Annotations;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class ComputerUIActivatorSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BaseComputerUserInterfaceComponent, ActivateInWorldEvent>(HandleActivate);
}
private void HandleActivate(EntityUid uid, BaseComputerUserInterfaceComponent component, ActivateInWorldEvent args)
{
component.ActivateThunk(args);
}
}
}

View File

@@ -1,18 +0,0 @@
using Content.Server.GameObjects.Components.Atmos;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class GasAnalyzerSystem : EntitySystem
{
public override void Update(float frameTime)
{
foreach (var analyzer in ComponentManager.EntityQuery<GasAnalyzerComponent>(true))
{
analyzer.Update(frameTime);
}
}
}
}

View File

@@ -1,33 +0,0 @@
using Content.Server.GameObjects.Components.Atmos;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class GasCanisterSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GasCanisterComponent, PhysicsBodyTypeChangedEvent>(OnBodyTypeChanged);
}
private static void OnBodyTypeChanged(
EntityUid uid,
GasCanisterComponent component,
PhysicsBodyTypeChangedEvent args)
{
component.AnchorUpdate();
}
public override void Update(float frameTime)
{
foreach (var component in ComponentManager.EntityQuery<GasCanisterComponent>(true))
{
component.Update(frameTime);
}
}
}
}

View File

@@ -1,28 +0,0 @@
using Content.Server.GameObjects.Components.Atmos;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class GasTankSystem : EntitySystem
{
private float _timer = 0f;
private const float Interval = 0.5f;
public override void Update(float frameTime)
{
base.Update(frameTime);
_timer += frameTime;
if (_timer < Interval) return;
_timer = 0f;
foreach (var gasTank in EntityManager.ComponentManager.EntityQuery<GasTankComponent>(true))
{
gasTank.Update();
}
}
}
}