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

@@ -0,0 +1,171 @@
#nullable enable
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
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.Atmos.Components
{
[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);
UpdatePosition();
}
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;
}
public void MapInit()
{
UpdatePosition();
}
protected override void Shutdown()
{
base.Shutdown();
_airBlocked = false;
InvalidatePosition(_lastPosition.Item1, _lastPosition.Item2);
if (_fixVacuum)
{
_atmosphereSystem.GetGridAtmosphere(_lastPosition.Item1)?.FixVacuum(_lastPosition.Item2);
}
}
public void OnSnapGridMove(SnapGridPositionChangedEvent ev)
{
// Invalidate old position.
InvalidatePosition(ev.OldGrid, ev.OldPosition);
// Update and invalidate new position.
_lastPosition = (ev.NewGrid, ev.Position);
InvalidatePosition(ev.NewGrid, ev.Position);
}
private void UpdatePosition()
{
if (!Owner.Transform.Anchored || !Owner.Transform.GridID.IsValid())
return;
var grid = _mapManager.GetGrid(Owner.Transform.GridID);
_lastPosition = (Owner.Transform.GridID, grid.TileIndicesFor(Owner.Transform.Coordinates));
InvalidatePosition(_lastPosition.Item1, _lastPosition.Item2);
}
private void InvalidatePosition(GridId gridId, Vector2i pos)
{
if (!gridId.IsValid())
return;
var gridAtmos = _atmosphereSystem.GetGridAtmosphere(gridId);
gridAtmos?.UpdateAdjacentBits(pos);
gridAtmos?.Invalidate(pos);
}
}
}

View File

