Firelocks and atmos optimizations (#2029)

* Some work

* unlit layers for firelock.rsi

* firelock stuff I guess

* changes dunno

* Support for non-fulltile firelocks!

* Fix TurfHelpers

* Replace GridCoordinates (ew) for EntityCoordinates (YAY)

* whoops

* Fix firelocks

* fix glass firelocks

* Big optimizations

* Optimize even further

* Support for non-fulltile airblockers rotating

* whoops.

* Adds edge firelocks

* Fix atmos bug with gasmixture serialization

* Redundant adjacent update

* ignored components

* Add gas mixture tests

* new test case for removeratio test

* Apply suggestions from code review

Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com>

* address all reviews

Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com>
This commit is contained in:
Víctor Aguilera Puerto
2020-09-12 20:20:19 +02:00
committed by GitHub
75 changed files with 1566 additions and 88 deletions

View File

@@ -2,6 +2,7 @@
using Content.Client.GameObjects.Components.Wires; using Content.Client.GameObjects.Components.Wires;
using Content.Shared.Audio; using Content.Shared.Audio;
using Content.Shared.GameObjects.Components.Doors; using Content.Shared.GameObjects.Components.Doors;
using JetBrains.Annotations;
using Robust.Client.Animations; using Robust.Client.Animations;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Client.GameObjects.Components.Animations; using Robust.Client.GameObjects.Components.Animations;
@@ -12,6 +13,7 @@ using YamlDotNet.RepresentationModel;
namespace Content.Client.GameObjects.Components.Doors namespace Content.Client.GameObjects.Components.Doors
{ {
[UsedImplicitly]
public class AirlockVisualizer : AppearanceVisualizer public class AirlockVisualizer : AppearanceVisualizer
{ {
private const string AnimationKey = "airlock_animation"; private const string AnimationKey = "airlock_animation";
@@ -24,11 +26,17 @@ namespace Content.Client.GameObjects.Components.Doors
{ {
base.LoadData(node); base.LoadData(node);
var delay = 0.8f;
var openSound = node.GetNode("open_sound").AsString(); var openSound = node.GetNode("open_sound").AsString();
var closeSound = node.GetNode("close_sound").AsString(); var closeSound = node.GetNode("close_sound").AsString();
var denySound = node.GetNode("deny_sound").AsString(); var denySound = node.GetNode("deny_sound").AsString();
if (node.TryGetNode("animation_time", out var yamlNode))
{
delay = yamlNode.AsFloat();
}
CloseAnimation = new Animation {Length = TimeSpan.FromSeconds(0.8f)}; CloseAnimation = new Animation {Length = TimeSpan.FromSeconds(delay)};
{ {
var flick = new AnimationTrackSpriteFlick(); var flick = new AnimationTrackSpriteFlick();
CloseAnimation.AnimationTracks.Add(flick); CloseAnimation.AnimationTracks.Add(flick);
@@ -50,7 +58,7 @@ namespace Content.Client.GameObjects.Components.Doors
sound.KeyFrames.Add(new AnimationTrackPlaySound.KeyFrame(closeSound, 0)); sound.KeyFrames.Add(new AnimationTrackPlaySound.KeyFrame(closeSound, 0));
} }
OpenAnimation = new Animation {Length = TimeSpan.FromSeconds(0.8f)}; OpenAnimation = new Animation {Length = TimeSpan.FromSeconds(delay)};
{ {
var flick = new AnimationTrackSpriteFlick(); var flick = new AnimationTrackSpriteFlick();
OpenAnimation.AnimationTracks.Add(flick); OpenAnimation.AnimationTracks.Add(flick);

View File

@@ -176,6 +176,7 @@
"ExtinguisherCabinet", "ExtinguisherCabinet",
"ExtinguisherCabinetFilled", "ExtinguisherCabinetFilled",
"FireExtinguisher", "FireExtinguisher",
"Firelock",
"AtmosPlaque", "AtmosPlaque",
"Spillable", "Spillable",
}; };

View File

@@ -0,0 +1,85 @@
using System.Threading.Tasks;
using Content.Server.Atmos;
using Content.Shared.Atmos;
using NUnit.Framework;
namespace Content.IntegrationTests.Tests.Atmos
{
[TestFixture]
[TestOf(typeof(GasMixture))]
public class GasMixtureTest : ContentIntegrationTest
{
[Test]
public async Task TestMerge()
{
var server = StartServerDummyTicker();
server.Assert(() =>
{
var a = new GasMixture(10f);
var b = new GasMixture(10f);
a.AdjustMoles(Gas.Oxygen, 50);
b.AdjustMoles(Gas.Nitrogen, 50);
// a now has 50 moles of oxygen
Assert.That(a.TotalMoles, Is.EqualTo(50));
Assert.That(a.GetMoles(Gas.Oxygen), Is.EqualTo(50));
// b now has 50 moles of nitrogen
Assert.That(b.TotalMoles, Is.EqualTo(50));
Assert.That(b.GetMoles(Gas.Nitrogen), Is.EqualTo(50));
b.Merge(a);
// b now has its contents and the contents of a
Assert.That(b.TotalMoles, Is.EqualTo(100));
Assert.That(b.GetMoles(Gas.Oxygen), Is.EqualTo(50));
Assert.That(b.GetMoles(Gas.Nitrogen), Is.EqualTo(50));
// a should be the same, however.
Assert.That(a.TotalMoles, Is.EqualTo(50));
Assert.That(a.GetMoles(Gas.Oxygen), Is.EqualTo(50));
});
await server.WaitIdleAsync();
}
[Test]
[TestCase(0.5f)]
[TestCase(0.25f)]
[TestCase(0.75f)]
[TestCase(1f)]
[TestCase(0f)]
[TestCase(Atmospherics.BreathPercentage)]
public async Task RemoveRatio(float ratio)
{
var server = StartServerDummyTicker();
server.Assert(() =>
{
var a = new GasMixture(10f);
a.AdjustMoles(Gas.Oxygen, 100);
a.AdjustMoles(Gas.Nitrogen, 100);
var origTotal = a.TotalMoles;
// we remove moles from the mixture with a ratio.
var b = a.RemoveRatio(ratio);
// check that the amount of moles in the original and the new mixture are correct.
Assert.That(b.TotalMoles, Is.EqualTo(origTotal * ratio));
Assert.That(a.TotalMoles, Is.EqualTo(origTotal - b.TotalMoles));
Assert.That(b.GetMoles(Gas.Oxygen), Is.EqualTo(100 * ratio));
Assert.That(b.GetMoles(Gas.Nitrogen), Is.EqualTo(100 * ratio));
Assert.That(a.GetMoles(Gas.Oxygen), Is.EqualTo(100 - b.GetMoles(Gas.Oxygen)));
Assert.That(a.GetMoles(Gas.Nitrogen), Is.EqualTo(100 - b.GetMoles(Gas.Nitrogen)));
});
await server.WaitIdleAsync();
}
}
}

View File

@@ -545,6 +545,10 @@ namespace Content.Server.Atmos
serializer.DataField(ref _moles, "moles", new float[Atmospherics.TotalNumberOfGases]); serializer.DataField(ref _moles, "moles", new float[Atmospherics.TotalNumberOfGases]);
serializer.DataField(ref _molesArchived, "molesArchived", new float[Atmospherics.TotalNumberOfGases]); serializer.DataField(ref _molesArchived, "molesArchived", new float[Atmospherics.TotalNumberOfGases]);
serializer.DataField(ref _temperature, "temperature", Atmospherics.TCMB); serializer.DataField(ref _temperature, "temperature", Atmospherics.TCMB);
// The arrays MUST have a specific length.
Array.Resize(ref _moles, Atmospherics.TotalNumberOfGases);
Array.Resize(ref _molesArchived, Atmospherics.TotalNumberOfGases);
} }
public override bool Equals(object? obj) public override bool Equals(object? obj)

View File

@@ -1,8 +1,11 @@
using System.Collections.Generic; using System.Collections.Generic;
using Content.Server.GameObjects.Components.Atmos;
using Content.Server.GameObjects.Components.Atmos.Piping; using Content.Server.GameObjects.Components.Atmos.Piping;
using Content.Server.GameObjects.Components.NodeContainer.NodeGroups; using Content.Server.GameObjects.Components.NodeContainer.NodeGroups;
using Content.Shared.Atmos;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Content.Server.Atmos namespace Content.Server.Atmos
{ {
@@ -42,6 +45,12 @@ namespace Content.Server.Atmos
/// </summary> /// </summary>
void FixVacuum(MapIndices indices); void FixVacuum(MapIndices indices);
/// <summary>
/// Revalidates indices immediately.
/// </summary>
/// <param name="indices"></param>
void UpdateAdjacentBits(MapIndices indices);
/// <summary> /// <summary>
/// Adds an active tile so it becomes processed every update until it becomes inactive. /// Adds an active tile so it becomes processed every update until it becomes inactive.
/// Also makes the tile excited. /// Also makes the tile excited.
@@ -109,6 +118,7 @@ namespace Content.Server.Atmos
/// Returns a tile. /// Returns a tile.
/// </summary> /// </summary>
/// <param name="indices"></param> /// <param name="indices"></param>
/// <param name="createSpace"></param>
/// <returns></returns> /// <returns></returns>
TileAtmosphere GetTile(MapIndices indices, bool createSpace = true); TileAtmosphere GetTile(MapIndices indices, bool createSpace = true);
@@ -116,17 +126,19 @@ namespace Content.Server.Atmos
/// Returns a tile. /// Returns a tile.
/// </summary> /// </summary>
/// <param name="coordinates"></param> /// <param name="coordinates"></param>
/// <param name="createSpace"></param>
/// <returns></returns> /// <returns></returns>
TileAtmosphere GetTile(EntityCoordinates coordinates, bool createSpace = true); TileAtmosphere GetTile(EntityCoordinates coordinates, bool createSpace = true);
/// <summary> /// <summary>
/// Returns if the tile in question is air-blocked. /// Returns if the tile in question is air-blocked.
/// This could be due to a wall, an airlock, etc. /// This could be due to a wall, an airlock, etc.
/// Also see AirtightComponent. /// <seealso cref="AirtightComponent"/>
/// </summary> /// </summary>
/// <param name="indices"></param> /// <param name="indices"></param>
/// <param name="direction"></param>
/// <returns></returns> /// <returns></returns>
bool IsAirBlocked(MapIndices indices); bool IsAirBlocked(MapIndices indices, AtmosDirection direction);
/// <summary> /// <summary>
/// Returns if the tile in question is space. /// Returns if the tile in question is space.
@@ -142,6 +154,11 @@ namespace Content.Server.Atmos
/// <returns></returns> /// <returns></returns>
float GetVolumeForCells(int cellCount); float GetVolumeForCells(int cellCount);
/// <summary>
/// Returns a dictionary of adjacent TileAtmospheres.
/// </summary>
Dictionary<AtmosDirection, TileAtmosphere> GetAdjacentTiles(MapIndices indices, bool includeAirBlocked = false);
void Update(float frameTime); void Update(float frameTime);
void AddPipeNet(IPipeNet pipeNet); void AddPipeNet(IPipeNet pipeNet);

View File

@@ -6,6 +6,7 @@ using Content.Server.Atmos.Reactions;
using Content.Server.GameObjects.Components.Atmos; using Content.Server.GameObjects.Components.Atmos;
using Content.Server.GameObjects.EntitySystems.Atmos; using Content.Server.GameObjects.EntitySystems.Atmos;
using Content.Server.Interfaces; using Content.Server.Interfaces;
using Content.Server.Utility;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Audio; using Content.Shared.Audio;
using Content.Shared.Maps; using Content.Shared.Maps;
@@ -66,7 +67,7 @@ namespace Content.Server.Atmos
public float HeatCapacity { get; set; } = 1f; public float HeatCapacity { get; set; } = 1f;
[ViewVariables] [ViewVariables]
public float ThermalConductivity => Tile?.Tile.GetContentTileDefinition().ThermalConductivity ?? 0.05f; public float ThermalConductivity { get; set; } = 0.05f;
[ViewVariables] [ViewVariables]
public bool Excited { get; set; } public bool Excited { get; set; }
@@ -111,8 +112,13 @@ namespace Content.Server.Atmos
[ViewVariables] [ViewVariables]
public GasMixture Air { get; set; } public GasMixture Air { get; set; }
[ViewVariables, UsedImplicitly]
private int _blockedAirflow => (int)BlockedAirflow;
public AtmosDirection BlockedAirflow { get; set; } = AtmosDirection.Invalid;
[ViewVariables] [ViewVariables]
public bool BlocksAir => _gridAtmosphereComponent.IsAirBlocked(GridIndices); public bool BlocksAllAir => BlockedAirflow == AtmosDirection.All;
public TileAtmosphere(GridAtmosphereComponent atmosphereComponent, GridId gridIndex, MapIndices gridIndices, GasMixture mixture = null, bool immutable = false) public TileAtmosphere(GridAtmosphereComponent atmosphereComponent, GridId gridIndex, MapIndices gridIndices, GasMixture mixture = null, bool immutable = false)
{ {
@@ -867,12 +873,12 @@ namespace Content.Server.Atmos
private void FinishSuperconduction() private void FinishSuperconduction()
{ {
// Conduct with air on my tile if I have it // Conduct with air on my tile if I have it
if (!BlocksAir) if (!BlocksAllAir)
{ {
Temperature = Air.TemperatureShare(ThermalConductivity, Temperature, HeatCapacity); Temperature = Air.TemperatureShare(ThermalConductivity, Temperature, HeatCapacity);
} }
FinishSuperconduction(BlocksAir ? Temperature : Air.Temperature); FinishSuperconduction(BlocksAllAir ? Temperature : Air.Temperature);
} }
private void FinishSuperconduction(float temperature) private void FinishSuperconduction(float temperature)
@@ -886,9 +892,9 @@ namespace Content.Server.Atmos
private void NeighborConductWithSource(TileAtmosphere other) private void NeighborConductWithSource(TileAtmosphere other)
{ {
if (BlocksAir) if (BlocksAllAir)
{ {
if (!other.BlocksAir) if (!other.BlocksAllAir)
{ {
other.TemperatureShareOpenToSolid(this); other.TemperatureShareOpenToSolid(this);
} }
@@ -901,7 +907,7 @@ namespace Content.Server.Atmos
return; return;
} }
if (!other.BlocksAir) if (!other.BlocksAllAir)
{ {
other.Air.TemperatureShare(Air, Atmospherics.WindowHeatTransferCoefficient); other.Air.TemperatureShare(Air, Atmospherics.WindowHeatTransferCoefficient);
} }
@@ -952,7 +958,7 @@ namespace Content.Server.Atmos
public AtmosDirection ConductivityDirections() public AtmosDirection ConductivityDirections()
{ {
if(BlocksAir) if(BlocksAllAir)
{ {
if(_archivedCycle < _gridAtmosphereComponent.UpdateCounter) if(_archivedCycle < _gridAtmosphereComponent.UpdateCounter)
Archive(_gridAtmosphereComponent.UpdateCounter); Archive(_gridAtmosphereComponent.UpdateCounter);
@@ -1087,7 +1093,25 @@ namespace Content.Server.Atmos
private void ConsiderFirelocks(TileAtmosphere other) private void ConsiderFirelocks(TileAtmosphere other)
{ {
// TODO ATMOS firelocks! var reconsiderAdjacent = false;
foreach (var entity in GridIndices.GetEntitiesInTileFast(GridIndex, _gridAtmosphereComponent.GridTileLookupSystem))
{
if (!entity.TryGetComponent(out FirelockComponent firelock)) continue;
reconsiderAdjacent |= firelock.EmergencyPressureStop();
}
foreach (var entity in other.GridIndices.GetEntitiesInTileFast(other.GridIndex, _gridAtmosphereComponent.GridTileLookupSystem))
{
if (!entity.TryGetComponent(out FirelockComponent firelock)) continue;
reconsiderAdjacent |= firelock.EmergencyPressureStop();
}
if (reconsiderAdjacent)
{
UpdateAdjacent();
other.UpdateAdjacent();
}
} }
private void React() private void React()
@@ -1130,7 +1154,7 @@ namespace Content.Server.Atmos
_adjacentTiles[direction.ToIndex()] = adjacent; _adjacentTiles[direction.ToIndex()] = adjacent;
adjacent?.UpdateAdjacent(direction.GetOpposite()); adjacent?.UpdateAdjacent(direction.GetOpposite());
if (adjacent != null && !_gridAtmosphereComponent.IsAirBlocked(adjacent.GridIndices)) if (adjacent != null && !BlockedAirflow.HasFlag(direction) && !_gridAtmosphereComponent.IsAirBlocked(adjacent.GridIndices, direction.GetOpposite()))
{ {
_adjacentBits |= direction; _adjacentBits |= direction;
} }
@@ -1138,10 +1162,16 @@ namespace Content.Server.Atmos
} }
public void UpdateAdjacent(AtmosDirection direction) public void UpdateAdjacent(AtmosDirection direction)
{
if (!_gridAtmosphereComponent.IsAirBlocked(GridIndices.Offset(direction.ToDirection())))
{ {
_adjacentTiles[direction.ToIndex()] = _gridAtmosphereComponent.GetTile(GridIndices.Offset(direction.ToDirection())); _adjacentTiles[direction.ToIndex()] = _gridAtmosphereComponent.GetTile(GridIndices.Offset(direction.ToDirection()));
if (!BlockedAirflow.HasFlag(direction) && !_gridAtmosphereComponent.IsAirBlocked(GridIndices.Offset(direction.ToDirection()), direction.GetOpposite()))
{
_adjacentBits |= direction;
}
else
{
_adjacentBits &= ~direction;
} }
} }

View File

@@ -1,5 +1,7 @@
#nullable enable #nullable enable
using System;
using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Atmos;
using Robust.Server.Interfaces.GameObjects; using Robust.Server.Interfaces.GameObjects;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components.Transform; using Robust.Shared.GameObjects.Components.Transform;
@@ -9,6 +11,7 @@ using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Log; using Robust.Shared.Log;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
@@ -21,12 +24,21 @@ namespace Content.Server.GameObjects.Components.Atmos
[Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!;
private (GridId, MapIndices) _lastPosition; private (GridId, MapIndices) _lastPosition;
private AtmosphereSystem _atmosphereSystem = default!;
public override string Name => "Airtight"; public override string Name => "Airtight";
[ViewVariables]
private int _airBlockedDirection;
private bool _airBlocked = true; private bool _airBlocked = true;
private bool _fixVacuum = false; private bool _fixVacuum = false;
[ViewVariables]
private bool _rotateAirBlocked = true;
[ViewVariables]
private bool _fixAirBlockedDirectionInitialize = true;
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public bool AirBlocked public bool AirBlocked
{ {
@@ -35,11 +47,19 @@ namespace Content.Server.GameObjects.Components.Atmos
{ {
_airBlocked = value; _airBlocked = value;
if (Owner.TryGetComponent(out SnapGridComponent? snapGrid)) UpdatePosition();
{
EntitySystem.Get<AtmosphereSystem>().GetGridAtmosphere(Owner.Transform.GridID)?.Invalidate(snapGrid.Position);
} }
} }
public AtmosDirection AirBlockedDirection
{
get => (AtmosDirection)_airBlockedDirection;
set
{
_airBlockedDirection = (int) value;
UpdatePosition();
}
} }
[ViewVariables] [ViewVariables]
@@ -51,22 +71,53 @@ namespace Content.Server.GameObjects.Components.Atmos
serializer.DataField(ref _airBlocked, "airBlocked", true); serializer.DataField(ref _airBlocked, "airBlocked", true);
serializer.DataField(ref _fixVacuum, "fixVacuum", true); serializer.DataField(ref _fixVacuum, "fixVacuum", true);
serializer.DataField(ref _airBlockedDirection, "airBlockedDirection", (int)AtmosDirection.All, WithFormat.Flags<AtmosDirectionFlags>());
serializer.DataField(ref _rotateAirBlocked, "rotateAirBlocked", true);
serializer.DataField(ref _fixAirBlockedDirectionInitialize, "fixAirBlockedDirectionInitialize", true);
} }
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
// Using the SnapGrid is critical for the performance of the room builder, and thus if _atmosphereSystem = EntitySystem.Get<AtmosphereSystem>();
// it is absent the component will not be airtight. A warning is much easier to track
// down than the object magically not being airtight, so log one if the SnapGrid component // Using the SnapGrid is critical for performance, and thus if it is absent the component
// is missing. // will not be airtight. A warning is much easier to track down than the object magically
// not being airtight, so log one if the SnapGrid component is missing.
if (!Owner.EnsureComponent(out SnapGridComponent _)) if (!Owner.EnsureComponent(out SnapGridComponent _))
Logger.Warning($"Entity {Owner} at {Owner.Transform.MapPosition.ToString()} didn't have a {nameof(SnapGridComponent)}"); Logger.Warning($"Entity {Owner} at {Owner.Transform.MapPosition.ToString()} didn't have a {nameof(SnapGridComponent)}");
Owner.EntityManager.EventBus.SubscribeEvent<RotateEvent>(EventSource.Local, this, RotateEvent);
if(_fixAirBlockedDirectionInitialize)
RotateEvent(new RotateEvent(Owner, Angle.South, Owner.Transform.LocalRotation));
UpdatePosition(); UpdatePosition();
} }
private void RotateEvent(RotateEvent ev)
{
if (!_rotateAirBlocked || ev.Sender != Owner || ev.NewRotation == ev.OldRotation || AirBlockedDirection == AtmosDirection.Invalid)
return;
var diff = ev.NewRotation - ev.OldRotation;
var newAirBlockedDirs = AtmosDirection.Invalid;
// TODO ATMOS MULTIZ When we make multiZ atmos, special case this.
for (int i = 0; i < Atmospherics.Directions; i++)
{
var direction = (AtmosDirection) (1 << i);
if (!AirBlockedDirection.HasFlag(direction)) continue;
var angle = direction.ToAngle();
angle += diff;
newAirBlockedDirs |= angle.ToAtmosDirectionCardinal();
}
AirBlockedDirection = newAirBlockedDirs;
}
public void MapInit() public void MapInit()
{ {
if (Owner.TryGetComponent(out SnapGridComponent? snapGrid)) if (Owner.TryGetComponent(out SnapGridComponent? snapGrid))
@@ -89,13 +140,10 @@ namespace Content.Server.GameObjects.Components.Atmos
snapGrid.OnPositionChanged -= OnTransformMove; snapGrid.OnPositionChanged -= OnTransformMove;
} }
if (_fixVacuum) UpdatePosition(_lastPosition.Item1, _lastPosition.Item2);
{
var mapIndices = Owner.Transform.Coordinates.ToMapIndices(_entityManager, _mapManager);
EntitySystem.Get<AtmosphereSystem>().GetGridAtmosphere(Owner.Transform.GridID)?.FixVacuum(mapIndices);
}
UpdatePosition(); if (_fixVacuum)
_atmosphereSystem.GetGridAtmosphere(_lastPosition.Item1)?.FixVacuum(_lastPosition.Item2);
} }
private void OnTransformMove() private void OnTransformMove()
@@ -111,13 +159,18 @@ namespace Content.Server.GameObjects.Components.Atmos
private void UpdatePosition() private void UpdatePosition()
{ {
var mapIndices = Owner.Transform.Coordinates.ToMapIndices(_entityManager, _mapManager); if (Owner.TryGetComponent(out SnapGridComponent? snapGrid))
UpdatePosition(Owner.Transform.GridID, mapIndices); UpdatePosition(Owner.Transform.GridID, snapGrid.Position);
} }
private void UpdatePosition(GridId gridId, MapIndices pos) private void UpdatePosition(GridId gridId, MapIndices pos)
{ {
EntitySystem.Get<AtmosphereSystem>().GetGridAtmosphere(gridId)?.Invalidate(pos); var gridAtmos = _atmosphereSystem.GetGridAtmosphere(gridId);
if (gridAtmos == null) return;
gridAtmos.UpdateAdjacentBits(pos);
gridAtmos.Invalidate(pos);
} }
} }
} }

View File

@@ -0,0 +1,105 @@
using System;
using System.Threading.Tasks;
using Content.Server.Atmos;
using Content.Server.GameObjects.Components.Doors;
using Content.Server.GameObjects.Components.Interactable;
using Content.Server.Interfaces;
using Content.Shared.GameObjects.Components.Doors;
using Content.Shared.GameObjects.Components.Interactable;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
namespace Content.Server.GameObjects.Components.Atmos
{
[RegisterComponent]
public class FirelockComponent : ServerDoorComponent, IInteractUsing, ICollideBehavior
{
public override string Name => "Firelock";
protected override TimeSpan CloseTimeOne => TimeSpan.FromSeconds(0.1f);
protected override TimeSpan CloseTimeTwo => TimeSpan.FromSeconds(0.6f);
protected override TimeSpan OpenTimeOne => TimeSpan.FromSeconds(0.1f);
protected override TimeSpan OpenTimeTwo => TimeSpan.FromSeconds(0.6f);
public void CollideWith(IEntity collidedWith)
{
// We do nothing.
}
protected override void Startup()
{
base.Startup();
if (Owner.TryGetComponent(out AirtightComponent airtightComponent))
{
airtightComponent.AirBlocked = false;
}
if (Owner.TryGetComponent(out ICollidableComponent collidableComponent))
{
collidableComponent.Hard = false;
}
Safety = false;
if (Occludes && Owner.TryGetComponent(out OccluderComponent occluder))
{
occluder.Enabled = false;
}
State = DoorState.Open;
SetAppearance(DoorVisualState.Open);
}
public bool EmergencyPressureStop()
{
var closed = State == DoorState.Open && Close();
if(closed)
Owner.GetComponent<AirtightComponent>().AirBlocked = true;
return closed;
}
public override bool CanOpen()
{
return !IsHoldingFire() && !IsHoldingPressure() && base.CanOpen();
}
public override bool CanClose(IEntity user) => true;
public override bool CanOpen(IEntity user) => CanOpen();
public async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
{
if (!eventArgs.Using.TryGetComponent<ToolComponent>(out var tool))
return false;
if (tool.HasQuality(ToolQuality.Prying))
{
var holdingPressure = IsHoldingPressure();
var holdingFire = IsHoldingFire();
if (State == DoorState.Closed)
{
if(holdingPressure)
Owner.PopupMessage(eventArgs.User, "A gush of air blows in your face... Maybe you should reconsider.");
}
if (!await tool.UseTool(eventArgs.User, Owner, holdingPressure || holdingFire ? 1.5f : 0.25f, ToolQuality.Prying)) return false;
if (State == DoorState.Closed)
Open();
else if (State == DoorState.Open)
Close();
return true;
}
return false;
}
}
}

View File

@@ -9,11 +9,14 @@ using Content.Server.GameObjects.Components.Atmos.Piping;
using Content.Server.GameObjects.Components.NodeContainer.NodeGroups; using Content.Server.GameObjects.Components.NodeContainer.NodeGroups;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Maps; using Content.Shared.Maps;
using Robust.Server.GameObjects.EntitySystems.TileLookup;
using Robust.Server.Interfaces.GameObjects; using Robust.Server.Interfaces.GameObjects;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components.Map; using Robust.Shared.GameObjects.Components.Map;
using Robust.Shared.GameObjects.Components.Transform; using Robust.Shared.GameObjects.Components.Transform;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.Map; using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.Timing; using Robust.Shared.Timing;
@@ -32,6 +35,8 @@ namespace Content.Server.GameObjects.Components.Atmos
[Robust.Shared.IoC.Dependency] private ITileDefinitionManager _tileDefinitionManager = default!; [Robust.Shared.IoC.Dependency] private ITileDefinitionManager _tileDefinitionManager = default!;
[Robust.Shared.IoC.Dependency] private IServerEntityManager _serverEntityManager = default!; [Robust.Shared.IoC.Dependency] private IServerEntityManager _serverEntityManager = default!;
public GridTileLookupSystem GridTileLookupSystem { get; private set; } = default!;
/// <summary> /// <summary>
/// Check current execution time every n instances processed. /// Check current execution time every n instances processed.
/// </summary> /// </summary>
@@ -52,6 +57,7 @@ namespace Content.Server.GameObjects.Components.Atmos
private bool _paused = false; private bool _paused = false;
private float _timer = 0f; private float _timer = 0f;
private Stopwatch _stopwatch = new Stopwatch(); private Stopwatch _stopwatch = new Stopwatch();
private GridId _gridId;
[ViewVariables] [ViewVariables]
public int UpdateCounter { get; private set; } = 0; public int UpdateCounter { get; private set; } = 0;
@@ -155,23 +161,25 @@ namespace Content.Server.GameObjects.Components.Atmos
/// <inheritdoc /> /// <inheritdoc />
public virtual void PryTile(MapIndices indices) public virtual void PryTile(MapIndices indices)
{ {
if (!Owner.TryGetComponent(out IMapGridComponent? mapGridComponent)) return;
if (IsSpace(indices) || IsAirBlocked(indices)) return; if (IsSpace(indices) || IsAirBlocked(indices)) return;
var mapGrid = mapGridComponent.Grid; indices.PryTile(_gridId, _mapManager, _tileDefinitionManager, _serverEntityManager);
indices.PryTile(mapGrid.Index, _mapManager, _tileDefinitionManager, _serverEntityManager);
} }
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
RepopulateTiles(); RepopulateTiles();
GridTileLookupSystem = EntitySystem.Get<GridTileLookupSystem>();
} }
public override void OnAdd() public override void OnAdd()
{ {
base.OnAdd(); base.OnAdd();
RepopulateTiles();
if (Owner.TryGetComponent(out IMapGridComponent? mapGrid))
_gridId = mapGrid.GridIndex;
} }
public virtual void RepopulateTiles() public virtual void RepopulateTiles()
@@ -182,6 +190,8 @@ namespace Content.Server.GameObjects.Components.Atmos
{ {
if(!Tiles.ContainsKey(tile.GridIndices)) if(!Tiles.ContainsKey(tile.GridIndices))
Tiles.Add(tile.GridIndices, new TileAtmosphere(this, tile.GridIndex, tile.GridIndices, new GasMixture(GetVolumeForCells(1)){Temperature = Atmospherics.T20C})); Tiles.Add(tile.GridIndices, new TileAtmosphere(this, tile.GridIndex, tile.GridIndices, new GasMixture(GetVolumeForCells(1)){Temperature = Atmospherics.T20C}));
Invalidate(tile.GridIndices);
} }
foreach (var (_, tile) in Tiles.ToArray()) foreach (var (_, tile) in Tiles.ToArray())
@@ -199,16 +209,13 @@ namespace Content.Server.GameObjects.Components.Atmos
protected virtual void Revalidate() protected virtual void Revalidate()
{ {
if (!Owner.TryGetComponent(out IMapGridComponent? mapGrid)) return;
foreach (var indices in _invalidatedCoords.ToArray()) foreach (var indices in _invalidatedCoords.ToArray())
{ {
var tile = GetTile(indices); var tile = GetTile(indices);
AddActiveTile(tile);
if (tile == null) if (tile == null)
{ {
tile = new TileAtmosphere(this, mapGrid.Grid.Index, indices, new GasMixture(GetVolumeForCells(1)){Temperature = Atmospherics.T20C}); tile = new TileAtmosphere(this, _gridId, indices, new GasMixture(GetVolumeForCells(1)){Temperature = Atmospherics.T20C});
Tiles[indices] = tile; Tiles[indices] = tile;
} }
@@ -224,19 +231,19 @@ namespace Content.Server.GameObjects.Components.Atmos
} }
else else
{ {
var obs = GetObstructingComponent(indices); if (tile.Air == null && NeedsVacuumFixing(indices))
if (obs != null)
{
if (tile.Air == null && obs.FixVacuum)
{ {
FixVacuum(tile.GridIndices); FixVacuum(tile.GridIndices);
} }
}
tile.Air ??= new GasMixture(GetVolumeForCells(1)){Temperature = Atmospherics.T20C}; tile.Air ??= new GasMixture(GetVolumeForCells(1)){Temperature = Atmospherics.T20C};
} }
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(); tile.UpdateAdjacent();
tile.UpdateVisuals(); tile.UpdateVisuals();
@@ -246,19 +253,23 @@ namespace Content.Server.GameObjects.Components.Atmos
var otherIndices = indices.Offset(direction.ToDirection()); var otherIndices = indices.Offset(direction.ToDirection());
var otherTile = GetTile(otherIndices); var otherTile = GetTile(otherIndices);
AddActiveTile(otherTile); AddActiveTile(otherTile);
otherTile?.UpdateAdjacent(direction.GetOpposite());
} }
} }
_invalidatedCoords.Clear(); _invalidatedCoords.Clear();
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void UpdateAdjacentBits(MapIndices indices)
{
GetTile(indices)?.UpdateAdjacent();
}
/// <inheritdoc /> /// <inheritdoc />
public virtual void FixVacuum(MapIndices indices) public virtual void FixVacuum(MapIndices indices)
{ {
if (!Owner.TryGetComponent(out IMapGridComponent? mapGrid)) return;
var tile = GetTile(indices); var tile = GetTile(indices);
if (tile?.GridIndex != mapGrid.Grid.Index) return; if (tile?.GridIndex != _gridId) return;
var adjacent = GetAdjacentTiles(indices); var adjacent = GetAdjacentTiles(indices);
tile.Air = new GasMixture(GetVolumeForCells(1)){Temperature = Atmospherics.T20C}; tile.Air = new GasMixture(GetVolumeForCells(1)){Temperature = Atmospherics.T20C};
Tiles[indices] = tile; Tiles[indices] = tile;
@@ -277,8 +288,7 @@ namespace Content.Server.GameObjects.Components.Atmos
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual void AddActiveTile(TileAtmosphere? tile) public virtual void AddActiveTile(TileAtmosphere? tile)
{ {
if (!Owner.TryGetComponent(out IMapGridComponent? mapGrid)) return; if (tile?.GridIndex != _gridId) return;
if (tile?.GridIndex != mapGrid.Grid.Index) return;
tile.Excited = true; tile.Excited = true;
_activeTiles.Add(tile); _activeTiles.Add(tile);
} }
@@ -297,8 +307,7 @@ namespace Content.Server.GameObjects.Components.Atmos
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual void AddHotspotTile(TileAtmosphere? tile) public virtual void AddHotspotTile(TileAtmosphere? tile)
{ {
if (!Owner.TryGetComponent(out IMapGridComponent? mapGrid)) return; if (tile?.GridIndex != _gridId || tile?.Air == null) return;
if (tile?.GridIndex != mapGrid.Grid.Index || tile?.Air == null) return;
_hotspotTiles.Add(tile); _hotspotTiles.Add(tile);
} }
@@ -312,8 +321,7 @@ namespace Content.Server.GameObjects.Components.Atmos
public virtual void AddSuperconductivityTile(TileAtmosphere? tile) public virtual void AddSuperconductivityTile(TileAtmosphere? tile)
{ {
if (!Owner.TryGetComponent(out IMapGridComponent? mapGrid)) return; if (tile?.GridIndex != _gridId) return;
if (tile?.GridIndex != mapGrid.Grid.Index) return;
_superconductivityTiles.Add(tile); _superconductivityTiles.Add(tile);
} }
@@ -327,8 +335,7 @@ namespace Content.Server.GameObjects.Components.Atmos
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual void AddHighPressureDelta(TileAtmosphere? tile) public virtual void AddHighPressureDelta(TileAtmosphere? tile)
{ {
if (!Owner.TryGetComponent(out IMapGridComponent? mapGrid)) return; if (tile?.GridIndex != _gridId) return;
if (tile?.GridIndex != mapGrid.Grid.Index) return;
_highPressureDelta.Add(tile); _highPressureDelta.Add(tile);
} }
@@ -382,24 +389,30 @@ namespace Content.Server.GameObjects.Components.Atmos
/// <inheritdoc /> /// <inheritdoc />
public TileAtmosphere? GetTile(MapIndices indices, bool createSpace = true) public TileAtmosphere? GetTile(MapIndices indices, bool createSpace = true)
{ {
if (!Owner.TryGetComponent(out IMapGridComponent? mapGrid)) return null;
if (Tiles.TryGetValue(indices, out var tile)) return tile; if (Tiles.TryGetValue(indices, out var tile)) return tile;
// We don't have that tile! // We don't have that tile!
if (IsSpace(indices) && createSpace) if (IsSpace(indices) && createSpace)
{ {
return new TileAtmosphere(this, mapGrid.Grid.Index, indices, new GasMixture(GetVolumeForCells(1)){Temperature = Atmospherics.TCMB}, true); return new TileAtmosphere(this, _gridId, indices, new GasMixture(GetVolumeForCells(1)){Temperature = Atmospherics.TCMB}, true);
} }
return null; return null;
} }
/// <inheritdoc /> /// <inheritdoc />
public bool IsAirBlocked(MapIndices indices) public bool IsAirBlocked(MapIndices indices, AtmosDirection direction = AtmosDirection.All)
{ {
var ac = GetObstructingComponent(indices); foreach (var obstructingComponent in GetObstructingComponents(indices))
return ac != null && ac.AirBlocked; {
if (!obstructingComponent.AirBlocked)
continue;
if (obstructingComponent.AirBlockedDirection.HasFlag(direction))
return true;
}
return false;
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -763,17 +776,43 @@ namespace Content.Server.GameObjects.Components.Atmos
return true; return true;
} }
private AirtightComponent? GetObstructingComponent(MapIndices indices) private IEnumerable<AirtightComponent> GetObstructingComponents(MapIndices indices)
{ {
if (!Owner.TryGetComponent(out IMapGridComponent? mapGrid)) return default; var gridLookup = EntitySystem.Get<GridTileLookupSystem>();
foreach (var v in mapGrid.Grid.GetSnapGridCell(indices, SnapGridOffset.Center)) var list = new List<AirtightComponent>();
foreach (var v in gridLookup.GetEntitiesIntersecting(_gridId, indices))
{ {
if (v.Owner.TryGetComponent<AirtightComponent>(out var ac)) if (v.TryGetComponent<AirtightComponent>(out var ac))
return ac; list.Add(ac);
} }
return null; return list;
}
private bool NeedsVacuumFixing(MapIndices indices)
{
var value = false;
foreach (var airtightComponent in GetObstructingComponents(indices))
{
value |= airtightComponent.FixVacuum;
}
return value;
}
private AtmosDirection GetBlockedDirections(MapIndices indices)
{
var value = AtmosDirection.Invalid;
foreach (var airtightComponent in GetObstructingComponents(indices))
{
value |= airtightComponent.AirBlockedDirection;
}
return value;
} }
public void Dispose() public void Dispose()

View File

@@ -2,6 +2,7 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using Content.Server.Atmos;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Access; using Content.Server.GameObjects.Components.Access;
using Content.Server.GameObjects.Components.Atmos; using Content.Server.GameObjects.Components.Atmos;
@@ -54,11 +55,11 @@ namespace Content.Server.GameObjects.Components.Doors
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private static readonly TimeSpan CloseTimeOne = TimeSpan.FromSeconds(0.3f); protected virtual TimeSpan CloseTimeOne => TimeSpan.FromSeconds(0.3f);
private static readonly TimeSpan CloseTimeTwo = TimeSpan.FromSeconds(0.9f); protected virtual TimeSpan CloseTimeTwo => TimeSpan.FromSeconds(0.9f);
private static readonly TimeSpan OpenTimeOne = TimeSpan.FromSeconds(0.3f); protected virtual TimeSpan OpenTimeOne => TimeSpan.FromSeconds(0.3f);
private static readonly TimeSpan OpenTimeTwo = TimeSpan.FromSeconds(0.9f); protected virtual TimeSpan OpenTimeTwo => TimeSpan.FromSeconds(0.9f);
private static readonly TimeSpan DenyTime = TimeSpan.FromSeconds(0.45f); protected virtual TimeSpan DenyTime => TimeSpan.FromSeconds(0.45f);
private const int DoorCrushDamage = 15; private const int DoorCrushDamage = 15;
private const float DoorStunTime = 5f; private const float DoorStunTime = 5f;
@@ -67,6 +68,8 @@ namespace Content.Server.GameObjects.Components.Doors
[ViewVariables(VVAccess.ReadWrite)] private bool _occludes; [ViewVariables(VVAccess.ReadWrite)] private bool _occludes;
public bool Occludes => _occludes;
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public bool IsWeldedShut public bool IsWeldedShut
{ {
@@ -86,12 +89,16 @@ namespace Content.Server.GameObjects.Components.Doors
private bool _canWeldShut = true; private bool _canWeldShut = true;
[ViewVariables(VVAccess.ReadWrite)]
private bool _canCrush = true;
public override void ExposeData(ObjectSerializer serializer) public override void ExposeData(ObjectSerializer serializer)
{ {
base.ExposeData(serializer); base.ExposeData(serializer);
serializer.DataField(ref _occludes, "occludes", true); serializer.DataField(ref _occludes, "occludes", true);
serializer.DataField(ref _isWeldedShut, "welded", false); serializer.DataField(ref _isWeldedShut, "welded", false);
serializer.DataField(ref _canCrush, "canCrush", true);
} }
public override void OnRemove() public override void OnRemove()
@@ -146,7 +153,7 @@ namespace Content.Server.GameObjects.Components.Doors
} }
} }
private void SetAppearance(DoorVisualState state) protected void SetAppearance(DoorVisualState state)
{ {
if (Owner.TryGetComponent(out AppearanceComponent? appearance)) if (Owner.TryGetComponent(out AppearanceComponent? appearance))
{ {
@@ -159,7 +166,7 @@ namespace Content.Server.GameObjects.Components.Doors
return !_isWeldedShut; return !_isWeldedShut;
} }
public bool CanOpen(IEntity user) public virtual bool CanOpen(IEntity user)
{ {
if (!CanOpen()) return false; if (!CanOpen()) return false;
@@ -253,7 +260,7 @@ namespace Content.Server.GameObjects.Components.Doors
return true; return true;
} }
public bool CanClose(IEntity user) public virtual bool CanClose(IEntity user)
{ {
if (!CanClose()) return false; if (!CanClose()) return false;
if (!Owner.TryGetComponent(out AccessReader? accessReader)) if (!Owner.TryGetComponent(out AccessReader? accessReader))
@@ -313,11 +320,62 @@ namespace Content.Server.GameObjects.Components.Doors
} }
} }
public bool IsHoldingPressure(float threshold = 20)
{
var atmosphereSystem = EntitySystem.Get<AtmosphereSystem>();
if (!Owner.Transform.Coordinates.TryGetTileAtmosphere(out var tileAtmos))
return false;
var gridAtmosphere = atmosphereSystem.GetGridAtmosphere(Owner.Transform.GridID);
if (gridAtmosphere == null)
return false;
var minMoles = float.MaxValue;
var maxMoles = 0f;
foreach (var (direction, adjacent) in gridAtmosphere.GetAdjacentTiles(tileAtmos.GridIndices))
{
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.GridID);
if (gridAtmosphere == null)
return false;
foreach (var (direction, adjacent) in gridAtmosphere.GetAdjacentTiles(tileAtmos.GridIndices))
{
if (adjacent.Hotspot.Valid)
return true;
}
return false;
}
public bool Close() public bool Close()
{ {
bool shouldCheckCrush = false; bool shouldCheckCrush = false;
if (Owner.TryGetComponent(out ICollidableComponent? collidable) && collidable.IsColliding(Vector2.Zero, false)) if (_canCrush && Owner.TryGetComponent(out ICollidableComponent? collidable) && collidable.IsColliding(Vector2.Zero, false))
{ {
if (Safety) if (Safety)
return false; return false;
@@ -336,7 +394,7 @@ namespace Content.Server.GameObjects.Components.Doors
Timer.Spawn(CloseTimeOne, async () => Timer.Spawn(CloseTimeOne, async () =>
{ {
if (shouldCheckCrush) if (shouldCheckCrush && _canCrush)
{ {
CheckCrush(); CheckCrush();
} }

View File

@@ -0,0 +1,40 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Content.Shared.Maps;
using Robust.Server.GameObjects.EntitySystems.TileLookup;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Map;
namespace Content.Server.Utility
{
public static class GridTileLookupHelpers
{
/// <summary>
/// Helper that returns all entities in a turf very fast.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IEnumerable<IEntity> GetEntitiesInTileFast(this TileRef turf, GridTileLookupSystem? gridTileLookup = null)
{
gridTileLookup ??= EntitySystem.Get<GridTileLookupSystem>();
return gridTileLookup.GetEntitiesIntersecting(turf.GridIndex, turf.GridIndices);
}
/// <summary>
/// Helper that returns all entities in a turf.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IEnumerable<IEntity> GetEntitiesInTileFast(this MapIndices indices, GridId gridId, GridTileLookupSystem? gridTileLookup = null)
{
var turf = indices.GetTileRef(gridId);
if (turf == null)
return Enumerable.Empty<IEntity>();
return GetEntitiesInTileFast(turf.Value, gridTileLookup);
}
}
}

View File

@@ -1,13 +1,15 @@
using System; using System;
using Robust.Shared.Maths; using Robust.Shared.Maths;
using Robust.Shared.Serialization;
namespace Content.Shared.Atmos namespace Content.Shared.Atmos
{ {
/// <summary> /// <summary>
/// The reason we use this over <see cref="Direction"/> is that we are going to do some heavy bitflag usage. /// The reason we use this over <see cref="Direction"/> is that we are going to do some heavy bitflag usage.
/// </summary> /// </summary>
[Flags] [Flags, Serializable]
public enum AtmosDirection : byte [FlagsFor(typeof(AtmosDirectionFlags))]
public enum AtmosDirection
{ {
Invalid = 0, Invalid = 0,
North = 1 << 0, North = 1 << 0,
@@ -75,6 +77,49 @@ namespace Content.Shared.Atmos
}; };
} }
/// <summary>
/// Converts a direction to an angle, where angle is -PI to +PI.
/// </summary>
/// <param name="direction"></param>
/// <returns></returns>
public static Angle ToAngle(this AtmosDirection direction)
{
return direction switch
{
AtmosDirection.East => Angle.FromDegrees(0),
AtmosDirection.North => Angle.FromDegrees(90),
AtmosDirection.West => Angle.FromDegrees(180),
AtmosDirection.South => Angle.FromDegrees(270),
AtmosDirection.NorthEast => Angle.FromDegrees(45),
AtmosDirection.NorthWest => Angle.FromDegrees(135),
AtmosDirection.SouthWest => Angle.FromDegrees(225),
AtmosDirection.SouthEast => Angle.FromDegrees(315),
_ => throw new ArgumentOutOfRangeException(nameof(direction), $"It was {direction}."),
};
}
/// <summary>
/// Converts an angle to a cardinal AtmosDirection
/// </summary>
/// <param name="angle"></param>
/// <returns></returns>
public static AtmosDirection ToAtmosDirectionCardinal(this Angle angle)
{
return angle.GetCardinalDir().ToAtmosDirection();
}
/// <summary>
/// Converts an angle to an AtmosDirection
/// </summary>
/// <param name="angle"></param>
/// <returns></returns>
public static AtmosDirection ToAtmosDirection(this Angle angle)
{
return angle.GetDir().ToAtmosDirection();
}
public static int ToIndex(this AtmosDirection direction) public static int ToIndex(this AtmosDirection direction)
{ {
// This will throw if you pass an invalid direction. Not this method's fault, but yours! // This will throw if you pass an invalid direction. Not this method's fault, but yours!
@@ -85,5 +130,12 @@ namespace Content.Shared.Atmos
{ {
return direction | other; return direction | other;
} }
public static AtmosDirection WithoutFlag(this AtmosDirection direction, AtmosDirection other)
{
return direction & ~other;
} }
}
public sealed class AtmosDirectionFlags { }
} }

View File

@@ -1,8 +1,11 @@
#nullable enable #nullable enable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Content.Shared.Physics; using Content.Shared.Physics;
using Content.Shared.Utility; using Content.Shared.Utility;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map; using Robust.Shared.Interfaces.Map;
using Robust.Shared.Interfaces.Physics; using Robust.Shared.Interfaces.Physics;
@@ -57,7 +60,7 @@ namespace Content.Shared.Maps
if (!mapManager.TryGetGrid(coordinates.GetGridId(entityManager), out var grid)) if (!mapManager.TryGetGrid(coordinates.GetGridId(entityManager), out var grid))
return null; return null;
if (!grid.TryGetTileRef(coordinates.ToMapIndices(entityManager, mapManager), out var tile)) if (!grid.TryGetTileRef(coordinates, out var tile))
return null; return null;
return tile; return tile;
@@ -120,13 +123,40 @@ namespace Content.Shared.Maps
/// <summary> /// <summary>
/// Helper that returns all entities in a turf. /// Helper that returns all entities in a turf.
/// </summary> /// </summary>
public static IEnumerable<IEntity> GetEntitiesInTile(this TileRef turf, bool approximate = false) [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IEnumerable<IEntity> GetEntitiesInTile(this TileRef turf, bool approximate = false, IEntityManager? entityManager = null)
{ {
var entityManager = IoCManager.Resolve<IEntityManager>(); entityManager ??= IoCManager.Resolve<IEntityManager>();
return entityManager.GetEntitiesIntersecting(turf.MapIndex, GetWorldTileBox(turf), approximate); return entityManager.GetEntitiesIntersecting(turf.MapIndex, GetWorldTileBox(turf), approximate);
} }
/// <summary>
/// Helper that returns all entities in a turf.
/// </summary>
public static IEnumerable<IEntity> GetEntitiesInTile(this EntityCoordinates coordinates, bool approximate = false, IEntityManager? entityManager = null)
{
var turf = coordinates.GetTileRef();
if (turf == null)
return Enumerable.Empty<IEntity>();
return GetEntitiesInTile(turf.Value, approximate, entityManager);
}
/// <summary>
/// Helper that returns all entities in a turf.
/// </summary>
public static IEnumerable<IEntity> GetEntitiesInTile(this MapIndices indices, GridId gridId, bool approximate = false, IEntityManager? entityManager = null)
{
var turf = indices.GetTileRef(gridId);
if (turf == null)
return Enumerable.Empty<IEntity>();
return GetEntitiesInTile(turf.Value, approximate, entityManager);
}
/// <summary> /// <summary>
/// Checks if a turf has something dense on it. /// Checks if a turf has something dense on it.
/// </summary> /// </summary>

View File

@@ -56,7 +56,6 @@
type: WiresBoundUserInterface type: WiresBoundUserInterface
- type: Airtight - type: Airtight
fixVacuum: true fixVacuum: true
adjacentAtmosphere: true
- type: Occluder - type: Occluder
- type: SnapGrid - type: SnapGrid
offset: Center offset: Center

View File

@@ -0,0 +1,108 @@
- type: entity
id: Firelock
name: firelock
description: Apply crowbar.
components:
- type: Clickable
- type: InteractionOutline
- type: Sprite
netsync: false
drawdepth: Mobs # They're on the same layer as mobs, perspective.
sprite: Constructible/Structures/Doors/firelock.rsi
layers:
- state: closed
map: ["enum.DoorVisualLayers.Base"]
- state: closed_unlit
shader: unshaded
map: ["enum.DoorVisualLayers.BaseUnlit"]
- state: welded
map: ["enum.DoorVisualLayers.BaseWelded"]
- state: bolted
shader: unshaded
map: ["enum.DoorVisualLayers.BaseBolted"]
- state: panel_open
map: ["enum.WiresVisualLayers.MaintenancePanel"]
- type: Icon
sprite: Constructible/Structures/Doors/firelock.rsi
state: closed
- type: Collidable
shapes:
- !type:PhysShapeAabb
bounds: "-0.49,-0.49,0.49,0.49" # don't want this colliding with walls or they won't close
mask:
- MobImpassable
layer:
- Opaque
- Impassable
- MobImpassable
- VaultImpassable
- SmallImpassable
- type: Firelock
- type: Appearance
visuals:
- type: AirlockVisualizer
open_sound: /Audio/Machines/airlock_open.ogg
close_sound: /Audio/Machines/airlock_close.ogg
deny_sound: /Audio/Machines/airlock_deny.ogg
animation_time: 0.6
- type: WiresVisualizer
- type: Wires
BoardName: "Firelock Control"
LayoutId: Firelock
- type: UserInterface
interfaces:
- key: enum.WiresUiKey.Key
type: WiresBoundUserInterface
- type: Airtight
fixVacuum: true
- type: Occluder
- type: SnapGrid
offset: Center
placement:
mode: SnapgridCenter
- type: entity
id: FirelockGlass
parent: Firelock
name: glass firelock
components:
- type: Firelock
occludes: false
- type: Occluder
enabled: false
- type: Sprite
sprite: Constructible/Structures/Doors/firelock_glass.rsi
- type: Icon
sprite: Constructible/Structures/Doors/firelock_glass.rsi
- type: entity
id: FirelockEdge
parent: Firelock
name: firelock
prefix: south
components:
- type: Firelock
occludes: false
canCrush: false
- type: Occluder
enabled: false
- type: Sprite
sprite: Constructible/Structures/Doors/edge_door_hazard.rsi
- type: Icon
sprite: Constructible/Structures/Doors/edge_door_hazard.rsi
- type: Airtight
fixVacuum: true
airBlockedDirection:
- South
- type: Collidable
shapes:
- !type:PhysShapeAabb
bounds: "-0.49,-0.49,-0.2,0.49" # don't want this colliding with walls or they won't close
mask:
- MobImpassable
layer:
- Opaque
- Impassable
- MobImpassable
- VaultImpassable
- SmallImpassable

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,430 @@
{
"version": 1,
"size": {
"x": 32,
"y": 32
},
"license": "CC-BY-SA 3.0",
"copyright": "Taken from https://github.com/vgstation-coders/vgstation13/ at 38b65a605df7ae2907d6bf0d4aebc5faa1bbc561",
"states": [
{
"name": "alert_cold",
"directions": 4,
"delays": [
[
0.5,
0.5
],
[
0.5,
0.5
],
[
0.5,
0.5
],
[
0.5,
0.5
]
]
},
{
"name": "alert_hot",
"directions": 4,
"delays": [
[
0.5,
0.5
],
[
0.5,
0.5
],
[
0.5,
0.5
],
[
0.5,
0.5
]
]
},
{
"name": "bolted",
"directions": 4,
"delays": [
[
1.0
],
[
1.0
],
[
1.0
],
[
1.0
]
]
},
{
"name": "closed",
"directions": 4,
"delays": [
[
1.0
],
[
1.0
],
[
1.0
],
[
1.0
]
]
},
{
"name": "closed_unlit",
"directions": 4,
"delays": [
[
1.0
],
[
1.0
],
[
1.0
],
[
1.0
]
]
},
{
"name": "closing",
"directions": 4,
"delays": [
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
],
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
],
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
],
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
]
]
},
{
"name": "closing_unlit",
"directions": 4,
"delays": [
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
],
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
],
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
],
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
]
]
},
{
"name": "deny",
"directions": 4,
"delays": [
[
1.0
],
[
1.0
],
[
1.0
],
[
1.0
]
]
},
{
"name": "deny_unlit",
"directions": 4,
"delays": [
[
1.0
],
[
1.0
],
[
1.0
],
[
1.0
]
]
},
{
"name": "locked",
"directions": 4,
"delays": [
[
1.0
],
[
1.0
],
[
1.0
],
[
1.0
]
]
},
{
"name": "open",
"directions": 4,
"delays": [
[
1.0
],
[
1.0
],
[
1.0
],
[
1.0
]
]
},
{
"name": "opening",
"directions": 4,
"delays": [
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
],
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
],
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
],
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
]
]
},
{
"name": "opening_unlit",
"directions": 4,
"delays": [
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
],
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
],
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
],
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
]
]
},
{
"name": "palert",
"directions": 4,
"delays": [
[
0.5,
0.5
],
[
0.5,
0.5
],
[
0.5,
0.5
],
[
0.5,
0.5
]
]
},
{
"name": "panel_closing",
"directions": 1,
"delays": [
[
0.1,
0.1,
0.07,
0.07,
0.07,
0.07,
0.27
]
]
},
{
"name": "panel_open",
"directions": 1,
"delays": [
[
1.0
]
]
},
{
"name": "panel_opening",
"directions": 1,
"delays": [
[
0.2,
0.07,
0.07,
0.07,
0.07,
0.07,
0.2
]
]
},
{
"name": "welded",
"directions": 4,
"delays": [
[
1.0
],
[
1.0
],
[
1.0
],
[
1.0
]
]
},
{
"name": "welded_open",
"directions": 4,
"delays": [
[
1.0
],
[
1.0
],
[
1.0
],
[
1.0
]
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,227 @@
{
"version": 1,
"size": {
"x": 32,
"y": 32
},
"license": "CC-BY-SA 3.0",
"copyright": "Taken from https://github.com/tgstation/tgstation at 04e43d8c1d5097fdb697addd4395fb849dd341bd",
"states": [
{
"name": "bolted",
"directions": 1,
"delays": [
[
1.0
]
]
},
{
"name": "closed",
"directions": 1,
"delays": [
[
1.0
]
]
},
{
"name": "closed_unlit",
"directions": 1,
"delays": [
[
1.0
]
]
},
{
"name": "closing",
"directions": 1,
"delays": [
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
]
]
},
{
"name": "closing_unlit",
"directions": 1,
"delays": [
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
]
]
},
{
"name": "deny",
"directions": 1,
"delays": [
[
0.1,
0.1,
0.1
]
]
},
{
"name": "deny_unlit",
"directions": 1,
"delays": [
[
0.1,
0.1,
0.1
]
]
},
{
"name": "frame1",
"directions": 1,
"delays": [
[
1.0
]
]
},
{
"name": "frame2",
"directions": 1,
"delays": [
[
1.0
]
]
},
{
"name": "frame3",
"directions": 1,
"delays": [
[
1.0
]
]
},
{
"name": "frame4",
"directions": 1,
"delays": [
[
1.0
]
]
},
{
"name": "locked",
"directions": 1,
"delays": [
[
1.0
]
]
},
{
"name": "open",
"directions": 1,
"delays": [
[
1.0
]
]
},
{
"name": "opening",
"directions": 1,
"delays": [
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
]
]
},
{
"name": "opening_unlit",
"directions": 1,
"delays": [
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
]
]
},
{
"name": "panel_closing",
"directions": 1,
"delays": [
[
0.1,
0.1,
0.07,
0.07,
0.07,
0.07,
0.27
]
]
},
{
"name": "panel_open",
"directions": 1,
"delays": [
[
1.0
]
]
},
{
"name": "panel_opening",
"directions": 1,
"delays": [
[
0.2,
0.07,
0.07,
0.07,
0.07,
0.07,
0.2
]
]
},
{
"name": "welded",
"directions": 1,
"delays": [
[
1.0
]
]
},
{
"name": "welded_open",
"directions": 1,
"delays": [
[
1.0
]
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,191 @@
{
"version": 1,
"size": {
"x": 32,
"y": 32
},
"license": "CC-BY-SA 3.0",
"copyright": "Taken from https://github.com/tgstation/tgstation at 04e43d8c1d5097fdb697addd4395fb849dd341bd",
"states": [
{
"name": "bolted",
"directions": 1,
"delays": [
[
1.0
]
]
},
{
"name": "closed",
"directions": 1,
"delays": [
[
1.0
]
]
},
{
"name": "closed_unlit",
"directions": 1,
"delays": [
[
1.0
]
]
},
{
"name": "closing",
"directions": 1,
"delays": [
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
]
]
},
{
"name": "closing_unlit",
"directions": 1,
"delays": [
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
]
]
},
{
"name": "deny",
"directions": 1,
"delays": [
[
0.1,
0.1,
0.1
]
]
},
{
"name": "deny_unlit",
"directions": 1,
"delays": [
[
0.1,
0.1,
0.1
]
]
},
{
"name": "locked",
"directions": 1,
"delays": [
[
1.0
]
]
},
{
"name": "open",
"directions": 1,
"delays": [
[
1.0
]
]
},
{
"name": "opening",
"directions": 1,
"delays": [
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
]
]
},
{
"name": "opening_unlit",
"directions": 1,
"delays": [
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
]
]
},
{
"name": "panel_closing",
"directions": 1,
"delays": [
[
0.1,
0.1,
0.07,
0.07,
0.07,
0.07,
0.27
]
]
},
{
"name": "panel_open",
"directions": 1,
"delays": [
[
1.0
]
]
},
{
"name": "panel_opening",
"directions": 1,
"delays": [
[
0.2,
0.07,
0.07,
0.07,
0.07,
0.07,
0.2
]
]
},
{
"name": "welded",
"directions": 1,
"delays": [
[
1.0
]
]
},
{
"name": "welded_open",
"directions": 1,
"delays": [
[
1.0
]
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

View File

@@ -66,6 +66,7 @@
<s:String x:Key="/Default/FilterSettingsManager/CoverageFilterXml/@EntryValue">&lt;data&gt;&lt;IncludeFilters /&gt;&lt;ExcludeFilters&gt;&lt;Filter ModuleMask="Lidgren.Network" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /&gt;&lt;/ExcludeFilters&gt;&lt;/data&gt;</s:String> <s:String x:Key="/Default/FilterSettingsManager/CoverageFilterXml/@EntryValue">&lt;data&gt;&lt;IncludeFilters /&gt;&lt;ExcludeFilters&gt;&lt;Filter ModuleMask="Lidgren.Network" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /&gt;&lt;/ExcludeFilters&gt;&lt;/data&gt;</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Collidable/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Collidable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Cooldowns/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Cooldowns/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Firelock/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=cvar/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=cvar/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=cvars/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=cvars/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Discharger/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Discharger/@EntryIndexedValue">True</s:Boolean>