@@ -0,0 +1,44 @@
#nullable enable
using Content.Server.Temperature.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.ViewVariables;
namespace Content.Server.Atmos.Components
{
/// <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

@@ -0,0 +1,106 @@
using Content.Shared.Atmos.Visuals;
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.Atmos.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

@@ -0,0 +1,90 @@
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.Atmos.Components
{
/// <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

@@ -0,0 +1,138 @@
using Content.Server.Power.Components;
using Content.Server.UserInterface;
using Content.Shared.ActionBlocker;
using Content.Shared.Interaction;
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

@@ -0,0 +1,61 @@
#nullable enable
using Content.Server.Body.Respiratory;
using Content.Shared.Inventory;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Atmos.Components
{
/// <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

@@ -0,0 +1,117 @@
#nullable enable
using Content.Server.Atmos.EntitySystems;
using Content.Server.Doors;
using Content.Server.Doors.Components;
using Content.Shared.Doors;
using Content.Shared.Interaction;
using Content.Shared.Notification.Managers;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
namespace Content.Server.Atmos.Components
{
/// <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

@@ -0,0 +1,222 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.Alert;
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.Atmos.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Interaction;
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.Atmos.Components
{
[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

@@ -0,0 +1,287 @@
#nullable enable
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Hands.Components;
using Content.Server.UserInterface;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.DragDrop;
using Content.Shared.Interaction;
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.Atmos.Components
{
[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

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

View File

@@ -0,0 +1,408 @@
#nullable enable
#nullable disable warnings
using System;
using Content.Server.Body.Respiratory;
using Content.Server.Explosion;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.Interfaces;
using Content.Server.NodeContainer;
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.Atmos.Components;
using Content.Shared.Audio;
using Content.Shared.DragDrop;
using Content.Shared.Examine;
using Content.Shared.Interaction;
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.Atmos.Components
{
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
public class GasTankComponent : Component, IExamine, IGasMixtureHolder, IUse, IDropped, IActivate
{
public override string Name => "GasTank";
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;
[ViewVariables]
public GasMixture Air
{
// TODO ATMOS Kill it with fire.
get
{
if (!Owner.TryGetComponent(out NodeContainerComponent nodeContainer))
throw new InvalidOperationException("Can't get tank air without a node container!");
if (!nodeContainer.TryGetNode(TankName, out PipeNode? node))
throw new InvalidOperationException($"Node container doesn't have a pipenode called {TankName}!");
return node.Air;
}
set
{
// This will throw if the node container is not found.
var nodeContainer = Owner.GetComponent<NodeContainerComponent>();
if (!nodeContainer.TryGetNode(TankName, out PipeNode? node))
throw new InvalidOperationException($"Node container doesn't have a pipenode called {TankName}!");
node.Air = value;
}
}
[DataField("air")] [ViewVariables]
public GasMixture InitialMixture { 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;
/// <summary>
/// NodeContainer node.
/// </summary>
[DataField("tank")]
public string TankName { get; set; } = "tank";
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

@@ -0,0 +1,948 @@
#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.EntitySystems;
using Content.Server.Atmos.Piping.Components;
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.Atmos.Components
{
/// <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!;
[Dependency] private IGameTiming _gameTiming = 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;
public virtual bool Simulated => true;
[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<AtmosDeviceComponent> _atmosDevices = new();
[ViewVariables]
private double _atmosDevicesLastProcess;
[ViewVariables]
private Queue<TileAtmosphere> _currentRunTiles = new();
[ViewVariables]
private Queue<ExcitedGroup> _currentRunExcitedGroups = new();
[ViewVariables]
private Queue<IPipeNet> _currentRunPipeNet = new();
[ViewVariables]
private Queue<AtmosDeviceComponent> _currentRunAtmosDevices = new();
[ViewVariables]
private ProcessState _state = ProcessState.TileEqualize;
public GridAtmosphereComponent()
{
_paused = false;
}
private enum ProcessState
{
TileEqualize,
ActiveTiles,
ExcitedGroups,
HighPressureDelta,
Hotspots,
Superconductivity,
PipeNet,
AtmosDevices,
}
/// <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 AddAtmosDevice(AtmosDeviceComponent atmosDevice)
{
_atmosDevices.Add(atmosDevice);
}
public virtual void RemoveAtmosDevice(AtmosDeviceComponent atmosDevice)
{
_atmosDevices.Remove(atmosDevice);
}
/// <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.AtmosDevices;
break;
case ProcessState.AtmosDevices:
if (!ProcessAtmosDevices(_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 ProcessAtmosDevices(bool resumed = false, float lagCheck = 5f)
{
_stopwatch.Restart();
if(!resumed)
_currentRunAtmosDevices = new Queue<AtmosDeviceComponent>(_atmosDevices);
var time = _gameTiming.CurTime;
var updateEvent = new AtmosDeviceUpdateEvent(this);
var number = 0;
while (_currentRunAtmosDevices.Count > 0)
{
var device = _currentRunAtmosDevices.Dequeue();
Owner.EntityManager.EventBus.RaiseLocalEvent(device.Owner.Uid, updateEvent, false);
device.LastProcess = time;
if (number++ < LagCheckIterations) continue;
number = 0;
// Process the rest next time.
if (_stopwatch.Elapsed.TotalMilliseconds >= lagCheck)
{
_atmosDevicesLastProcess = _stopwatch.Elapsed.TotalMilliseconds;
return false;
}
}
_atmosDevicesLastProcess = _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

@@ -0,0 +1,184 @@
#nullable enable
using System.Collections.Generic;
using Content.Server.Atmos.Piping.Components;
using Content.Server.NodeContainer.NodeGroups;
using Content.Shared.Atmos;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Content.Server.Atmos.Components
{
public interface IGridAtmosphereComponent : IComponent, IEnumerable<TileAtmosphere>
{
/// <summary>
/// Whether this atmosphere is simulated or not.
/// </summary>
bool Simulated { get; }
/// <summary>
/// Number of times <see cref="Update"/> has been called.
/// </summary>
int UpdateCounter { get; }
/// <summary>
/// Control variable for equalization.
/// </summary>
long EqualizationQueueCycleControl { get; set; }
/// <summary>
/// Attemps to pry a tile.
/// </summary>
/// <param name="indices"></param>
void PryTile(Vector2i indices);
/// <summary>
/// Burns a tile.
/// </summary>
/// <param name="gridIndices"></param>
void BurnTile(Vector2i gridIndices);
/// <summary>
/// Invalidates a coordinate to be revalidated again.
/// Use this after changing a tile's gas contents, or when the tile becomes space, etc.
/// </summary>
/// <param name="indices"></param>
void Invalidate(Vector2i indices);
/// <summary>
/// Attempts to fix a sudden vacuum by creating gas.
/// </summary>
void FixVacuum(Vector2i indices);
/// <summary>
/// Revalidates indices immediately.
/// </summary>
/// <param name="indices"></param>
void UpdateAdjacentBits(Vector2i indices);
/// <summary>
/// Adds an active tile so it becomes processed every update until it becomes inactive.
/// Also makes the tile excited.
/// </summary>
/// <param name="tile"></param>
void AddActiveTile(TileAtmosphere tile);
/// <summary>
/// Removes an active tile and disposes of its <seealso cref="ExcitedGroup"/>.
/// Use with caution.
/// </summary>
/// <param name="tile"></param>
void RemoveActiveTile(TileAtmosphere tile, bool disposeGroup = true);
/// <summary>
/// Marks a tile as having a hotspot so it can be processed.
/// </summary>
/// <param name="tile"></param>
void AddHotspotTile(TileAtmosphere tile);
/// <summary>
/// Removes a tile from the hotspot processing list.
/// </summary>
/// <param name="tile"></param>
void RemoveHotspotTile(TileAtmosphere tile);
/// <summary>
/// Marks a tile as superconductive so it can be processed.
/// </summary>
/// <param name="tile"></param>
void AddSuperconductivityTile(TileAtmosphere tile);
/// <summary>
/// Removes a tile from the superconductivity processing list.
/// </summary>
/// <param name="tile"></param>
void RemoveSuperconductivityTile(TileAtmosphere tile);
/// <summary>
/// Marks a tile has having high pressure differences that need to be equalized.
/// </summary>
/// <param name="tile"></param>
void AddHighPressureDelta(TileAtmosphere tile);
/// <summary>
/// Returns whether the tile in question is marked as having high pressure differences or not.
/// </summary>
/// <param name="tile"></param>
/// <returns></returns>
bool HasHighPressureDelta(TileAtmosphere tile);
/// <summary>
/// Adds a excited group to be processed.
/// </summary>
/// <param name="excitedGroup"></param>
void AddExcitedGroup(ExcitedGroup excitedGroup);
/// <summary>
/// Removes an excited group.
/// </summary>
/// <param name="excitedGroup"></param>
void RemoveExcitedGroup(ExcitedGroup excitedGroup);
/// <summary>
/// Returns a tile.
/// </summary>
/// <param name="indices"></param>
/// <param name="createSpace"></param>
/// <returns></returns>
TileAtmosphere? GetTile(Vector2i indices, bool createSpace = true);
/// <summary>
/// Returns a tile.
/// </summary>
/// <param name="coordinates"></param>
/// <param name="createSpace"></param>
/// <returns></returns>
TileAtmosphere? GetTile(EntityCoordinates coordinates, bool createSpace = true);
/// <summary>
/// Returns if the tile in question is air-blocked.
/// This could be due to a wall, an airlock, etc.
/// <seealso cref="AirtightComponent"/>
/// </summary>
/// <param name="indices"></param>
/// <param name="direction"></param>
/// <returns></returns>
bool IsAirBlocked(Vector2i indices, AtmosDirection direction);
/// <summary>
/// Returns if the tile in question is space.
/// </summary>
/// <param name="indices"></param>
/// <returns></returns>
bool IsSpace(Vector2i indices);
/// <summary>
/// Returns the volume in liters for a number of cells/tiles.
/// </summary>
/// <param name="cellCount"></param>
/// <returns></returns>
float GetVolumeForCells(int cellCount);
void RepopulateTiles();
/// <summary>
/// Returns a dictionary of adjacent TileAtmospheres.
/// </summary>
Dictionary<AtmosDirection, TileAtmosphere> GetAdjacentTiles(EntityCoordinates coordinates, bool includeAirBlocked = false);
/// <summary>
/// Returns a dictionary of adjacent TileAtmospheres.
/// </summary>
Dictionary<AtmosDirection, TileAtmosphere> GetAdjacentTiles(Vector2i indices, bool includeAirBlocked = false);
void Update(float frameTime);
void AddPipeNet(IPipeNet pipeNet);
void RemovePipeNet(IPipeNet pipeNet);
void AddAtmosDevice(AtmosDeviceComponent atmosDevice);
void RemoveAtmosDevice(AtmosDeviceComponent atmosDevice);
}
}

View File

@@ -0,0 +1,128 @@
#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.Atmos.Components
{
[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

@@ -0,0 +1,21 @@
using Content.Server.Pressure;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.Atmos.Components
{
[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

@@ -0,0 +1,33 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Content.Server.Atmos.Components
{
[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

@@ -0,0 +1,113 @@
#nullable enable
using System;
using Content.Server.Atmos.Piping.Components;
using Content.Server.NodeContainer.NodeGroups;
using Content.Shared.Atmos;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
namespace Content.Server.Atmos.Components
{
[RegisterComponent]
[ComponentReference(typeof(IGridAtmosphereComponent))]
[ComponentReference(typeof(GridAtmosphereComponent))]
[Serializable]
public class UnsimulatedGridAtmosphereComponent : GridAtmosphereComponent, IGridAtmosphereComponent
{
public override string Name => "UnsimulatedGridAtmosphere";
public override bool Simulated => false;
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 AddAtmosDevice(AtmosDeviceComponent atmosDevice) { }
public override void RemoveAtmosDevice(AtmosDeviceComponent atmosDevice) { }
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 ProcessAtmosDevices(bool resumed = false, float lagCheck = 5f)
{
return false;
}
}
}