Merge remote-tracking branch 'upstream/master' into master-syndicate

# Conflicts:
#	Content.Packaging/ServerPackaging.cs
#	Resources/Prototypes/Accents/word_replacements.yml
#	Resources/Prototypes/Entities/Clothing/Neck/mantles.yml
#	Resources/Prototypes/Entities/Structures/Doors/Airlocks/highsec.yml
#	Resources/Prototypes/Maps/marathon.yml
#	Resources/Prototypes/Maps/packed.yml
#	Resources/Prototypes/Roles/Jobs/Engineering/atmospheric_technician.yml
#	Resources/Prototypes/Roles/Jobs/Security/detective.yml
#	Resources/Textures/Clothing/Head/Hardsuits/cybersun.rsi/meta.json
#	Resources/Textures/Clothing/Head/Hardsuits/spatiohelm.rsi/meta.json
#	Resources/Textures/Clothing/Head/Hardsuits/syndiecommander.rsi/meta.json
#	Resources/Textures/Clothing/Head/Hardsuits/syndieelite.rsi/meta.json
#	Resources/Textures/Clothing/Head/Helmets/atmos_firehelmet.rsi/meta.json
#	Resources/Textures/Clothing/Head/Helmets/paramedhelm.rsi/meta.json
#	Resources/Textures/Clothing/Mask/plaguedoctormask.rsi/meta.json
#	Resources/Textures/Clothing/Neck/Cloaks/capcloakformal.rsi/meta.json
#	Resources/Textures/Clothing/Neck/mantles/capmantle.rsi/meta.json
#	Resources/Textures/Clothing/Neck/mantles/cemantle.rsi/meta.json
#	Resources/Textures/Clothing/Neck/mantles/cmomantle.rsi/meta.json
#	Resources/Textures/Clothing/Neck/mantles/hopmantle.rsi/meta.json
#	Resources/Textures/Clothing/Neck/mantles/hosmantle.rsi/meta.json
#	Resources/Textures/Clothing/Neck/mantles/rdmantle.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Armor/lingarmor.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat.rsi/equipped-OUTERCLOTHING.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat.rsi/icon-open.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat.rsi/icon.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat.rsi/inhand-left.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat.rsi/inhand-right.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat.rsi/open-equipped-OUTERCLOTHING.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat.rsi/open-inhand-left.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat.rsi/open-inhand-right.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_chem.rsi/equipped-OUTERCLOTHING.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_chem.rsi/icon-open.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_chem.rsi/icon.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_chem.rsi/inhand-left.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_chem.rsi/inhand-right.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_chem.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_chem.rsi/open-equipped-OUTERCLOTHING.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_chem.rsi/open-inhand-left.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_chem.rsi/open-inhand-right.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_cmo.rsi/equipped-OUTERCLOTHING.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_cmo.rsi/icon-open.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_cmo.rsi/icon.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_cmo.rsi/inhand-left.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_cmo.rsi/inhand-right.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_cmo.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_cmo.rsi/open-equipped-OUTERCLOTHING.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_gene.rsi/equipped-OUTERCLOTHING.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_gene.rsi/icon.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_gene.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_robo.rsi/equipped-OUTERCLOTHING.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_robo.rsi/icon.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_robo.rsi/inhand-left.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_robo.rsi/inhand-right.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_robo.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_sci.rsi/equipped-OUTERCLOTHING-reptilian.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_sci.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_sci.rsi/open-equipped-OUTERCLOTHING-reptilian.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_viro.rsi/equipped-OUTERCLOTHING.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_viro.rsi/icon.png
#	Resources/Textures/Clothing/OuterClothing/Coats/labcoat_viro.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Coats/rndcoat.rsi/equipped-OUTERCLOTHING.png
#	Resources/Textures/Clothing/OuterClothing/Coats/rndcoat.rsi/icon.png
#	Resources/Textures/Clothing/OuterClothing/Hardsuits/cybersun.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Hardsuits/paramed.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Hardsuits/spatio.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Hardsuits/syndiecommander.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Hardsuits/syndieelite.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Suits/atmos_firesuit.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coat.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coatatmos.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coatcap.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coatcargo.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coatce.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coatcentcom.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coatchem.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coatcmo.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coatengi.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coatgen.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coathop.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coathos.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coathydro.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coatjani.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coatmed.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coatmime.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coatminer.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coatparamed.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coatqm.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coatrd.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coatrobo.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coatsci.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coatsec.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coatviro.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/WinterCoats/coatwarden.rsi/meta.json
#	Resources/Textures/Clothing/Shoes/Boots/combatboots.rsi/meta.json
#	Resources/Textures/Clothing/Shoes/Specific/bling.rsi/meta.json
#	Resources/Textures/Clothing/Uniforms/Jumpskirt/atmosf.rsi/meta.json
#	Resources/Textures/Clothing/Uniforms/Jumpskirt/capformaldress.rsi/meta.json
#	Resources/Textures/Clothing/Uniforms/Jumpskirt/operative_s.rsi/meta.json
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/atmos.rsi/meta.json
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/capformal.rsi/meta.json
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/centcom_officer.rsi/meta.json
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/centcomformal.rsi/meta.json
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/hosformal.rsi/meta.json
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/journalist.rsi/meta.json
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/operative.rsi/meta.json
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/psychologist.rsi/meta.json
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/reporter.rsi/meta.json
#	Resources/Textures/Mobs/Animals/regalrat.rsi/meta.json
#	Resources/Textures/Objects/Storage/boxes.rsi/meta.json
#	Resources/Textures/Objects/Weapons/Guns/Battery/antiquelasergun.rsi/meta.json
#	Resources/Textures/Structures/Doors/Airlocks/Glass/atmospherics.rsi/meta.json
#	Resources/Textures/Structures/Doors/Airlocks/Standard/atmospherics.rsi/meta.json
#	Resources/Textures/Structures/Doors/Airlocks/highsec/highsec.rsi/meta.json
#	Resources/Textures/Structures/Furniture/chairs.rsi/meta.json
#	Resources/Textures/Structures/Holo/holofan.rsi/meta.json
#	Resources/Textures/Structures/Holo/wetfloor.rsi/meta.json
This commit is contained in:
Morb0
2024-03-27 01:12:55 +03:00
571 changed files with 1718894 additions and 1708629 deletions

15
.github/workflows/check-crlf.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
name: CRLF Check
on:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
jobs:
build:
name: CRLF Check
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.6.0
- name: Check for CRLF
run: Tools/check_crlf.py

View File

@@ -1,4 +1,4 @@
name: Close PR's on master
name: Close PRs on master
on:
pull_request_target:

13
.github/workflows/labeler-untriaged.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
name: "Labels: Untriaged"
on:
issues:
types: [opened]
jobs:
add_label:
runs-on: ubuntu-latest
steps:
- uses: actions-ecosystem/action-add-labels@v1
with:
labels: "Status: Untriaged"

View File

@@ -2,6 +2,7 @@ using Content.Client.Eui;
using Content.Shared.Administration.Notes;
using Content.Shared.Eui;
using JetBrains.Annotations;
using Robust.Client.UserInterface.Controls;
using static Content.Shared.Administration.Notes.AdminMessageEuiMsg;
namespace Content.Client.Administration.UI.AdminRemarks;
@@ -14,9 +15,8 @@ public sealed class AdminMessageEui : BaseEui
public AdminMessageEui()
{
_popup = new AdminMessagePopupWindow();
_popup.OnAcceptPressed += () => SendMessage(new Accept());
_popup.OnDismissPressed += () => SendMessage(new Dismiss());
_popup.OnClose += () => SendMessage(new CloseEuiMessage());
_popup.OnAcceptPressed += () => SendMessage(new Dismiss(true));
_popup.OnDismissPressed += () => SendMessage(new Dismiss(false));
}
public override void HandleState(EuiStateBase state)
@@ -26,13 +26,17 @@ public sealed class AdminMessageEui : BaseEui
return;
}
_popup.SetMessage(s.Message);
_popup.SetDetails(s.AdminName, s.AddedOn);
_popup.Timer = s.Time;
_popup.SetState(s);
}
public override void Opened()
{
_popup.OpenCentered();
_popup.UserInterfaceManager.WindowRoot.AddChild(_popup);
LayoutContainer.SetAnchorPreset(_popup, LayoutContainer.LayoutPreset.Wide);
}
public override void Closed()
{
_popup.Orphan();
}
}

View File

@@ -0,0 +1,6 @@
<Control xmlns="https://spacestation14.io" Margin="0 0 0 8">
<BoxContainer Orientation="Vertical">
<RichTextLabel Name="Admin" Margin="0 0 0 4" />
<RichTextLabel Name="Message" Margin="2 0 0 0" />
</BoxContainer>
</Control>

View File

@@ -0,0 +1,23 @@
using Content.Shared.Administration.Notes;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;
namespace Content.Client.Administration.UI.AdminRemarks;
[GenerateTypedNameReferences]
public sealed partial class AdminMessagePopupMessage : Control
{
public AdminMessagePopupMessage(AdminMessageEuiState.Message message)
{
RobustXamlLoader.Load(this);
Admin.SetMessage(FormattedMessage.FromMarkup(Loc.GetString(
"admin-notes-message-admin",
("admin", message.AdminName),
("date", message.AddedOn.ToLocalTime()))));
Message.SetMessage(message.Text);
}
}

View File

@@ -1,22 +1,36 @@
<ui:FancyWindow xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
VerticalExpand="True" HorizontalExpand="True"
Title="{Loc admin-notes-message-window-title}"
MinSize="600 170">
<PanelContainer VerticalExpand="True" HorizontalExpand="True" StyleClasses="BackgroundDark">
<ScrollContainer HScrollEnabled="False" VerticalExpand="True" HorizontalExpand="True" Margin="4">
<BoxContainer Orientation="Vertical" SeparationOverride="10" VerticalAlignment="Bottom">
<Label Name="AdminLabel" Text="Loading..." />
<RichTextLabel Name="MessageLabel" />
<Control xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client">
<PanelContainer MouseFilter="Stop">
<PanelContainer.PanelOverride>
<!-- semi-transparent background -->
<gfx:StyleBoxFlat BackgroundColor="#000000AA" />
</PanelContainer.PanelOverride>
<Control HorizontalAlignment="Center" VerticalAlignment="Center" MaxWidth="600">
<PanelContainer StyleClasses="AngleRect" />
<BoxContainer Orientation="Vertical" Margin="4">
<RichTextLabel Name="Description" />
<!-- Contains actual messages -->
<ScrollContainer HScrollEnabled="False" Margin="4" VerticalExpand="True" ReturnMeasure="True" MaxHeight="400">
<BoxContainer Orientation="Vertical" Name="MessageContainer" Margin="0 2 0 0" />
</ScrollContainer>
<Label Name="WaitLabel" />
<BoxContainer Orientation="Horizontal">
<Button Name="DismissButton"
Text="{Loc 'admin-notes-message-dismiss'}" />
Text="{Loc 'admin-notes-message-dismiss'}"
Disabled="True"
HorizontalExpand="True"
StyleClasses="OpenRight" />
<Button Name="AcceptButton"
Text="{Loc 'admin-notes-message-accept'}"
Disabled="True" />
Disabled="True"
HorizontalExpand="True"
StyleClasses="OpenLeft" />
</BoxContainer>
</BoxContainer>
</ScrollContainer>
</Control>
</PanelContainer>
</ui:FancyWindow>
</Control>

View File

@@ -1,56 +1,65 @@
using Content.Client.UserInterface.Controls;
using Content.Client.Stylesheets;
using Content.Shared.Administration.Notes;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.Administration.UI.AdminRemarks;
[GenerateTypedNameReferences]
public sealed partial class AdminMessagePopupWindow : FancyWindow
public sealed partial class AdminMessagePopupWindow : Control
{
private float _timer = float.MaxValue;
public float Timer
{
get => _timer;
set
{
WaitLabel.Text = Loc.GetString("admin-notes-message-wait", ("time", MathF.Floor(value)));
_timer = value;
}
}
public event Action? OnDismissPressed;
public event Action? OnAcceptPressed;
public AdminMessagePopupWindow()
{
RobustXamlLoader.Load(this);
Stylesheet = IoCManager.Resolve<IStylesheetManager>().SheetSpace;
AcceptButton.OnPressed += OnAcceptButtonPressed;
DismissButton.OnPressed += OnDismissButtonPressed;
}
public void SetMessage(string message)
public float Timer
{
MessageLabel.SetMessage(message);
get => _timer;
private set
{
WaitLabel.Text = Loc.GetString("admin-notes-message-wait", ("time", MathF.Floor(value)));
_timer = value;
}
}
public void SetDetails(string adminName, DateTime addedOn)
public void SetState(AdminMessageEuiState state)
{
AdminLabel.Text = Loc.GetString("admin-notes-message-admin", ("admin", adminName), ("date", addedOn));
Timer = (float) state.Time.TotalSeconds;
MessageContainer.RemoveAllChildren();
foreach (var message in state.Messages)
{
MessageContainer.AddChild(new AdminMessagePopupMessage(message));
}
Description.SetMessage(FormattedMessage.FromMarkup(Loc.GetString("admin-notes-message-desc", ("count", state.Messages.Length))));
}
private void OnDismissButtonPressed(BaseButton.ButtonEventArgs obj)
{
OnDismissPressed?.Invoke();
Close();
}
private void OnAcceptButtonPressed(BaseButton.ButtonEventArgs obj)
{
OnAcceptPressed?.Invoke();
Close();
}
protected override void FrameUpdate(FrameEventArgs args)
@@ -70,6 +79,7 @@ public sealed partial class AdminMessagePopupWindow : FancyWindow
else
{
AcceptButton.Disabled = false;
DismissButton.Disabled = false;
}
}
}

View File

@@ -256,6 +256,8 @@ public sealed class AtmosDebugOverlay : Overlay
handle.DrawString(_font, pos, $"Map: {data.MapAtmosphere}");
pos += offset;
handle.DrawString(_font, pos, $"NoGrid: {data.NoGrid}");
pos += offset;
handle.DrawString(_font, pos, $"Immutable: {data.Immutable}");
}
private void GetGrids(MapId mapId, Box2Rotated box)

View File

@@ -8,7 +8,6 @@ using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.Graphics;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
@@ -23,7 +22,7 @@ namespace Content.Client.Atmos.Overlays
private readonly IEntityManager _entManager;
private readonly IMapManager _mapManager;
public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities;
public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities | OverlaySpace.WorldSpaceBelowWorld;
private readonly ShaderInstance _shader;
// Gas overlays
@@ -79,7 +78,8 @@ namespace Content.Client.Atmos.Overlays
var rsi = resourceCache.GetResource<RSIResource>(animated.RsiPath).RSI;
var stateId = animated.RsiState;
if (!rsi.TryGetState(stateId, out var state)) continue;
if (!rsi.TryGetState(stateId, out var state))
continue;
_frames[i] = state.GetFrames(RsiDirection.South);
_frameDelays[i] = state.GetDelays();
@@ -111,7 +111,8 @@ namespace Content.Client.Atmos.Overlays
for (var i = 0; i < _gasCount; i++)
{
var delays = _frameDelays[i];
if (delays.Length == 0) continue;
if (delays.Length == 0)
continue;
var frameCount = _frameCounter[i];
_timer[i] += args.DeltaSeconds;
@@ -127,7 +128,8 @@ namespace Content.Client.Atmos.Overlays
for (var i = 0; i < FireStates; i++)
{
var delays = _fireFrameDelays[i];
if (delays.Length == 0) continue;
if (delays.Length == 0)
continue;
var frameCount = _fireFrameCounter[i];
_fireTimer[i] += args.DeltaSeconds;
@@ -161,26 +163,10 @@ namespace Content.Client.Atmos.Overlays
var mapUid = _mapManager.GetMapEntityId(args.MapId);
if (_entManager.TryGetComponent<MapAtmosphereComponent>(mapUid, out var atmos))
{
var bottomLeft = args.WorldAABB.BottomLeft.Floored();
var topRight = args.WorldAABB.TopRight.Ceiled();
DrawMapOverlay(drawHandle, args, mapUid, atmos);
for (var x = bottomLeft.X; x <= topRight.X; x++)
{
for (var y = bottomLeft.Y; y <= topRight.Y; y++)
{
var tilePosition = new Vector2(x, y);
for (var i = 0; i < atmos.OverlayData.Opacity.Length; i++)
{
var opacity = atmos.OverlayData.Opacity[i];
if (opacity > 0)
args.WorldHandle.DrawTexture(_frames[i][_frameCounter[i]], tilePosition, Color.White.WithAlpha(opacity));
}
}
}
}
if (args.Space != OverlaySpace.WorldSpaceEntities)
return;
// TODO: WorldBounds callback.
_mapManager.FindGridsIntersecting(args.MapId, args.WorldAABB, ref gridState,
@@ -265,5 +251,41 @@ namespace Content.Client.Atmos.Overlays
drawHandle.UseShader(null);
drawHandle.SetTransform(Matrix3.Identity);
}
private void DrawMapOverlay(
DrawingHandleWorld handle,
OverlayDrawArgs args,
EntityUid map,
MapAtmosphereComponent atmos)
{
var mapGrid = _entManager.HasComponent<MapGridComponent>(map);
// map-grid atmospheres get drawn above grids
if (mapGrid && args.Space != OverlaySpace.WorldSpaceEntities)
return;
// Normal map atmospheres get drawn below grids
if (!mapGrid && args.Space != OverlaySpace.WorldSpaceBelowWorld)
return;
var bottomLeft = args.WorldAABB.BottomLeft.Floored();
var topRight = args.WorldAABB.TopRight.Ceiled();
for (var x = bottomLeft.X; x <= topRight.X; x++)
{
for (var y = bottomLeft.Y; y <= topRight.Y; y++)
{
var tilePosition = new Vector2(x, y);
for (var i = 0; i < atmos.OverlayData.Opacity.Length; i++)
{
var opacity = atmos.OverlayData.Opacity[i];
if (opacity > 0)
handle.DrawTexture(_frames[i][_frameCounter[i]], tilePosition, Color.White.WithAlpha(opacity));
}
}
}
}
}
}

View File

@@ -27,6 +27,7 @@ namespace Content.Client.Construction
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
private readonly Dictionary<int, EntityUid> _ghosts = new();
@@ -195,7 +196,7 @@ namespace Content.Client.Construction
return false;
// This InRangeUnobstructed should probably be replaced with "is there something blocking us in that tile?"
var predicate = GetPredicate(prototype.CanBuildInImpassable, loc.ToMap(EntityManager));
var predicate = GetPredicate(prototype.CanBuildInImpassable, loc.ToMap(EntityManager, _transformSystem));
if (!_interactionSystem.InRangeUnobstructed(user, loc, 20f, predicate: predicate))
return false;

View File

@@ -170,7 +170,7 @@ namespace Content.Client.ContextMenu.UI
if (_combatMode.IsInCombatMode(args.Session?.AttachedEntity))
return false;
var coords = args.Coordinates.ToMap(_entityManager);
var coords = args.Coordinates.ToMap(_entityManager, _xform);
if (_verbSystem.TryGetEntityMenuEntities(coords, out var entities))
OpenRootMenu(entities);

View File

@@ -22,6 +22,9 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
private const string AnimationKey = "disposal_unit_animation";
private const string DefaultFlushState = "disposal-flush";
private const string DefaultChargeState = "disposal-charging";
public override void Initialize()
{
base.Initialize();
@@ -101,12 +104,18 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
sprite.LayerSetVisible(DisposalUnitVisualLayers.Base, state == VisualState.Anchored);
sprite.LayerSetVisible(DisposalUnitVisualLayers.BaseFlush, state is VisualState.Flushing or VisualState.Charging);
var chargingState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.BaseCharging, out var chargingLayer)
? sprite.LayerGetState(chargingLayer)
: new RSI.StateId(DefaultChargeState);
// This is a transient state so not too worried about replaying in range.
if (state == VisualState.Flushing)
{
if (!_animationSystem.HasRunningAnimation(uid, AnimationKey))
{
var flushState = new RSI.StateId("disposal-flush");
var flushState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.BaseFlush, out var flushLayer)
? sprite.LayerGetState(flushLayer)
: new RSI.StateId(DefaultFlushState);
// Setup the flush animation to play
var anim = new Animation
@@ -124,7 +133,7 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
// Return to base state (though, depending on how the unit is
// configured we might get an appearance change event telling
// us to go to charging state)
new AnimationTrackSpriteFlick.KeyFrame("disposal-charging", (float) unit.FlushDelay.TotalSeconds)
new AnimationTrackSpriteFlick.KeyFrame(chargingState, (float) unit.FlushDelay.TotalSeconds)
}
},
}
@@ -147,7 +156,7 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
}
else if (state == VisualState.Charging)
{
sprite.LayerSetState(DisposalUnitVisualLayers.BaseFlush, new RSI.StateId("disposal-charging"));
sprite.LayerSetState(DisposalUnitVisualLayers.BaseFlush, chargingState);
}
else
{

View File

@@ -104,7 +104,7 @@ namespace Content.Client.Gameplay
public IEnumerable<EntityUid> GetClickableEntities(EntityCoordinates coordinates)
{
return GetClickableEntities(coordinates.ToMap(_entityManager));
return GetClickableEntities(coordinates.ToMap(_entityManager, _entitySystemManager.GetEntitySystem<SharedTransformSystem>()));
}
public IEnumerable<EntityUid> GetClickableEntities(MapCoordinates coordinates)

View File

@@ -127,17 +127,16 @@ public sealed partial class MarkingPicker : Control
IoCManager.InjectDependencies(this);
IoCManager.Instance!.TryResolveType(out _sponsorsManager); // Corvax-Sponsors
SetupCategoryButtons();
CMarkingCategoryButton.OnItemSelected += OnCategoryChange;
CMarkingsUnused.OnItemSelected += item =>
_selectedUnusedMarking = CMarkingsUnused[item.ItemIndex];
CMarkingAdd.OnPressed += args =>
CMarkingAdd.OnPressed += _ =>
MarkingAdd();
CMarkingsUsed.OnItemSelected += OnUsedMarkingSelected;
CMarkingRemove.OnPressed += args =>
CMarkingRemove.OnPressed += _ =>
MarkingRemove();
CMarkingRankUp.OnPressed += _ => SwapMarkingUp();
@@ -149,16 +148,34 @@ public sealed partial class MarkingPicker : Control
private void SetupCategoryButtons()
{
CMarkingCategoryButton.Clear();
var validCategories = new List<MarkingCategories>();
for (var i = 0; i < _markingCategories.Count; i++)
{
if (_ignoreCategories.Contains(_markingCategories[i]))
var category = _markingCategories[i];
var markings = GetMarkings(category);
if (_ignoreCategories.Contains(category) ||
markings.Count == 0)
{
continue;
}
CMarkingCategoryButton.AddItem(Loc.GetString($"markings-category-{_markingCategories[i].ToString()}"), i);
validCategories.Add(category);
CMarkingCategoryButton.AddItem(Loc.GetString($"markings-category-{category.ToString()}"), i);
}
if (validCategories.Contains(_selectedMarkingCategory))
{
CMarkingCategoryButton.SelectId(_markingCategories.IndexOf(_selectedMarkingCategory));
}
else if (validCategories.Count > 0)
{
_selectedMarkingCategory = validCategories[0];
}
else
{
_selectedMarkingCategory = MarkingCategories.Chest;
}
CMarkingCategoryButton.SelectId(_markingCategories.IndexOf(_selectedMarkingCategory));
}
private string GetMarkingName(MarkingPrototype marking) => Loc.GetString($"marking-{marking.ID}");
@@ -182,16 +199,21 @@ public sealed partial class MarkingPicker : Control
return result;
}
private IReadOnlyDictionary<string, MarkingPrototype> GetMarkings(MarkingCategories category)
{
return IgnoreSpecies
? _markingManager.MarkingsByCategoryAndSex(category, _currentSex)
: _markingManager.MarkingsByCategoryAndSpeciesAndSex(category, _currentSpecies, _currentSex);
}
public void Populate(string filter)
{
SetupCategoryButtons();
CMarkingsUnused.Clear();
_selectedUnusedMarking = null;
var markings = IgnoreSpecies
? _markingManager.MarkingsByCategoryAndSex(_selectedMarkingCategory, _currentSex)
: _markingManager.MarkingsByCategoryAndSpeciesAndSex(_selectedMarkingCategory, _currentSpecies, _currentSex);
var sortedMarkings = markings.Values.Where(m =>
var sortedMarkings = GetMarkings(_selectedMarkingCategory).Values.Where(m =>
m.ID.ToLower().Contains(filter.ToLower()) ||
GetMarkingName(m).ToLower().Contains(filter.ToLower())
).OrderBy(p => Loc.GetString(GetMarkingName(p)));

View File

@@ -47,7 +47,7 @@ namespace Content.Client.IconSmoothing
var xform = Transform(uid);
if (xform.Anchored)
{
component.LastPosition = _mapManager.TryGetGrid(xform.GridUid, out var grid)
component.LastPosition = TryComp<MapGridComponent>(xform.GridUid, out var grid)
? (xform.GridUid.Value, grid.TileIndicesFor(xform.Coordinates))
: (null, new Vector2i(0, 0));
@@ -134,7 +134,7 @@ namespace Content.Client.IconSmoothing
Vector2i pos;
if (transform.Anchored && _mapManager.TryGetGrid(transform.GridUid, out var grid))
if (transform.Anchored && TryComp<MapGridComponent>(transform.GridUid, out var grid))
{
pos = grid.CoordinatesToTile(transform.Coordinates);
}
@@ -144,7 +144,7 @@ namespace Content.Client.IconSmoothing
if (comp.LastPosition is not (EntityUid gridId, Vector2i oldPos))
return;
if (!_mapManager.TryGetGrid(gridId, out grid))
if (!TryComp(gridId, out grid))
return;
pos = oldPos;
@@ -206,7 +206,7 @@ namespace Content.Client.IconSmoothing
{
var directions = DirectionFlag.None;
if (_mapManager.TryGetGrid(xform.GridUid, out grid))
if (TryComp(xform.GridUid, out grid))
{
var pos = grid.TileIndicesFor(xform.Coordinates);
@@ -240,7 +240,7 @@ namespace Content.Client.IconSmoothing
if (xform.Anchored)
{
if (!_mapManager.TryGetGrid(xform.GridUid, out grid))
if (!TryComp(xform.GridUid, out grid))
{
Log.Error($"Failed to calculate IconSmoothComponent sprite in {uid} because grid {xform.GridUid} was missing.");
return;

View File

@@ -83,7 +83,7 @@ public sealed partial class MappingSystem : EntitySystem
if (tileDef is not ContentTileDefinition contentTileDef)
return;
var tileIcon = contentTileDef.IsSpace
var tileIcon = contentTileDef.MapAtmosphere
? _spaceIcon
: new Texture(contentTileDef.Sprite!.Value);

View File

@@ -4,6 +4,7 @@ using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Robust.Client.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components;
using Robust.Shared.Timing;
@@ -12,7 +13,6 @@ namespace Content.Client.Movement.Systems;
public sealed class JetpackSystem : SharedJetpackSystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly ClothingSystem _clothing = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
@@ -75,7 +75,7 @@ public sealed class JetpackSystem : SharedJetpackSystem
var coordinates = uidXform.Coordinates;
var gridUid = coordinates.GetGridUid(EntityManager);
if (_mapManager.TryGetGrid(gridUid, out var grid))
if (TryComp<MapGridComponent>(gridUid, out var grid))
{
coordinates = new EntityCoordinates(gridUid.Value, grid.WorldToLocal(coordinates.ToMapPos(EntityManager, _transform)));
}

View File

@@ -23,6 +23,7 @@ namespace Content.Client.NPC
[Dependency] private readonly IResourceCache _cache = default!;
[Dependency] private readonly NPCSteeringSystem _steering = default!;
[Dependency] private readonly MapSystem _mapSystem = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
public PathfindingDebugMode Modes
{
@@ -39,7 +40,7 @@ namespace Content.Client.NPC
}
else if (!overlayManager.HasOverlay<PathfindingOverlay>())
{
overlayManager.AddOverlay(new PathfindingOverlay(EntityManager, _eyeManager, _inputManager, _mapManager, _cache, this, _mapSystem));
overlayManager.AddOverlay(new PathfindingOverlay(EntityManager, _eyeManager, _inputManager, _mapManager, _cache, this, _mapSystem, _transformSystem));
}
if ((value & PathfindingDebugMode.Steering) != 0x0)
@@ -140,6 +141,7 @@ namespace Content.Client.NPC
private readonly IMapManager _mapManager;
private readonly PathfindingSystem _system;
private readonly MapSystem _mapSystem;
private readonly SharedTransformSystem _transformSystem;
public override OverlaySpace Space => OverlaySpace.ScreenSpace | OverlaySpace.WorldSpace;
@@ -153,7 +155,8 @@ namespace Content.Client.NPC
IMapManager mapManager,
IResourceCache cache,
PathfindingSystem system,
MapSystem mapSystem)
MapSystem mapSystem,
SharedTransformSystem transformSystem)
{
_entManager = entManager;
_eyeManager = eyeManager;
@@ -161,6 +164,7 @@ namespace Content.Client.NPC
_mapManager = mapManager;
_system = system;
_mapSystem = mapSystem;
_transformSystem = transformSystem;
_font = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
}
@@ -480,7 +484,7 @@ namespace Content.Client.NPC
if (neighborPoly.NetEntity != poly.GraphUid)
{
color = Color.Green;
var neighborMap = _entManager.GetCoordinates(neighborPoly).ToMap(_entManager);
var neighborMap = _entManager.GetCoordinates(neighborPoly).ToMap(_entManager, _transformSystem);
if (neighborMap.MapId != args.MapId)
continue;

View File

@@ -80,7 +80,7 @@ namespace Content.Client.NodeContainer
var xform = _entityManager.GetComponent<TransformComponent>(_entityManager.GetEntity(node.Entity));
if (!_mapManager.TryGetGrid(xform.GridUid, out var grid))
if (!_entityManager.TryGetComponent<MapGridComponent>(xform.GridUid, out var grid))
return;
var gridTile = grid.TileIndicesFor(xform.Coordinates);
@@ -145,7 +145,7 @@ namespace Content.Client.NodeContainer
foreach (var (gridId, gridDict) in _gridIndex)
{
var grid = _mapManager.GetGrid(gridId);
var grid = _entityManager.GetComponent<MapGridComponent>(gridId);
var (_, _, worldMatrix, invMatrix) = _entityManager.GetComponent<TransformComponent>(gridId).GetWorldPositionRotationMatrixWithInv();
var lCursorBox = invMatrix.TransformBox(cursorBox);

View File

@@ -403,7 +403,7 @@ namespace Content.Client.Options.UI.Tabs
Mod1 = mods[0],
Mod2 = mods[1],
Mod3 = mods[2],
Priority = 0,
Priority = _currentlyRebinding.Binding?.Priority ?? 0,
Type = bindType,
CanFocus = key == Keyboard.Key.MouseLeft
|| key == Keyboard.Key.MouseRight

View File

@@ -22,7 +22,7 @@ public sealed class PopupOverlay : Overlay
private readonly PopupSystem _popup;
private readonly PopupUIController _controller;
private readonly ExamineSystemShared _examine;
private readonly SharedTransformSystem _transform;
private readonly ShaderInstance _shader;
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
@@ -35,6 +35,7 @@ public sealed class PopupOverlay : Overlay
IUserInterfaceManager uiManager,
PopupUIController controller,
ExamineSystemShared examine,
SharedTransformSystem transform,
PopupSystem popup)
{
_configManager = configManager;
@@ -42,6 +43,7 @@ public sealed class PopupOverlay : Overlay
_playerMgr = playerMgr;
_uiManager = uiManager;
_examine = examine;
_transform = transform;
_popup = popup;
_controller = controller;
@@ -76,7 +78,7 @@ public sealed class PopupOverlay : Overlay
foreach (var popup in _popup.WorldLabels)
{
var mapPos = popup.InitialPos.ToMap(_entManager);
var mapPos = popup.InitialPos.ToMap(_entManager, _transform);
if (mapPos.MapId != args.MapId)
continue;

View File

@@ -28,6 +28,7 @@ namespace Content.Client.Popups
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
[Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
[Dependency] private readonly ExamineSystemShared _examine = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public IReadOnlyList<WorldPopupLabel> WorldLabels => _aliveWorldLabels;
public IReadOnlyList<CursorPopupLabel> CursorLabels => _aliveCursorLabels;
@@ -54,6 +55,7 @@ namespace Content.Client.Popups
_uiManager,
_uiManager.GetUIController<PopupUIController>(),
_examine,
_transform,
this));
}

View File

@@ -4,13 +4,12 @@ using Content.Client.Radiation.Systems;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
namespace Content.Client.Radiation.Overlays;
public sealed class RadiationDebugOverlay : Overlay
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
private readonly RadiationSystem _radiation;
@@ -63,7 +62,7 @@ public sealed class RadiationDebugOverlay : Overlay
{
var gridUid = _entityManager.GetEntity(netGrid);
if (!_mapManager.TryGetGrid(gridUid, out var grid))
if (!_entityManager.TryGetComponent<MapGridComponent>(gridUid, out var grid))
continue;
foreach (var (tile, rads) in blockers)
@@ -88,7 +87,7 @@ public sealed class RadiationDebugOverlay : Overlay
{
var gridUid = _entityManager.GetEntity(netGrid);
if (!_mapManager.TryGetGrid(gridUid, out var grid))
if (!_entityManager.TryGetComponent<MapGridComponent>(gridUid, out var grid))
continue;
if (query.TryGetComponent(gridUid, out var trs) && trs.MapID != args.MapId)
continue;
@@ -127,7 +126,7 @@ public sealed class RadiationDebugOverlay : Overlay
{
var gridUid = _entityManager.GetEntity(netGrid);
if (!_mapManager.TryGetGrid(gridUid, out var grid))
if (!_entityManager.TryGetComponent<MapGridComponent>(gridUid, out var grid))
continue;
var (destTile, _) = blockers.Last();
var destWorld = grid.GridTileToWorldPos(destTile);

View File

@@ -121,7 +121,10 @@ namespace Content.Client.Radiation.Overlays
private bool PulseQualifies(EntityUid pulseEntity, MapCoordinates currentEyeLoc)
{
return _entityManager.GetComponent<TransformComponent>(pulseEntity).MapID == currentEyeLoc.MapId && _entityManager.GetComponent<TransformComponent>(pulseEntity).Coordinates.InRange(_entityManager, EntityCoordinates.FromMap(_entityManager, _entityManager.GetComponent<TransformComponent>(pulseEntity).ParentUid, currentEyeLoc), MaxDist);
var transformComponent = _entityManager.GetComponent<TransformComponent>(pulseEntity);
var transformSystem = _entityManager.System<SharedTransformSystem>();
return transformComponent.MapID == currentEyeLoc.MapId
&& transformComponent.Coordinates.InRange(_entityManager, transformSystem, EntityCoordinates.FromMap(transformComponent.ParentUid, currentEyeLoc, transformSystem, _entityManager), MaxDist);
}
private sealed record RadiationShaderInstance(MapCoordinates CurrentMapCoords, float Range, TimeSpan Start, float Duration)

View File

@@ -0,0 +1,16 @@
using Content.Client.Remote.UI;
using Content.Client.Items;
using Content.Shared.Remotes.EntitySystems;
using Content.Shared.Remotes.Components;
namespace Content.Client.Remotes.EntitySystems;
public sealed class DoorRemoteSystem : SharedDoorRemoteSystem
{
public override void Initialize()
{
base.Initialize();
Subs.ItemStatus<DoorRemoteComponent>(ent => new DoorRemoteStatusControl(ent));
}
}

View File

@@ -0,0 +1,46 @@
using Content.Client.Message;
using Content.Client.Stylesheets;
using Content.Shared.Remotes.Components;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Timing;
namespace Content.Client.Remote.UI;
public sealed class DoorRemoteStatusControl : Control
{
private readonly Entity<DoorRemoteComponent> _entity;
private readonly RichTextLabel _label;
// set to toggle bolts initially just so that it updates on first pickup of remote
private OperatingMode PrevOperatingMode = OperatingMode.placeholderForUiUpdates;
public DoorRemoteStatusControl(Entity<DoorRemoteComponent> entity)
{
_entity = entity;
_label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
AddChild(_label);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
// only updates the UI if any of the details are different than they previously were
if (PrevOperatingMode == _entity.Comp.Mode)
return;
PrevOperatingMode = _entity.Comp.Mode;
// Update current volume and injector state
var modeStringLocalized = Loc.GetString(_entity.Comp.Mode switch
{
OperatingMode.OpenClose => "door-remote-open-close-text",
OperatingMode.ToggleBolts => "door-remote-toggle-bolt-text",
OperatingMode.ToggleEmergencyAccess => "door-remote-emergency-access-text",
_ => "door-remote-invalid-text"
});
_label.SetMarkup(Loc.GetString("door-remote-mode-label", ("modeString", modeStringLocalized)));
}
}

View File

@@ -16,6 +16,7 @@ namespace Content.Client.Sandbox
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly IPlacementManager _placement = default!;
[Dependency] private readonly ContentEyeSystem _contentEye = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
private bool _sandboxEnabled;
public bool SandboxAllowed { get; private set; }
@@ -108,7 +109,7 @@ namespace Content.Client.Sandbox
}
// Try copy tile.
if (!_map.TryFindGridAt(coords.ToMap(EntityManager), out _, out var grid) || !grid.TryGetTileRef(coords, out var tileRef))
if (!_map.TryFindGridAt(coords.ToMap(EntityManager, _transform), out _, out var grid) || !grid.TryGetTileRef(coords, out var tileRef))
return false;
if (_placement.Eraser)

View File

@@ -66,11 +66,11 @@ public partial class BaseShuttleControl : MapGridControl
protected void DrawData(DrawingHandleScreen handle, string text)
{
var coordsDimensions = handle.GetDimensions(Font, text, UIScale);
var coordsDimensions = handle.GetDimensions(Font, text, 1f);
const float coordsMargins = 5f;
handle.DrawString(Font,
new Vector2(coordsMargins, Height) - new Vector2(0f, coordsDimensions.Y + coordsMargins),
new Vector2(coordsMargins, PixelHeight) - new Vector2(0f, coordsDimensions.Y + coordsMargins),
text,
Color.FromSrgb(IFFComponent.SelfColor));
}

View File

@@ -116,7 +116,7 @@ public sealed partial class ShuttleMapControl : BaseShuttleControl
var mapTransform = Matrix3.CreateInverseTransform(Offset, Angle.Zero);
if (beaconsOnly && TryGetBeacon(_beacons, mapTransform, args.RelativePosition, PixelRect, out var foundBeacon, out _))
if (beaconsOnly && TryGetBeacon(_beacons, mapTransform, args.RelativePixelPosition, PixelRect, out var foundBeacon, out _))
{
RequestBeaconFTL?.Invoke(foundBeacon.Entity, _ftlAngle);
}
@@ -206,7 +206,8 @@ public sealed partial class ShuttleMapControl : BaseShuttleControl
private List<IMapObject> GetViewportMapObjects(Matrix3 matty, List<IMapObject> mapObjects)
{
var results = new List<IMapObject>();
var viewBox = SizeBox.Scale(1.2f);
var enlargement = new Vector2i((int) (16 * UIScale), (int) (16 * UIScale));
var viewBox = new UIBox2i(Vector2i.Zero - enlargement, PixelSize + enlargement);
foreach (var mapObj in mapObjects)
{
@@ -398,8 +399,8 @@ public sealed partial class ShuttleMapControl : BaseShuttleControl
foreach (var (gridUiPos, iffText) in sendStrings)
{
var textWidth = handle.GetDimensions(_font, iffText, UIScale);
handle.DrawString(_font, gridUiPos + textWidth with { X = -textWidth.X / 2f }, iffText, adjustedColor);
var textWidth = handle.GetDimensions(_font, iffText, 1f);
handle.DrawString(_font, gridUiPos + textWidth with { X = -textWidth.X / 2f, Y = textWidth.Y * UIScale }, iffText, adjustedColor);
}
}
@@ -587,7 +588,7 @@ public sealed partial class ShuttleMapControl : BaseShuttleControl
var distance = (localPos - mousePos).Length();
if (distance > BeaconSnapRange ||
if (distance > BeaconSnapRange * UIScale ||
distance > nearestValue)
{
continue;

View File

@@ -42,11 +42,12 @@ public class ActionButtonContainer : GridContainer
{
var button = new ActionButton(_entity);
if (keys.TryGetValue(index, out var boundKey))
{
button.KeyBind = boundKey;
if (!keys.TryGetValue(index, out var boundKey))
return button;
var binding = _input.GetKeyBinding(boundKey);
button.KeyBind = boundKey;
if (_input.TryGetKeyBinding(boundKey, out var binding))
{
button.Label.Text = binding.GetKeyString();
}

View File

@@ -128,7 +128,7 @@ namespace Content.IntegrationTests.Tests.Body
metaSys.Update(1.0f);
metaSys.Update(1.0f);
respSys.Update(2.0f);
Assert.That(GetMapMoles(), Is.EqualTo(startingMoles).Within(0.0001));
Assert.That(GetMapMoles(), Is.EqualTo(startingMoles).Within(0.0002));
});
}

View File

@@ -72,7 +72,7 @@ public sealed class FluidSpill
var puddleOrigin = new Vector2i(0, 0);
await server.WaitAssertion(() =>
{
var grid = mapManager.GetGrid(gridId);
var grid = entityManager.GetComponent<MapGridComponent>(gridId);
var solution = new Solution("Blood", FixedPoint2.New(100));
var tileRef = grid.GetTileRef(puddleOrigin);
#pragma warning disable NUnit2045 // Interdependent tests
@@ -86,7 +86,7 @@ public sealed class FluidSpill
await server.WaitAssertion(() =>
{
var grid = mapManager.GetGrid(gridId);
var grid = entityManager.GetComponent<MapGridComponent>(gridId);
var puddle = GetPuddleEntity(entityManager, grid, puddleOrigin);
#pragma warning disable NUnit2045 // Interdependent tests

View File

@@ -1006,15 +1006,10 @@ public abstract partial class InteractionTest
await Server.WaitPost(() =>
{
var atmosSystem = SEntMan.System<AtmosphereSystem>();
var atmos = SEntMan.EnsureComponent<MapAtmosphereComponent>(target);
var moles = new float[Atmospherics.AdjustedNumberOfGases];
moles[(int) Gas.Oxygen] = 21.824779f;
moles[(int) Gas.Nitrogen] = 82.10312f;
atmosSystem.SetMapAtmosphere(target, false, new GasMixture(2500)
{
Temperature = 293.15f,
Moles = moles,
}, atmos);
atmosSystem.SetMapAtmosphere(target, false, new GasMixture(moles, Atmospherics.T20C));
});
}

View File

@@ -14,7 +14,7 @@ public static class ClientPackaging
/// <summary>
/// Be advised this can be called from server packaging during a HybridACZ build.
/// </summary>
public static async Task PackageClient(bool skipBuild, IPackageLogger logger)
public static async Task PackageClient(bool skipBuild, string configuration, IPackageLogger logger)
{
logger.Info("Building client...");
@@ -27,7 +27,7 @@ public static class ClientPackaging
{
"build",
Path.Combine("Content.Client", "Content.Client.csproj"),
"-c", "Release",
"-c", configuration,
"--nologo",
"/v:m",
"/t:Rebuild",

View File

@@ -31,6 +31,11 @@ public sealed class CommandLineArgs
/// </summary>
public bool HybridAcz { get; set; }
/// <summary>
/// Configuration used for when packaging the server. (Release, Debug, Tools)
/// </summary>
public string Configuration { get; set; }
// CommandLineArgs, 3rd of her name.
public static bool TryParse(IReadOnlyList<string> args, [NotNullWhen(true)] out CommandLineArgs? parsed)
{
@@ -39,6 +44,7 @@ public sealed class CommandLineArgs
var skipBuild = false;
var wipeRelease = true;
var hybridAcz = false;
var configuration = "Release";
List<string>? platforms = null;
using var enumerator = args.GetEnumerator();
@@ -89,6 +95,16 @@ public sealed class CommandLineArgs
platforms ??= new List<string>();
platforms.Add(enumerator.Current);
}
else if (arg == "--configuration")
{
if (!enumerator.MoveNext())
{
Console.WriteLine("No configuration provided");
return false;
}
configuration = enumerator.Current;
}
else if (arg == "--help")
{
PrintHelp();
@@ -106,7 +122,7 @@ public sealed class CommandLineArgs
return false;
}
parsed = new CommandLineArgs(client.Value, skipBuild, wipeRelease, hybridAcz, platforms);
parsed = new CommandLineArgs(client.Value, skipBuild, wipeRelease, hybridAcz, platforms, configuration);
return true;
}
@@ -120,6 +136,7 @@ Options:
--no-wipe-release Don't wipe the release folder before creating files.
--hybrid-acz Use HybridACZ for server builds.
--platform Platform for server builds. Default will output several x64 targets.
--configuration Configuration to use for building the server (Release, Debug, Tools). Default is Release.
");
}
@@ -128,12 +145,14 @@ Options:
bool skipBuild,
bool wipeRelease,
bool hybridAcz,
List<string>? platforms)
List<string>? platforms,
string configuration)
{
Client = client;
SkipBuild = skipBuild;
WipeRelease = wipeRelease;
HybridAcz = hybridAcz;
Platforms = platforms;
Configuration = configuration;
}
}

View File

@@ -17,11 +17,11 @@ if (!parsed.SkipBuild)
if (parsed.Client)
{
await ClientPackaging.PackageClient(parsed.SkipBuild, logger);
await ClientPackaging.PackageClient(parsed.SkipBuild, parsed.Configuration, logger);
}
else
{
await ServerPackaging.PackageServer(parsed.SkipBuild, parsed.HybridAcz, logger, parsed.Platforms);
await ServerPackaging.PackageServer(parsed.SkipBuild, parsed.HybridAcz, logger, parsed.Configuration, parsed.Platforms);
}
void WipeBin()

View File

@@ -74,8 +74,7 @@ public static class ServerPackaging
};
private static readonly bool UseSecrets = File.Exists(Path.Combine("Secrets", "CorvaxSecrets.sln")); // Corvax-Secrets
public static async Task PackageServer(bool skipBuild, bool hybridAcz, IPackageLogger logger, List<string>? platforms = null)
public static async Task PackageServer(bool skipBuild, bool hybridAcz, IPackageLogger logger, string configuration, List<string>? platforms = null)
{
if (platforms == null)
{
@@ -88,7 +87,7 @@ public static class ServerPackaging
// Rather than hosting the client ZIP on the watchdog or on a separate server,
// Hybrid ACZ uses the ACZ hosting functionality to host it as part of the status host,
// which means that features such as automatic UPnP forwarding still work properly.
await ClientPackaging.PackageClient(skipBuild, logger);
await ClientPackaging.PackageClient(skipBuild, configuration, logger);
}
// Good variable naming right here.
@@ -97,13 +96,13 @@ public static class ServerPackaging
if (!platforms.Contains(platform.Rid))
continue;
await BuildPlatform(platform, skipBuild, hybridAcz, logger);
await BuildPlatform(platform, skipBuild, hybridAcz, configuration, logger);
}
}
private static async Task BuildPlatform(PlatformReg platform, bool skipBuild, bool hybridAcz, IPackageLogger logger)
private static async Task BuildPlatform(PlatformReg platform, bool skipBuild, bool hybridAcz, string configuration, IPackageLogger logger)
{
logger.Info($"Building project for {platform}...");
logger.Info($"Building project for {platform.TargetOs}...");
if (!skipBuild)
{
@@ -114,7 +113,7 @@ public static class ServerPackaging
{
"build",
Path.Combine("Content.Server", "Content.Server.csproj"),
"-c", "Release",
"-c", configuration,
"--nologo",
"/v:m",
$"/p:TargetOs={platform.TargetOs}",
@@ -146,7 +145,7 @@ public static class ServerPackaging
}
// Corvax-Secrets-End
await PublishClientServer(platform.Rid, platform.TargetOs);
await PublishClientServer(platform.Rid, platform.TargetOs, configuration);
}
logger.Info($"Packaging {platform.Rid} server...");
@@ -165,7 +164,7 @@ public static class ServerPackaging
logger.Info($"Finished packaging server in {sw.Elapsed}");
}
private static async Task PublishClientServer(string runtime, string targetOs)
private static async Task PublishClientServer(string runtime, string targetOs, string configuration)
{
await ProcessHelpers.RunCheck(new ProcessStartInfo
{
@@ -175,7 +174,7 @@ public static class ServerPackaging
"publish",
"--runtime", runtime,
"--no-self-contained",
"-c", "Release",
"-c", configuration,
$"/p:TargetOs={targetOs}",
"/p:FullRelease=True",
"/m",

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class AdminMessageDismiss : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "dismissed",
table: "admin_messages",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.Sql("UPDATE admin_messages SET dismissed = seen;");
migrationBuilder.AddCheckConstraint(
name: "NotDismissedAndSeen",
table: "admin_messages",
sql: "NOT dismissed OR seen");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropCheckConstraint(
name: "NotDismissedAndSeen",
table: "admin_messages");
migrationBuilder.DropColumn(
name: "dismissed",
table: "admin_messages");
}
}
}

View File

@@ -183,6 +183,10 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("uuid")
.HasColumnName("deleted_by_id");
b.Property<bool>("Dismissed")
.HasColumnType("boolean")
.HasColumnName("dismissed");
b.Property<DateTime?>("ExpirationTime")
.HasColumnType("timestamp with time zone")
.HasColumnName("expiration_time");
@@ -232,7 +236,10 @@ namespace Content.Server.Database.Migrations.Postgres
b.HasIndex("RoundId")
.HasDatabaseName("IX_admin_messages_round_id");
b.ToTable("admin_messages", (string)null);
b.ToTable("admin_messages", null, t =>
{
t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen");
});
});
modelBuilder.Entity("Content.Server.Database.AdminNote", b =>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class AdminMessageDismiss : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "dismissed",
table: "admin_messages",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.Sql("UPDATE admin_messages SET dismissed = seen;");
migrationBuilder.AddCheckConstraint(
name: "NotDismissedAndSeen",
table: "admin_messages",
sql: "NOT dismissed OR seen");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropCheckConstraint(
name: "NotDismissedAndSeen",
table: "admin_messages");
migrationBuilder.DropColumn(
name: "dismissed",
table: "admin_messages");
}
}
}

View File

@@ -166,6 +166,10 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("TEXT")
.HasColumnName("deleted_by_id");
b.Property<bool>("Dismissed")
.HasColumnType("INTEGER")
.HasColumnName("dismissed");
b.Property<DateTime?>("ExpirationTime")
.HasColumnType("TEXT")
.HasColumnName("expiration_time");
@@ -215,7 +219,10 @@ namespace Content.Server.Database.Migrations.Sqlite
b.HasIndex("RoundId")
.HasDatabaseName("IX_admin_messages_round_id");
b.ToTable("admin_messages", (string)null);
b.ToTable("admin_messages", null, t =>
{
t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen");
});
});
modelBuilder.Entity("Content.Server.Database.AdminNote", b =>

View File

@@ -268,6 +268,11 @@ namespace Content.Server.Database
.HasPrincipalKey(author => author.UserId)
.OnDelete(DeleteBehavior.SetNull);
// A message cannot be "dismissed" without also being "seen".
modelBuilder.Entity<AdminMessage>().ToTable(t =>
t.HasCheckConstraint("NotDismissedAndSeen",
"NOT dismissed OR seen"));
modelBuilder.Entity<ServerBan>()
.HasOne(ban => ban.CreatedBy)
.WithMany(author => author.AdminServerBansCreated)
@@ -970,6 +975,15 @@ namespace Content.Server.Database
[ForeignKey("DeletedBy")] public Guid? DeletedById { get; set; }
public Player? DeletedBy { get; set; }
public DateTime? DeletedAt { get; set; }
/// <summary>
/// Whether the message has been seen at least once by the player.
/// </summary>
public bool Seen { get; set; }
/// <summary>
/// Whether the message has been dismissed permanently by the player.
/// </summary>
public bool Dismissed { get; set; }
}
}

View File

@@ -19,6 +19,16 @@ public sealed class AdminWhoCommand : IConsoleCommand
var adminMgr = IoCManager.Resolve<IAdminManager>();
var afk = IoCManager.Resolve<IAfkManager>();
var seeStealth = true;
// If null it (hopefully) means it is being called from the console.
if (shell.Player != null)
{
var playerData = adminMgr.GetAdminData(shell.Player);
seeStealth = playerData != null && playerData.CanStealth();
}
var sb = new StringBuilder();
var first = true;
foreach (var admin in adminMgr.ActiveAdmins)
@@ -30,10 +40,16 @@ public sealed class AdminWhoCommand : IConsoleCommand
var adminData = adminMgr.GetAdminData(admin)!;
DebugTools.AssertNotNull(adminData);
if (adminData.Stealth && !seeStealth)
continue;
sb.Append(admin.Name);
if (adminData.Title is { } title)
sb.Append($": [{title}]");
if (adminData.Stealth)
sb.Append(" (S)");
if (shell.Player is { } player && adminMgr.HasAdminFlag(player, AdminFlags.Admin))
{
if (afk.IsAfk(admin))

View File

@@ -6,6 +6,7 @@ using Robust.Shared.Audio;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.Administration.Commands;
@@ -14,6 +15,7 @@ public sealed class PlayGlobalSoundCommand : IConsoleCommand
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IResourceManager _res = default!;
public string Command => "playglobalsound";
@@ -95,7 +97,7 @@ public sealed class PlayGlobalSoundCommand : IConsoleCommand
{
var hint = Loc.GetString("play-global-sound-command-arg-path");
var options = CompletionHelper.ContentFilePath(args[0], _res);
var options = CompletionHelper.AudioFilePath(args[0], _protoManager, _res);
return CompletionResult.FromHintOptions(options, hint);
}

View File

@@ -0,0 +1,39 @@
using Content.Server.Administration.Managers;
using Content.Shared.Administration;
using JetBrains.Annotations;
using Robust.Shared.Console;
using Robust.Shared.Utility;
namespace Content.Server.Administration.Commands;
[UsedImplicitly]
[AdminCommand(AdminFlags.Stealth)]
public sealed class StealthminCommand : LocalizedCommands
{
public override string Command => "stealthmin";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var player = shell.Player;
if (player == null)
{
shell.WriteLine(Loc.GetString("cmd-stealthmin-no-console"));
return;
}
var mgr = IoCManager.Resolve<IAdminManager>();
var adminData = mgr.GetAdminData(player);
DebugTools.AssertNotNull(adminData);
if (!adminData!.Stealth)
{
mgr.Stealth(player);
}
else
{
mgr.UnStealth(player);
}
}
}

View File

@@ -0,0 +1,98 @@
using System.Diagnostics.Metrics;
using System.Runtime.InteropServices;
using Content.Server.Afk;
using Robust.Server.DataMetrics;
namespace Content.Server.Administration.Managers;
// Handles metrics reporting for active admin count and such.
public sealed partial class AdminManager
{
private Dictionary<int, (int active, int afk, int deadminned)>? _adminOnlineCounts;
private const int SentinelRankId = -1;
[Dependency] private readonly IMetricsManager _metrics = default!;
[Dependency] private readonly IAfkManager _afkManager = default!;
[Dependency] private readonly IMeterFactory _meterFactory = default!;
private void InitializeMetrics()
{
_metrics.UpdateMetrics += MetricsOnUpdateMetrics;
var meter = _meterFactory.Create("SS14.AdminManager");
meter.CreateObservableGauge(
"admins_online_count",
MeasureAdminCount,
null,
"The count of online admins");
}
private void MetricsOnUpdateMetrics()
{
_sawmill.Verbose("Updating metrics");
var dict = new Dictionary<int, (int active, int afk, int deadminned)>();
foreach (var (session, reg) in _admins)
{
var rankId = reg.RankId ?? SentinelRankId;
ref var counts = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, rankId, out _);
if (reg.Data.Active)
{
if (_afkManager.IsAfk(session))
counts.afk += 1;
else
counts.active += 1;
}
else
{
counts.deadminned += 1;
}
}
// Neither prometheus-net nor dotnet-counters seem to handle stuff well if we STOP returning measurements.
// i.e. if the last admin with a rank disconnects.
// So if we have EVER reported a rank, always keep reporting it.
if (_adminOnlineCounts != null)
{
foreach (var rank in _adminOnlineCounts.Keys)
{
CollectionsMarshal.GetValueRefOrAddDefault(dict, rank, out _);
}
}
// Make sure "no rank" is always available. Avoid "no data".
CollectionsMarshal.GetValueRefOrAddDefault(dict, SentinelRankId, out _);
_adminOnlineCounts = dict;
}
private IEnumerable<Measurement<int>> MeasureAdminCount()
{
if (_adminOnlineCounts == null)
yield break;
foreach (var (rank, (active, afk, deadminned)) in _adminOnlineCounts)
{
yield return new Measurement<int>(
active,
new KeyValuePair<string, object?>("state", "active"),
new KeyValuePair<string, object?>("rank", rank == SentinelRankId ? "none" : rank.ToString()));
yield return new Measurement<int>(
afk,
new KeyValuePair<string, object?>("state", "afk"),
new KeyValuePair<string, object?>("rank", rank == SentinelRankId ? "none" : rank.ToString()));
yield return new Measurement<int>(
deadminned,
new KeyValuePair<string, object?>("state", "deadminned"),
new KeyValuePair<string, object?>("rank", rank == SentinelRankId ? "none" : rank.ToString()));
}
}
}

View File

@@ -23,7 +23,7 @@ using Robust.Shared.Utility;
namespace Content.Server.Administration.Managers
{
public sealed class AdminManager : IAdminManager, IPostInjectInit, IConGroupControllerImplementation
public sealed partial class AdminManager : IAdminManager, IPostInjectInit, IConGroupControllerImplementation
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IServerDbManager _dbManager = default!;
@@ -34,6 +34,7 @@ namespace Content.Server.Administration.Managers
[Dependency] private readonly IServerConsoleHost _consoleHost = default!;
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly ToolshedManager _toolshed = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private readonly Dictionary<ICommonSession, AdminReg> _admins = new();
private readonly HashSet<NetUserId> _promotedPlayers = new();
@@ -49,6 +50,8 @@ namespace Content.Server.Administration.Managers
private readonly AdminCommandPermissions _commandPermissions = new();
private readonly AdminCommandPermissions _toolshedCommandPermissions = new();
private ISawmill _sawmill = default!;
public bool IsAdmin(ICommonSession session, bool includeDeAdmin = false)
{
return GetAdminData(session, includeDeAdmin) != null;
@@ -95,6 +98,44 @@ namespace Content.Server.Administration.Managers
UpdateAdminStatus(session);
}
public void Stealth(ICommonSession session)
{
if (!_admins.TryGetValue(session, out var reg))
{
throw new ArgumentException($"Player {session} is not an admin");
}
if (reg.Data.Stealth)
return;
var playerData = session.ContentData()!;
playerData.Stealthed = true;
reg.Data.Stealth = true;
_chat.DispatchServerMessage(session, Loc.GetString("admin-manager-stealthed-message"));
_chat.SendAdminAnnouncement(Loc.GetString("admin-manager-self-de-admin-message", ("exAdminName", session.Name)), AdminFlags.Stealth);
_chat.SendAdminAnnouncement(Loc.GetString("admin-manager-self-enable-stealth", ("stealthAdminName", session.Name)), flagWhitelist: AdminFlags.Stealth);
}
public void UnStealth(ICommonSession session)
{
if (!_admins.TryGetValue(session, out var reg))
{
throw new ArgumentException($"Player {session} is not an admin");
}
if (!reg.Data.Stealth)
return;
var playerData = session.ContentData()!;
playerData.Stealthed = false;
reg.Data.Stealth = false;
_chat.DispatchServerMessage(session, Loc.GetString("admin-manager-unstealthed-message"));
_chat.SendAdminAnnouncement(Loc.GetString("admin-manager-self-re-admin-message", ("newAdminName", session.Name)), flagBlacklist: AdminFlags.Stealth);
_chat.SendAdminAnnouncement(Loc.GetString("admin-manager-self-disable-stealth", ("exStealthAdminName", session.Name)), flagWhitelist: AdminFlags.Stealth);
}
public void ReAdmin(ICommonSession session)
{
if (!_admins.TryGetValue(session, out var reg))
@@ -113,7 +154,16 @@ namespace Content.Server.Administration.Managers
plyData.ExplicitlyDeadminned = false;
reg.Data.Active = true;
_chat.SendAdminAnnouncement(Loc.GetString("admin-manager-self-re-admin-message", ("newAdminName", session.Name)));
if (reg.Data.Stealth)
{
_chat.SendAdminAnnouncement(Loc.GetString("admin-manager-self-re-admin-message", ("newAdminName", session.Name)));
}
else
{
_chat.DispatchServerMessage(session, Loc.GetString("admin-manager-stealthed-message"));
_chat.SendAdminAnnouncement(Loc.GetString("admin-manager-self-re-admin-message",
("newAdminName", session.Name)), flagWhitelist: AdminFlags.Stealth);
}
SendPermsChangedEvent(session);
UpdateAdminStatus(session);
@@ -165,6 +215,11 @@ namespace Content.Server.Administration.Managers
_chat.DispatchServerMessage(player, Loc.GetString("admin-manager-admin-permissions-updated-message"));
}
if (player.ContentData()!.Stealthed)
{
aData.Stealth = true;
}
}
SendPermsChangedEvent(player);
@@ -181,6 +236,8 @@ namespace Content.Server.Administration.Managers
public void Initialize()
{
_sawmill = _logManager.GetSawmill("admin");
_netMgr.RegisterNetMessage<MsgUpdateAdminStatus>();
// Cache permissions for loaded console commands with the requisite attributes.
@@ -234,6 +291,8 @@ namespace Content.Server.Administration.Managers
}
_toolshed.ActivePermissionController = this;
InitializeMetrics();
}
public void PromoteHost(ICommonSession player)
@@ -283,9 +342,19 @@ namespace Content.Server.Administration.Managers
}
else if (e.NewStatus == SessionStatus.Disconnected)
{
if (_admins.Remove(e.Session) && _cfg.GetCVar(CCVars.AdminAnnounceLogout))
if (_admins.Remove(e.Session, out var reg ) && _cfg.GetCVar(CCVars.AdminAnnounceLogout))
{
_chat.SendAdminAnnouncement(Loc.GetString("admin-manager-admin-logout-message", ("name", e.Session.Name)));
if (reg.Data.Stealth)
{
_chat.SendAdminAnnouncement(Loc.GetString("admin-manager-admin-logout-message",
("name", e.Session.Name)), flagWhitelist: AdminFlags.Stealth);
}
else
{
_chat.SendAdminAnnouncement(Loc.GetString("admin-manager-admin-logout-message",
("name", e.Session.Name)));
}
}
}
}
@@ -308,13 +377,27 @@ namespace Content.Server.Administration.Managers
_admins.Add(session, reg);
if (session.ContentData()!.Stealthed)
reg.Data.Stealth = true;
if (!session.ContentData()!.ExplicitlyDeadminned)
{
reg.Data.Active = true;
if (_cfg.GetCVar(CCVars.AdminAnnounceLogin))
{
_chat.SendAdminAnnouncement(Loc.GetString("admin-manager-admin-login-message", ("name", session.Name)));
if (reg.Data.Stealth)
{
_chat.DispatchServerMessage(session, Loc.GetString("admin-manager-stealthed-message"));
_chat.SendAdminAnnouncement(Loc.GetString("admin-manager-admin-login-message",
("name", session.Name)), flagWhitelist: AdminFlags.Stealth);
}
else
{
_chat.SendAdminAnnouncement(Loc.GetString("admin-manager-admin-login-message",
("name", session.Name)));
}
}
SendPermsChangedEvent(session);

View File

@@ -41,6 +41,16 @@ namespace Content.Server.Administration.Managers
/// </summary>
void ReAdmin(ICommonSession session);
/// <summary>
/// Make admin hidden from adminwho.
/// </summary>
void Stealth(ICommonSession session);
/// <summary>
/// Unhide admin from adminwho.
/// </summary>
void UnStealth(ICommonSession session);
/// <summary>
/// Re-loads the permissions of an player in case their admin data changed DB-side.
/// </summary>

View File

@@ -1,9 +1,11 @@
using System.Linq;
using Content.Server.Database;
using Content.Server.EUI;
using Content.Shared.Administration.Notes;
using Content.Shared.CCVar;
using Content.Shared.Eui;
using Robust.Shared.Configuration;
using Robust.Shared.Timing;
using static Content.Shared.Administration.Notes.AdminMessageEuiMsg;
namespace Content.Server.Administration.Notes;
@@ -12,32 +14,33 @@ public sealed class AdminMessageEui : BaseEui
{
[Dependency] private readonly IAdminNotesManager _notesMan = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private readonly float _closeWait;
private AdminMessageRecord? _message;
private DateTime _startTime;
[Dependency] private readonly IGameTiming _gameTiming = default!;
public AdminMessageEui()
private readonly TimeSpan _closeWait;
private readonly TimeSpan _endTime;
private readonly AdminMessageRecord[] _messages;
public AdminMessageEui(AdminMessageRecord[] messages)
{
IoCManager.InjectDependencies(this);
_closeWait = _cfg.GetCVar(CCVars.MessageWaitTime);
_closeWait = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.MessageWaitTime));
_endTime = _gameTiming.RealTime + _closeWait;
_messages = messages;
}
public void SetMessage(AdminMessageRecord message)
public override void Opened()
{
_message = message;
_startTime = DateTime.UtcNow;
StateDirty();
}
public override EuiStateBase GetNewState()
{
if (_message == null)
return new AdminMessageEuiState(float.MaxValue, "An error has occurred.", string.Empty, DateTime.MinValue);
return new AdminMessageEuiState(
_closeWait,
_message.Message,
_message.CreatedBy?.LastSeenUserName ?? "[System]",
_message.CreatedAt.UtcDateTime
_messages.Select(x => new AdminMessageEuiState.Message(
x.Message,
x.CreatedBy?.LastSeenUserName ?? Loc.GetString("admin-notes-fallback-admin-name"),
x.CreatedAt.UtcDateTime)).ToArray()
);
}
@@ -47,15 +50,14 @@ public sealed class AdminMessageEui : BaseEui
switch (msg)
{
case Accept:
if (_message == null)
break;
// No escape
if (DateTime.UtcNow - _startTime >= TimeSpan.FromSeconds(_closeWait))
await _notesMan.MarkMessageAsSeen(_message.Id);
Close();
break;
case Dismiss:
case Dismiss dismiss:
if (_gameTiming.RealTime < _endTime)
return;
foreach (var message in _messages)
{
await _notesMan.MarkMessageAsSeen(message.Id, dismiss.Permanent);
}
Close();
break;
}

View File

@@ -331,9 +331,9 @@ public sealed class AdminNotesManager : IAdminNotesManager, IPostInjectInit
return await _db.GetMessages(player);
}
public async Task MarkMessageAsSeen(int id)
public async Task MarkMessageAsSeen(int id, bool dismissedToo)
{
await _db.MarkMessageAsSeen(id);
await _db.MarkMessageAsSeen(id, dismissedToo);
}
public void PostInject()

View File

@@ -1,3 +1,4 @@
using System.Linq;
using Content.Server.Administration.Commands;
using Content.Server.Chat.Managers;
using Content.Server.EUI;
@@ -52,7 +53,7 @@ public sealed class AdminNotesSystem : EntitySystem
private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.NewStatus != SessionStatus.Connected)
if (e.NewStatus != SessionStatus.InGame)
return;
var messages = await _notes.GetNewMessages(e.Session.UserId);
@@ -69,19 +70,11 @@ public sealed class AdminNotesSystem : EntitySystem
_chat.SendAdminAlert(Loc.GetString("admin-notes-watchlist", ("player", username), ("message", watchlist.Message)));
}
foreach (var message in messages)
{
var messageString = Loc.GetString("admin-notes-new-message", ("admin", message.CreatedBy?.LastSeenUserName ?? "[System]"), ("message", message.Message));
// Only open the popup if the user hasn't seen it yet
if (!message.Seen)
{
var ui = new AdminMessageEui();
_euis.OpenEui(ui, e.Session);
ui.SetMessage(message);
var messagesToShow = messages.OrderBy(x => x.CreatedAt).Where(x => !x.Dismissed).ToArray();
if (messagesToShow.Length == 0)
return;
// Only send the message if they haven't seen it yet
_chat.DispatchServerMessage(e.Session, messageString);
}
}
var ui = new AdminMessageEui(messagesToShow);
_euis.OpenEui(ui, e.Session);
}
}

View File

@@ -45,5 +45,13 @@ public interface IAdminNotesManager
/// <param name="player">Desired player's <see cref="Guid"/></param>
/// <returns>All unread messages</returns>
Task<List<AdminMessageRecord>> GetNewMessages(Guid player);
Task MarkMessageAsSeen(int id);
/// <summary>
/// Mark an admin message as being seen by the target player.
/// </summary>
/// <param name="id">The database ID of the admin message.</param>
/// <param name="dismissedToo">
/// If true, the message is "permanently dismissed" and will not be shown to the player again when they join.
/// </param>
Task MarkMessageAsSeen(int id, bool dismissedToo);
}

View File

@@ -409,7 +409,7 @@ public sealed partial class AdminVerbSystem
var fixtures = Comp<FixturesComponent>(args.Target);
xform.Anchored = false; // Just in case.
_physics.SetBodyType(args.Target, BodyType.Dynamic, manager: fixtures, body: physics);
_physics.SetBodyStatus(physics, BodyStatus.InAir);
_physics.SetBodyStatus(args.Target, physics, BodyStatus.InAir);
_physics.WakeBody(args.Target, manager: fixtures, body: physics);
foreach (var fixture in fixtures.Fixtures.Values)
@@ -424,8 +424,8 @@ public sealed partial class AdminVerbSystem
_physics.SetLinearVelocity(args.Target, _random.NextVector2(1.5f, 1.5f), manager: fixtures, body: physics);
_physics.SetAngularVelocity(args.Target, MathF.PI * 12, manager: fixtures, body: physics);
_physics.SetLinearDamping(physics, 0f);
_physics.SetAngularDamping(physics, 0f);
_physics.SetLinearDamping(args.Target, physics, 0f);
_physics.SetAngularDamping(args.Target, physics, 0f);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-pinball-description")
@@ -444,7 +444,7 @@ public sealed partial class AdminVerbSystem
xform.Anchored = false; // Just in case.
_physics.SetBodyType(args.Target, BodyType.Dynamic, body: physics);
_physics.SetBodyStatus(physics, BodyStatus.InAir);
_physics.SetBodyStatus(args.Target, physics, BodyStatus.InAir);
_physics.WakeBody(args.Target, manager: fixtures, body: physics);
foreach (var fixture in fixtures.Fixtures.Values)
@@ -454,8 +454,8 @@ public sealed partial class AdminVerbSystem
_physics.SetLinearVelocity(args.Target, _random.NextVector2(8.0f, 8.0f), manager: fixtures, body: physics);
_physics.SetAngularVelocity(args.Target, MathF.PI * 12, manager: fixtures, body: physics);
_physics.SetLinearDamping(physics, 0f);
_physics.SetAngularDamping(physics, 0f);
_physics.SetLinearDamping(args.Target, physics, 0f);
_physics.SetAngularDamping(args.Target, physics, 0f);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-yeet-description")

View File

@@ -718,9 +718,21 @@ public sealed partial class AdminVerbSystem
Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Fun/caps.rsi"), "mag-6"),
Act = () =>
{
_quickDialog.OpenDialog(player, "Set Bullet Amount", $"Amount (max {ballisticAmmo.Capacity}):", (int amount) =>
_quickDialog.OpenDialog(player, "Set Bullet Amount", $"Amount (standard {ballisticAmmo.Capacity}):", (string amount) =>
{
ballisticAmmo.UnspawnedCount = amount;
if (!int.TryParse(amount, out var result))
return;
if (result > 0)
{
ballisticAmmo.UnspawnedCount = result;
}
else
{
ballisticAmmo.UnspawnedCount = 0;
}
_gun.UpdateBallisticAppearance(args.Target, ballisticAmmo);
});
},
Impact = LogImpact.Medium,

View File

@@ -24,7 +24,7 @@ namespace Content.Server.Anomaly;
/// </summary>
public sealed partial class AnomalySystem
{
[Dependency] private readonly MapSystem _mapSystem = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
private void InitializeGenerator()

View File

@@ -2,11 +2,10 @@ using Content.Server.Atmos.EntitySystems;
using Content.Server.Anomaly.Components;
using Content.Shared.Anomaly.Components;
using Content.Shared.Atmos;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Random;
using System.Linq;
using System.Numerics;
using Robust.Shared.Map.Components;
namespace Content.Server.Anomaly.Effects;
@@ -16,8 +15,6 @@ namespace Content.Server.Anomaly.Effects;
public sealed class GasProducerAnomalySystem : EntitySystem
{
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
[Dependency] private readonly TransformSystem _xform = default!;
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly IRobustRandom _random = default!;
public override void Initialize()
@@ -55,7 +52,7 @@ public sealed class GasProducerAnomalySystem : EntitySystem
{
var xform = Transform(uid);
if (!_map.TryGetGrid(xform.GridUid, out var grid))
if (!TryComp<MapGridComponent>(xform.GridUid, out var grid))
return;
var localpos = xform.Coordinates.Position;

View File

@@ -3,7 +3,7 @@ using Content.Server.Atmos.EntitySystems;
using Content.Shared.Administration;
using Content.Shared.Atmos;
using Robust.Shared.Console;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
namespace Content.Server.Atmos.Commands
{
@@ -11,7 +11,6 @@ namespace Content.Server.Atmos.Commands
public sealed class DeleteGasCommand : IConsoleCommand
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
public string Command => "deletegas";
public string Description => "Removes all gases from a grid, or just of one type if specified.";
@@ -119,7 +118,7 @@ namespace Content.Server.Atmos.Commands
return;
}
if (!_mapManager.TryGetGrid(gridId, out _))
if (!_entManager.TryGetComponent<MapGridComponent>(gridId, out _))
{
shell.WriteLine($"No grid exists with id {gridId}");
return;

View File

@@ -0,0 +1,94 @@
using Content.Server.Administration;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Administration;
using Content.Shared.Atmos;
using Robust.Shared.Console;
using Robust.Shared.Map;
namespace Content.Server.Atmos.Commands;
[AdminCommand(AdminFlags.Admin)]
public sealed class AddMapAtmosCommand : LocalizedCommands
{
[Dependency] private readonly IEntityManager _entities = default!;
[Dependency] private readonly IMapManager _map = default!;
private const string _cmd = "cmd-set-map-atmos";
public override string Command => "setmapatmos";
public override string Description => Loc.GetString($"{_cmd}-desc");
public override string Help => Loc.GetString($"{_cmd}-help");
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 2)
{
shell.WriteLine(Help);
return;
}
int.TryParse(args[0], out var id);
var map = _map.GetMapEntityId(new MapId(id));
if (!map.IsValid())
{
shell.WriteError(Loc.GetString("cmd-parse-failure-mapid", ("arg", args[0])));
return;
}
if (!bool.TryParse(args[1], out var space))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-bool", ("arg", args[1])));
return;
}
if (space || args.Length < 4)
{
_entities.RemoveComponent<MapAtmosphereComponent>(map);
shell.WriteLine(Loc.GetString($"{_cmd}-removed", ("map", id)));
return;
}
if (!float.TryParse(args[2], out var temp))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[2])));
return;
}
var mix = new GasMixture(Atmospherics.CellVolume) {Temperature = Math.Min(temp, Atmospherics.TCMB)};
for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
{
if (args.Length == 3 + i)
break;
if (!float.TryParse(args[3+i], out var moles))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[3+i])));
return;
}
mix.AdjustMoles(i, moles);
}
var atmos = _entities.EntitySysManager.GetEntitySystem<AtmosphereSystem>();
atmos.SetMapAtmosphere(map, space, mix);
shell.WriteLine(Loc.GetString($"{_cmd}-updated", ("map", id)));
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
return CompletionResult.FromHintOptions(CompletionHelper.MapIds(_entities), Loc.GetString($"{_cmd}-hint-map"));
if (args.Length == 2)
return CompletionResult.FromHintOptions(new[]{ "false", "true"}, Loc.GetString($"{_cmd}-hint-space"));
if (!bool.TryParse(args[1], out var space) || space)
return CompletionResult.Empty;
if (args.Length == 3)
return CompletionResult.FromHint(Loc.GetString($"{_cmd}-hint-temp"));
var gas = (Gas) args.Length - 4;
return CompletionResult.FromHint(Loc.GetString($"{_cmd}-hint-gas" , ("gas", gas.ToString())));
}
}

View File

@@ -1,34 +1,62 @@
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Atmos.Components
{
[RegisterComponent]
[RegisterComponent, Access(typeof(AirtightSystem))]
public sealed partial class AirtightComponent : Component
{
public (EntityUid Grid, Vector2i Tile) LastPosition { get; set; }
/// <summary>
/// The directions in which this entity should block airflow, relative to its own reference frame.
/// </summary>
[DataField("airBlockedDirection", customTypeSerializer: typeof(FlagSerializer<AtmosDirectionFlags>))]
public int InitialAirBlockedDirection { get; set; } = (int) AtmosDirection.All;
/// <summary>
/// The directions in which the entity is currently blocking airflow, relative to the grid that the entity is on.
/// I.e., this is a variant of <see cref="InitialAirBlockedDirection"/> that takes into account the entity's
/// current rotation.
/// </summary>
[ViewVariables]
public int CurrentAirBlockedDirection;
[DataField("airBlocked")]
/// <summary>
/// Whether the airtight entity is currently blocking airflow.
/// </summary>
[DataField]
public bool AirBlocked { get; set; } = true;
[DataField("fixVacuum")]
/// <summary>
/// If true, entities on this tile will attempt to draw air from surrounding tiles when they become unblocked
/// and currently have no air. This is generally only required when <see cref="NoAirWhenFullyAirBlocked"/> is
/// true, or if the entity is likely to occupy the same tile as another no-air airtight entity.
/// </summary>
[DataField]
public bool FixVacuum { get; set; } = true;
// I think fixvacuum exists to ensure that repeatedly closing/opening air-blocking doors doesn't end up
// depressurizing a room. However it can also effectively be used as a means of generating gasses for free
// TODO ATMOS Mass conservation. Make it actually push/pull air from adjacent tiles instead of destroying & creating,
// TODO ATMOS Do we need these two fields?
[DataField("rotateAirBlocked")]
public bool RotateAirBlocked { get; set; } = true;
// TODO ATMOS remove this? What is this even for??
[DataField("fixAirBlockedDirectionInitialize")]
public bool FixAirBlockedDirectionInitialize { get; set; } = true;
[DataField("noAirWhenFullyAirBlocked")]
/// <summary>
/// If true, then the tile that this entity is on will have no air at all if all directions are blocked.
/// </summary>
[DataField]
public bool NoAirWhenFullyAirBlocked { get; set; } = true;
/// <inheritdoc cref="CurrentAirBlockedDirection"/>
[Access(Other = AccessPermissions.ReadWriteExecute)]
public AtmosDirection AirBlockedDirection => (AtmosDirection)CurrentAirBlockedDirection;
}
}

View File

@@ -28,6 +28,9 @@ namespace Content.Server.Atmos.Components
[IncludeDataField(customTypeSerializer:typeof(TileAtmosCollectionSerializer))]
public Dictionary<Vector2i, TileAtmosphere> Tiles = new(1000);
[ViewVariables]
public HashSet<TileAtmosphere> MapTiles = new(1000);
[ViewVariables]
public readonly HashSet<TileAtmosphere> ActiveTiles = new(1000);
@@ -80,7 +83,10 @@ namespace Content.Server.Atmos.Components
public readonly HashSet<Vector2i> InvalidatedCoords = new(1000);
[ViewVariables]
public readonly Queue<Vector2i> CurrentRunInvalidatedCoordinates = new();
public readonly Queue<TileAtmosphere> CurrentRunInvalidatedTiles = new();
[ViewVariables]
public readonly List<TileAtmosphere> PossiblyDisconnectedTiles = new(100);
[ViewVariables]
public int InvalidatedCoordsCount => InvalidatedCoords.Count;

View File

@@ -12,12 +12,14 @@ public sealed partial class MapAtmosphereComponent : SharedMapAtmosphereComponen
/// <summary>
/// The default GasMixture a map will have. Space mixture by default.
/// </summary>
[DataField("mixture"), ViewVariables(VVAccess.ReadWrite)]
public GasMixture? Mixture = GasMixture.SpaceGas;
[DataField, ViewVariables(VVAccess.ReadWrite)]
public GasMixture Mixture = GasMixture.SpaceGas;
/// <summary>
/// Whether empty tiles will be considered space or not.
/// </summary>
[DataField("space"), ViewVariables(VVAccess.ReadWrite)]
[DataField, ViewVariables(VVAccess.ReadWrite)]
public bool Space = true;
public SharedGasTileOverlaySystem.GasOverlayData Overlay;
}

View File

@@ -2,7 +2,6 @@ using Content.Server.Atmos.Components;
using Content.Server.Explosion.EntitySystems;
using Content.Shared.Atmos;
using JetBrains.Annotations;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
namespace Content.Server.Atmos.EntitySystems
@@ -10,7 +9,7 @@ namespace Content.Server.Atmos.EntitySystems
[UsedImplicitly]
public sealed class AirtightSystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly ExplosionSystem _explosionSystem = default!;
@@ -86,8 +85,6 @@ namespace Content.Server.Atmos.EntitySystems
private bool AirtightMove(Entity<AirtightComponent> ent, ref MoveEvent ev)
{
var (owner, airtight) = ent;
if (!airtight.RotateAirBlocked || airtight.InitialAirBlockedDirection == (int)AtmosDirection.Invalid)
return false;
airtight.CurrentAirBlockedDirection = (int) Rotate((AtmosDirection)airtight.InitialAirBlockedDirection, ev.NewRotation);
var pos = airtight.LastPosition;
@@ -121,19 +118,16 @@ namespace Content.Server.Atmos.EntitySystems
if (!xform.Anchored || !TryComp(xform.GridUid, out MapGridComponent? grid))
return;
airtight.LastPosition = (xform.GridUid.Value, grid.TileIndicesFor(xform.Coordinates));
InvalidatePosition(airtight.LastPosition.Item1, airtight.LastPosition.Item2, airtight.FixVacuum && !airtight.AirBlocked);
var indices = _transform.GetGridTilePositionOrDefault((ent, xform), grid);
airtight.LastPosition = (xform.GridUid.Value, indices);
InvalidatePosition((xform.GridUid.Value, grid), indices);
}
public void InvalidatePosition(EntityUid gridId, Vector2i pos, bool fixVacuum = false)
public void InvalidatePosition(Entity<MapGridComponent?> grid, Vector2i pos)
{
if (!TryComp(gridId, out MapGridComponent? grid))
return;
var query = EntityManager.GetEntityQuery<AirtightComponent>();
_explosionSystem.UpdateAirtightMap(gridId, pos, grid, query);
// TODO make atmos system use query
_atmosphereSystem.InvalidateTile(gridId, pos);
_explosionSystem.UpdateAirtightMap(grid, pos, grid, query);
_atmosphereSystem.InvalidateTile(grid.Owner, pos);
}
private AtmosDirection Rotate(AtmosDirection myDirection, Angle myAngle)

View File

@@ -97,22 +97,20 @@ namespace Content.Server.Atmos.EntitySystems
}
}
private AtmosDebugOverlayData ConvertTileToData(TileAtmosphere? tile)
private AtmosDebugOverlayData? ConvertTileToData(TileAtmosphere tile)
{
if (tile == null)
return default;
return new AtmosDebugOverlayData(
tile.GridIndices,
tile.Air?.Temperature ?? default,
tile.Air?.Moles,
tile.PressureDirection,
tile.LastPressureDirection,
tile.BlockedAirflow,
tile.AirtightData.BlockedDirections,
tile.ExcitedGroup?.GetHashCode(),
tile.Space,
false,
false);
tile.MapAtmosphere,
tile.NoGridTile,
tile.Air?.Immutable ?? false);
}
public override void Update(float frameTime)

View File

@@ -1,37 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Atmos.Components;
using Robust.Shared.Map;
using Robust.Shared.Map.Enumerators;
namespace Content.Server.Atmos.EntitySystems;
public struct AtmosObstructionEnumerator
{
private AnchoredEntitiesEnumerator _enumerator;
private EntityQuery<AirtightComponent> _query;
public AtmosObstructionEnumerator(AnchoredEntitiesEnumerator enumerator, EntityQuery<AirtightComponent> query)
{
_enumerator = enumerator;
_query = query;
}
public bool MoveNext([NotNullWhen(true)] out AirtightComponent? airtight)
{
if (!_enumerator.MoveNext(out var uid))
{
airtight = null;
return false;
}
// No rider, it makes it uglier.
// ReSharper disable once ConvertIfStatementToReturnStatement
if (!_query.TryGetComponent(uid.Value, out airtight))
{
// ReSharper disable once TailRecursiveCall
return MoveNext(out airtight);
}
return true;
}
}

View File

@@ -85,10 +85,10 @@ public partial class AtmosphereSystem
return ev.Mixtures!;
}
public void InvalidateTile(EntityUid gridUid, Vector2i tile)
public void InvalidateTile(Entity<GridAtmosphereComponent?> entity, Vector2i tile)
{
var ev = new InvalidateTileMethodEvent(gridUid, tile);
RaiseLocalEvent(gridUid, ref ev);
if (_atmosQuery.Resolve(entity.Owner, ref entity.Comp, false))
entity.Comp.InvalidatedCoords.Add(tile);
}
public GasMixture?[]? GetTileMixtures(EntityUid? gridUid, EntityUid? mapUid, List<Vector2i> tiles, bool excite = false)
@@ -176,11 +176,11 @@ public partial class AtmosphereSystem
public bool IsTileAirBlocked(EntityUid gridUid, Vector2i tile, AtmosDirection directions = AtmosDirection.All, MapGridComponent? mapGridComp = null)
{
var ev = new IsTileAirBlockedMethodEvent(gridUid, tile, directions, mapGridComp);
RaiseLocalEvent(gridUid, ref ev);
if (!Resolve(gridUid, ref mapGridComp))
return false;
// If nothing handled the event, it'll default to true.
return ev.Result;
var data = GetAirtightData(gridUid, mapGridComp, tile);
return data.BlockedDirections.IsFlagSet(directions);
}
public bool IsTileSpace(EntityUid? gridUid, EntityUid? mapUid, Vector2i tile, MapGridComponent? mapGridComp = null)
@@ -231,12 +231,6 @@ public partial class AtmosphereSystem
return ev.Result ?? Enumerable.Empty<GasMixture>();
}
public void UpdateAdjacent(EntityUid gridUid, Vector2i tile, MapGridComponent? mapGridComp = null)
{
var ev = new UpdateAdjacentMethodEvent(gridUid, tile, mapGridComp);
RaiseLocalEvent(gridUid, ref ev);
}
public void HotspotExpose(EntityUid gridUid, Vector2i tile, float exposedTemperature, float exposedVolume,
EntityUid? sparkSourceUid = null, bool soh = false)
{
@@ -259,12 +253,6 @@ public partial class AtmosphereSystem
return ev.Result;
}
public void FixTileVacuum(EntityUid gridUid, Vector2i tile)
{
var ev = new FixTileVacuumMethodEvent(gridUid, tile);
RaiseLocalEvent(gridUid, ref ev);
}
public void AddPipeNet(EntityUid gridUid, PipeNet pipeNet)
{
var ev = new AddPipeNetMethodEvent(gridUid, pipeNet);
@@ -307,9 +295,6 @@ public partial class AtmosphereSystem
[ByRefEvent] private record struct GetAllMixturesMethodEvent
(EntityUid Grid, bool Excite = false, IEnumerable<GasMixture>? Mixtures = null, bool Handled = false);
[ByRefEvent] private record struct InvalidateTileMethodEvent
(EntityUid Grid, Vector2i Tile, bool Handled = false);
[ByRefEvent] private record struct GetTileMixturesMethodEvent
(EntityUid? GridUid, EntityUid? MapUid, List<Vector2i> Tiles, bool Excite = false, GasMixture?[]? Mixtures = null, bool Handled = false);
@@ -319,16 +304,6 @@ public partial class AtmosphereSystem
[ByRefEvent] private record struct ReactTileMethodEvent
(EntityUid GridId, Vector2i Tile, ReactionResult Result = default, bool Handled = false);
[ByRefEvent] private record struct IsTileAirBlockedMethodEvent
(EntityUid Grid, Vector2i Tile, AtmosDirection Direction = AtmosDirection.All, MapGridComponent? MapGridComponent = null, bool Result = false, bool Handled = false)
{
/// <summary>
/// True if one of the enabled blockers has <see cref="AirtightComponent.NoAirWhenFullyAirBlocked"/>. Note
/// that this does not actually check if all directions are blocked.
/// </summary>
public bool NoAir = false;
}
[ByRefEvent] private record struct IsTileSpaceMethodEvent
(EntityUid? Grid, EntityUid? Map, Vector2i Tile, MapGridComponent? MapGridComponent = null, bool Result = true, bool Handled = false);
@@ -339,9 +314,6 @@ public partial class AtmosphereSystem
(EntityUid Grid, Vector2i Tile, bool IncludeBlocked, bool Excite,
IEnumerable<GasMixture>? Result = null, bool Handled = false);
[ByRefEvent] private record struct UpdateAdjacentMethodEvent
(EntityUid Grid, Vector2i Tile, MapGridComponent? MapGridComponent = null, bool Handled = false);
[ByRefEvent] private record struct HotspotExposeMethodEvent
(EntityUid Grid, EntityUid? SparkSourceUid, Vector2i Tile, float ExposedTemperature, float ExposedVolume, bool soh, bool Handled = false);
@@ -351,9 +323,6 @@ public partial class AtmosphereSystem
[ByRefEvent] private record struct IsHotspotActiveMethodEvent
(EntityUid Grid, Vector2i Tile, bool Result = false, bool Handled = false);
[ByRefEvent] private record struct FixTileVacuumMethodEvent
(EntityUid Grid, Vector2i Tile, bool Handled = false);
[ByRefEvent] private record struct AddPipeNetMethodEvent
(EntityUid Grid, PipeNet PipeNet, bool Handled = false);

View File

@@ -92,6 +92,12 @@ public sealed partial class AtmosphereSystem
if (tile == null)
continue;
if (!_mapSystem.TryGetTile(gridComp, indices, out var gTile) || gTile.IsEmpty)
{
gridAtmosphere.Tiles.Remove(indices);
continue;
}
if (tile.Immutable && !IsTileSpace(euid, transform.MapUid, indices, gridComp))
{
tile = new GasMixture(tile.Volume) { Temperature = tile.Temperature };

View File

@@ -72,7 +72,8 @@ namespace Content.Server.Atmos.EntitySystems
var tileSize = excitedGroup.Tiles.Count;
if (excitedGroup.Disposed) return;
if (excitedGroup.Disposed)
return;
if (tileSize == 0)
{
@@ -98,7 +99,9 @@ namespace Content.Server.Atmos.EntitySystems
foreach (var tile in excitedGroup.Tiles)
{
if (tile?.Air == null) continue;
if (tile?.Air == null)
continue;
tile.Air.CopyFromMutable(combined);
InvalidateVisuals(tile.GridIndex, tile.GridIndices);
}
@@ -106,21 +109,23 @@ namespace Content.Server.Atmos.EntitySystems
excitedGroup.BreakdownCooldown = 0;
}
private void ExcitedGroupDismantle(GridAtmosphereComponent gridAtmosphere, ExcitedGroup excitedGroup, bool unexcite = true)
/// <summary>
/// This de-activates and removes all tiles in an excited group.
/// </summary>
private void DeactivateGroupTiles(GridAtmosphereComponent gridAtmosphere, ExcitedGroup excitedGroup)
{
foreach (var tile in excitedGroup.Tiles)
{
tile.ExcitedGroup = null;
if (!unexcite)
continue;
RemoveActiveTile(gridAtmosphere, tile);
}
excitedGroup.Tiles.Clear();
}
/// <summary>
/// This removes an excited group without de-activating its tiles.
/// </summary>
private void ExcitedGroupDispose(GridAtmosphereComponent gridAtmosphere, ExcitedGroup excitedGroup)
{
if (excitedGroup.Disposed)
@@ -129,9 +134,14 @@ namespace Content.Server.Atmos.EntitySystems
DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(excitedGroup), "Grid Atmosphere does not contain Excited Group!");
excitedGroup.Disposed = true;
gridAtmosphere.ExcitedGroups.Remove(excitedGroup);
ExcitedGroupDismantle(gridAtmosphere, excitedGroup, false);
foreach (var tile in excitedGroup.Tiles)
{
tile.ExcitedGroup = null;
}
excitedGroup.Tiles.Clear();
}
}
}

View File

@@ -14,6 +14,7 @@ public sealed partial class AtmosphereSystem
private void InitializeGridAtmosphere()
{
SubscribeLocalEvent<GridAtmosphereComponent, ComponentInit>(OnGridAtmosphereInit);
SubscribeLocalEvent<GridAtmosphereComponent, ComponentStartup>(OnGridAtmosphereStartup);
SubscribeLocalEvent<GridAtmosphereComponent, ComponentRemove>(OnAtmosphereRemove);
SubscribeLocalEvent<GridAtmosphereComponent, GridSplitEvent>(OnGridSplit);
@@ -22,19 +23,15 @@ public sealed partial class AtmosphereSystem
SubscribeLocalEvent<GridAtmosphereComponent, HasAtmosphereMethodEvent>(GridHasAtmosphere);
SubscribeLocalEvent<GridAtmosphereComponent, IsSimulatedGridMethodEvent>(GridIsSimulated);
SubscribeLocalEvent<GridAtmosphereComponent, GetAllMixturesMethodEvent>(GridGetAllMixtures);
SubscribeLocalEvent<GridAtmosphereComponent, InvalidateTileMethodEvent>(GridInvalidateTile);
SubscribeLocalEvent<GridAtmosphereComponent, GetTileMixtureMethodEvent>(GridGetTileMixture);
SubscribeLocalEvent<GridAtmosphereComponent, GetTileMixturesMethodEvent>(GridGetTileMixtures);
SubscribeLocalEvent<GridAtmosphereComponent, ReactTileMethodEvent>(GridReactTile);
SubscribeLocalEvent<GridAtmosphereComponent, IsTileAirBlockedMethodEvent>(GridIsTileAirBlocked);
SubscribeLocalEvent<GridAtmosphereComponent, IsTileSpaceMethodEvent>(GridIsTileSpace);
SubscribeLocalEvent<GridAtmosphereComponent, GetAdjacentTilesMethodEvent>(GridGetAdjacentTiles);
SubscribeLocalEvent<GridAtmosphereComponent, GetAdjacentTileMixturesMethodEvent>(GridGetAdjacentTileMixtures);
SubscribeLocalEvent<GridAtmosphereComponent, UpdateAdjacentMethodEvent>(GridUpdateAdjacent);
SubscribeLocalEvent<GridAtmosphereComponent, HotspotExposeMethodEvent>(GridHotspotExpose);
SubscribeLocalEvent<GridAtmosphereComponent, HotspotExtinguishMethodEvent>(GridHotspotExtinguish);
SubscribeLocalEvent<GridAtmosphereComponent, IsHotspotActiveMethodEvent>(GridIsHotspotActive);
SubscribeLocalEvent<GridAtmosphereComponent, FixTileVacuumMethodEvent>(GridFixTileVacuum);
SubscribeLocalEvent<GridAtmosphereComponent, AddPipeNetMethodEvent>(GridAddPipeNet);
SubscribeLocalEvent<GridAtmosphereComponent, RemovePipeNetMethodEvent>(GridRemovePipeNet);
SubscribeLocalEvent<GridAtmosphereComponent, AddAtmosDeviceMethodEvent>(GridAddAtmosDevice);
@@ -56,22 +53,23 @@ public sealed partial class AtmosphereSystem
}
}
private void OnGridAtmosphereInit(EntityUid uid, GridAtmosphereComponent gridAtmosphere, ComponentInit args)
private void OnGridAtmosphereInit(EntityUid uid, GridAtmosphereComponent component, ComponentInit args)
{
base.Initialize();
EnsureComp<GasTileOverlayComponent>(uid);
foreach (var tile in component.Tiles.Values)
{
tile.GridIndex = uid;
}
}
private void OnGridAtmosphereStartup(EntityUid uid, GridAtmosphereComponent component, ComponentStartup args)
{
if (!TryComp(uid, out MapGridComponent? mapGrid))
return;
EnsureComp<GasTileOverlayComponent>(uid);
foreach (var (indices, tile) in gridAtmosphere.Tiles)
{
gridAtmosphere.InvalidatedCoords.Add(indices);
tile.GridIndex = uid;
}
GridRepopulateTiles((uid, mapGrid, gridAtmosphere));
InvalidateAllTiles((uid, mapGrid, component));
}
private void OnGridSplit(EntityUid uid, GridAtmosphereComponent originalGridAtmos, ref GridSplitEvent args)
@@ -104,8 +102,7 @@ public sealed partial class AtmosphereSystem
continue;
// Copy a bunch of data over... Not great, maybe put this in TileAtmosphere?
newTileAtmosphere.Air = tileAtmosphere.Air?.Clone() ?? null;
newTileAtmosphere.MolesArchived = newTileAtmosphere.Air == null ? null : new float[Atmospherics.AdjustedNumberOfGases];
newTileAtmosphere.Air = tileAtmosphere.Air?.Clone();
newTileAtmosphere.Hotspot = tileAtmosphere.Hotspot;
newTileAtmosphere.HeatCapacity = tileAtmosphere.HeatCapacity;
newTileAtmosphere.Temperature = tileAtmosphere.Temperature;
@@ -170,15 +167,6 @@ public sealed partial class AtmosphereSystem
args.Handled = true;
}
private void GridInvalidateTile(EntityUid uid, GridAtmosphereComponent component, ref InvalidateTileMethodEvent args)
{
if (args.Handled)
return;
component.InvalidatedCoords.Add(args.Tile);
args.Handled = true;
}
private void GridGetTileMixture(EntityUid uid, GridAtmosphereComponent component,
ref GetTileMixtureMethodEvent args)
{
@@ -233,43 +221,6 @@ public sealed partial class AtmosphereSystem
args.Handled = true;
}
private void GridIsTileAirBlocked(EntityUid uid, GridAtmosphereComponent component,
ref IsTileAirBlockedMethodEvent args)
{
if (args.Handled)
return;
var mapGridComp = args.MapGridComponent;
if (!Resolve(uid, ref mapGridComp))
return;
var directions = AtmosDirection.Invalid;
var enumerator = GetObstructingComponentsEnumerator(mapGridComp, args.Tile);
while (enumerator.MoveNext(out var obstructingComponent))
{
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;
args.NoAir |= obstructingComponent.NoAirWhenFullyAirBlocked;
if (directions.IsFlagSet(args.Direction))
{
args.Result = true;
args.Handled = true;
return;
}
}
args.Result = false;
args.Handled = true;
}
private void GridIsTileSpace(EntityUid uid, GridAtmosphereComponent component, ref IsTileSpaceMethodEvent args)
{
if (args.Handled)
@@ -331,71 +282,58 @@ public sealed partial class AtmosphereSystem
args.Handled = true;
}
private void GridUpdateAdjacent(EntityUid uid, GridAtmosphereComponent component,
ref UpdateAdjacentMethodEvent args)
/// <summary>
/// Update array of adjacent tiles and the adjacency flags. Optionally activates all tiles with modified adjacencies.
/// </summary>
private void UpdateAdjacentTiles(
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
TileAtmosphere tile,
bool activate = false)
{
if (args.Handled)
return;
var mapGridComp = args.MapGridComponent;
if (!Resolve(uid, ref mapGridComp))
return;
var xform = Transform(uid);
EntityUid? mapUid = _mapManager.MapExists(xform.MapID) ? _mapManager.GetMapEntityId(xform.MapID) : null;
if (!component.Tiles.TryGetValue(args.Tile, out var tile))
return;
var uid = ent.Owner;
var atmos = ent.Comp1;
var blockedDirs = tile.AirtightData.BlockedDirections;
if (activate)
AddActiveTile(atmos, tile);
tile.AdjacentBits = AtmosDirection.Invalid;
tile.BlockedAirflow = GetBlockedDirections(mapGridComp, tile.GridIndices);
for (var i = 0; i < Atmospherics.Directions; i++)
{
var direction = (AtmosDirection) (1 << i);
var adjacentIndices = tile.GridIndices.Offset(direction);
var otherIndices = tile.GridIndices.Offset(direction);
if (!component.Tiles.TryGetValue(otherIndices, out var adjacent))
TileAtmosphere? adjacent;
if (!tile.NoGridTile)
{
adjacent = new TileAtmosphere(tile.GridIndex, otherIndices,
GetTileMixture(null, mapUid, otherIndices),
space: IsTileSpace(null, mapUid, otherIndices, mapGridComp));
adjacent = GetOrNewTile(uid, atmos, adjacentIndices);
}
var oppositeDirection = direction.GetOpposite();
adjacent.BlockedAirflow = GetBlockedDirections(mapGridComp, adjacent.GridIndices);
// Pass in MapGridComponent so we don't have to resolve it for every adjacent direction.
var tileBlockedEv = new IsTileAirBlockedMethodEvent(uid, tile.GridIndices, direction, mapGridComp);
GridIsTileAirBlocked(uid, component, ref tileBlockedEv);
var adjacentBlockedEv =
new IsTileAirBlockedMethodEvent(uid, adjacent.GridIndices, oppositeDirection, mapGridComp);
GridIsTileAirBlocked(uid, component, ref adjacentBlockedEv);
if (!adjacent.BlockedAirflow.IsFlagSet(oppositeDirection) && !tileBlockedEv.Result)
{
adjacent.AdjacentBits |= oppositeDirection;
adjacent.AdjacentTiles[oppositeDirection.ToIndex()] = tile;
}
else
{
adjacent.AdjacentBits &= ~oppositeDirection;
adjacent.AdjacentTiles[oppositeDirection.ToIndex()] = null;
}
if (!tile.BlockedAirflow.IsFlagSet(direction) && !adjacentBlockedEv.Result)
{
tile.AdjacentBits |= direction;
tile.AdjacentTiles[direction.ToIndex()] = adjacent;
}
else
else if (!atmos.Tiles.TryGetValue(adjacentIndices, out adjacent))
{
tile.AdjacentBits &= ~direction;
tile.AdjacentTiles[direction.ToIndex()] = null;
tile.AdjacentTiles[i] = null;
continue;
}
var adjBlockDirs = adjacent.AirtightData.BlockedDirections;
if (activate)
AddActiveTile(atmos, adjacent);
var oppositeDirection = direction.GetOpposite();
if (adjBlockDirs.IsFlagSet(oppositeDirection) || blockedDirs.IsFlagSet(direction))
{
// Adjacency is blocked by some airtight entity.
tile.AdjacentBits &= ~direction;
adjacent.AdjacentBits &= ~oppositeDirection;
tile.AdjacentTiles[i] = null;
adjacent.AdjacentTiles[oppositeDirection.ToIndex()] = null;
}
else
{
// No airtight entity in the way.
tile.AdjacentBits |= direction;
adjacent.AdjacentBits |= oppositeDirection;
tile.AdjacentTiles[i] = adjacent;
adjacent.AdjacentTiles[oppositeDirection.ToIndex()] = tile;
}
DebugTools.Assert(!(tile.AdjacentBits.IsFlagSet(direction) ^
@@ -409,6 +347,16 @@ public sealed partial class AtmosphereSystem
tile.MonstermosInfo.CurrentTransferDirection = AtmosDirection.Invalid;
}
private (GasMixture Air, bool IsSpace) GetDefaultMapAtmosphere(MapAtmosphereComponent? map)
{
if (map == null)
return (GasMixture.SpaceGas, true);
var air = map.Mixture;
DebugTools.Assert(air.Immutable);
return (air, map.Space);
}
private void GridHotspotExpose(EntityUid uid, GridAtmosphereComponent component, ref HotspotExposeMethodEvent args)
{
if (args.Handled)
@@ -451,54 +399,50 @@ public sealed partial class AtmosphereSystem
args.Handled = true;
}
private void GridFixTileVacuum(EntityUid uid, GridAtmosphereComponent component, ref FixTileVacuumMethodEvent args)
private void GridFixTileVacuum(TileAtmosphere tile)
{
if (args.Handled)
return;
var adjEv = new GetAdjacentTileMixturesMethodEvent(uid, args.Tile, false, true);
GridGetAdjacentTileMixtures(uid, component, ref adjEv);
if (!adjEv.Handled || !component.Tiles.TryGetValue(args.Tile, out var tile))
return;
if (!TryComp<MapGridComponent>(uid, out var mapGridComp))
return;
var adjacent = adjEv.Result!.ToArray();
// Return early, let's not cause any funny NaNs or needless vacuums.
if (adjacent.Length == 0)
return;
tile.Air = new GasMixture
{
Volume = GetVolumeForTiles(mapGridComp, 1),
Temperature = Atmospherics.T20C
};
tile.MolesArchived = new float[Atmospherics.AdjustedNumberOfGases];
DebugTools.AssertNotNull(tile.Air);
DebugTools.Assert(tile.Air?.Immutable == false );
Array.Clear(tile.MolesArchived);
tile.ArchivedCycle = 0;
var ratio = 1f / adjacent.Length;
var count = 0;
foreach (var adj in tile.AdjacentTiles)
{
if (adj?.Air != null)
count++;
}
if (count == 0)
return;
var ratio = 1f / count;
var totalTemperature = 0f;
foreach (var adj in adjacent)
foreach (var adj in tile.AdjacentTiles)
{
if (adj?.Air == null)
continue;
totalTemperature += adj.Temperature;
// TODO ATMOS. Why is this removing and then re-adding air to the neighbouring tiles?
// Is it some rounding issue to do with Atmospherics.GasMinMoles? because otherwise this is just unnecessary.
// if we get rid of this, then this could also just add moles and then multiply by ratio at the end, rather
// than having to iterate over adjacent tiles twice.
// Remove a bit of gas from the adjacent ratio...
var mix = adj.RemoveRatio(ratio);
var mix = adj.Air.RemoveRatio(ratio);
// And merge it to the new tile air.
Merge(tile.Air, mix);
// Return removed gas to its original mixture.
Merge(adj, mix);
Merge(adj.Air, mix);
}
// New temperature is the arithmetic mean of the sum of the adjacent temperatures...
tile.Air.Temperature = totalTemperature / adjacent.Length;
tile.Air.Temperature = totalTemperature / count;
}
private void GridAddPipeNet(EntityUid uid, GridAtmosphereComponent component, ref AddPipeNetMethodEvent args)
@@ -547,30 +491,21 @@ public sealed partial class AtmosphereSystem
/// <summary>
/// Repopulates all tiles on a grid atmosphere.
/// </summary>
/// <param name="mapGrid">The grid where to get all valid tiles from.</param>
/// <param name="gridAtmosphere">The grid atmosphere where the tiles will be repopulated.</param>
private void GridRepopulateTiles(Entity<MapGridComponent, GridAtmosphereComponent> grid)
public void InvalidateAllTiles(Entity<MapGridComponent?, GridAtmosphereComponent?> entity)
{
var (uid, mapGrid, gridAtmosphere) = grid;
var volume = GetVolumeForTiles(mapGrid, 1);
var (uid, grid, atmos) = entity;
if (!Resolve(uid, ref grid, ref atmos))
return;
foreach (var tile in mapGrid.GetAllTiles())
foreach (var indices in atmos.Tiles.Keys)
{
if (!gridAtmosphere.Tiles.ContainsKey(tile.GridIndices))
gridAtmosphere.Tiles[tile.GridIndices] = new TileAtmosphere(tile.GridUid, tile.GridIndices,
new GasMixture(volume) { Temperature = Atmospherics.T20C });
gridAtmosphere.InvalidatedCoords.Add(tile.GridIndices);
atmos.InvalidatedCoords.Add(indices);
}
TryComp(uid, out GasTileOverlayComponent? overlay);
// Gotta do this afterwards so we can properly update adjacent tiles.
foreach (var (position, _) in gridAtmosphere.Tiles.ToArray())
var enumerator = _map.GetAllTilesEnumerator(uid, grid);
while (enumerator.MoveNext(out var tile))
{
var ev = new UpdateAdjacentMethodEvent(uid, position);
GridUpdateAdjacent(uid, gridAtmosphere, ref ev);
InvalidateVisuals(uid, position, overlay);
atmos.InvalidatedCoords.Add(tile.Value.GridIndices);
}
}

View File

@@ -1,13 +1,11 @@
using Content.Server.Atmos.Components;
using Content.Shared.Atmos;
using Content.Shared.Audio;
using Content.Shared.Mobs.Components;
using Content.Shared.Physics;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Utility;
@@ -54,7 +52,7 @@ namespace Content.Server.Atmos.EntitySystems
if (HasComp<MobStateComponent>(uid) &&
TryComp<PhysicsComponent>(uid, out var body))
{
_physics.SetBodyStatus(body, BodyStatus.OnGround);
_physics.SetBodyStatus(uid, body, BodyStatus.OnGround);
}
if (TryComp<FixturesComponent>(uid, out var fixtures))
@@ -77,7 +75,7 @@ namespace Content.Server.Atmos.EntitySystems
if (!TryComp<FixturesComponent>(uid, out var fixtures))
return;
_physics.SetBodyStatus(body, BodyStatus.InAir);
_physics.SetBodyStatus(uid, body, BodyStatus.InAir);
foreach (var (id, fixture) in fixtures.Fixtures)
{
@@ -96,9 +94,9 @@ namespace Content.Server.Atmos.EntitySystems
// TODO ATMOS finish this
// Don't play the space wind sound on tiles that are on fire...
if(tile.PressureDifference > 15 && !tile.Hotspot.Valid)
if (tile.PressureDifference > 15 && !tile.Hotspot.Valid)
{
if(_spaceWindSoundCooldown == 0 && !string.IsNullOrEmpty(SpaceWindSound))
if (_spaceWindSoundCooldown == 0 && !string.IsNullOrEmpty(SpaceWindSound))
{
var coordinates = _mapSystem.ToCenterCoordinates(tile.GridIndex, tile.GridIndices);
_audio.PlayPvs(SpaceWindSound, coordinates, AudioParams.Default.WithVariation(0.125f).WithVolume(MathHelper.Clamp(tile.PressureDifference / 10, 10, 100)));
@@ -238,7 +236,7 @@ namespace Content.Server.Atmos.EntitySystems
// TODO: Technically these directions won't be correct but uhh I'm just here for optimisations buddy not to fix my old bugs.
if (throwTarget != EntityCoordinates.Invalid)
{
var pos = ((throwTarget.ToMap(EntityManager).Position - xform.WorldPosition).Normalized() + dirVec).Normalized();
var pos = ((throwTarget.ToMap(EntityManager, _transformSystem).Position - xform.WorldPosition).Normalized() + dirVec).Normalized();
_physics.ApplyLinearImpulse(uid, pos * moveForce, body: physics);
}
else

View File

@@ -1,12 +1,13 @@
using Content.Server.Atmos.Components;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Robust.Shared.Utility;
namespace Content.Server.Atmos.EntitySystems
{
public sealed partial class AtmosphereSystem
{
private void ProcessCell(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile, int fireCount, GasTileOverlayComponent? visuals)
private void ProcessCell(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile, int fireCount, GasTileOverlayComponent visuals)
{
// Can't process a tile without air
if (tile.Air == null)
@@ -116,15 +117,9 @@ namespace Content.Server.Atmos.EntitySystems
private void Archive(TileAtmosphere tile, int fireCount)
{
if (tile.Air != null)
{
tile.Air.Moles.AsSpan().CopyTo(tile.MolesArchived.AsSpan());
tile.TemperatureArchived = tile.Air.Temperature;
}
else
{
tile.TemperatureArchived = tile.Temperature;
}
tile.TemperatureArchived = tile.Temperature;
tile.ArchivedCycle = fireCount;
}
@@ -166,6 +161,12 @@ namespace Content.Server.Atmos.EntitySystems
/// <param name="disposeExcitedGroup">Whether to dispose of the tile's <see cref="ExcitedGroup"/></param>
private void RemoveActiveTile(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile, bool disposeExcitedGroup = true)
{
DebugTools.Assert(tile.Excited == gridAtmosphere.ActiveTiles.Contains(tile));
DebugTools.Assert(tile.Excited || tile.ExcitedGroup == null);
if (!tile.Excited)
return;
tile.Excited = false;
gridAtmosphere.ActiveTiles.Remove(tile);
@@ -186,7 +187,6 @@ namespace Content.Server.Atmos.EntitySystems
if (tile.Air == null)
return tile.HeatCapacity;
// Moles archived is not null if air is not null.
return GetHeatCapacityCalculation(tile.MolesArchived!, tile.Space);
}

View File

@@ -1,6 +1,8 @@
using Content.Server.Atmos.Components;
using Content.Shared.Atmos.Components;
using Robust.Shared.GameStates;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
namespace Content.Server.Atmos.EntitySystems;
@@ -8,10 +10,25 @@ public partial class AtmosphereSystem
{
private void InitializeMap()
{
SubscribeLocalEvent<MapAtmosphereComponent, ComponentInit>(OnMapStartup);
SubscribeLocalEvent<MapAtmosphereComponent, ComponentRemove>(OnMapRemove);
SubscribeLocalEvent<MapAtmosphereComponent, IsTileSpaceMethodEvent>(MapIsTileSpace);
SubscribeLocalEvent<MapAtmosphereComponent, GetTileMixtureMethodEvent>(MapGetTileMixture);
SubscribeLocalEvent<MapAtmosphereComponent, GetTileMixturesMethodEvent>(MapGetTileMixtures);
SubscribeLocalEvent<MapAtmosphereComponent, ComponentGetState>(OnMapGetState);
SubscribeLocalEvent<GridAtmosphereComponent, EntParentChangedMessage>(OnGridParentChanged);
}
private void OnMapStartup(EntityUid uid, MapAtmosphereComponent component, ComponentInit args)
{
component.Mixture.MarkImmutable();
component.Overlay = _gasTileOverlaySystem.GetOverlayData(component.Mixture);
}
private void OnMapRemove(EntityUid uid, MapAtmosphereComponent component, ComponentRemove args)
{
if (!TerminatingOrDeleted(uid))
RefreshAllGridMapAtmospheres(uid);
}
private void MapIsTileSpace(EntityUid uid, MapAtmosphereComponent component, ref IsTileSpaceMethodEvent args)
@@ -28,54 +45,115 @@ public partial class AtmosphereSystem
if (args.Handled)
return;
// Clone the mixture, if possible.
args.Mixture = component.Mixture?.Clone();
args.Mixture = component.Mixture;
args.Handled = true;
}
private void MapGetTileMixtures(EntityUid uid, MapAtmosphereComponent component, ref GetTileMixturesMethodEvent args)
{
if (args.Handled || component.Mixture == null)
if (args.Handled)
return;
args.Handled = true;
args.Mixtures ??= new GasMixture?[args.Tiles.Count];
for (var i = 0; i < args.Tiles.Count; i++)
{
args.Mixtures[i] ??= component.Mixture.Clone();
args.Mixtures[i] ??= component.Mixture;
}
}
private void OnMapGetState(EntityUid uid, MapAtmosphereComponent component, ref ComponentGetState args)
{
args.State = new MapAtmosphereComponentState(_gasTileOverlaySystem.GetOverlayData(component.Mixture));
args.State = new MapAtmosphereComponentState(component.Overlay);
}
public void SetMapAtmosphere(EntityUid uid, bool space, GasMixture mixture, MapAtmosphereComponent? component = null)
public void SetMapAtmosphere(EntityUid uid, bool space, GasMixture mixture)
{
DebugTools.Assert(HasComp<MapComponent>(uid));
var component = EnsureComp<MapAtmosphereComponent>(uid);
SetMapGasMixture(uid, mixture, component, false);
SetMapSpace(uid, space, component, false);
RefreshAllGridMapAtmospheres(uid);
}
public void SetMapGasMixture(EntityUid uid, GasMixture mixture, MapAtmosphereComponent? component = null, bool updateTiles = true)
{
if (!Resolve(uid, ref component))
return;
if (!mixture.Immutable)
{
mixture = mixture.Clone();
mixture.MarkImmutable();
}
component.Mixture = mixture;
component.Overlay = _gasTileOverlaySystem.GetOverlayData(component.Mixture);
Dirty(uid, component);
if (updateTiles)
RefreshAllGridMapAtmospheres(uid);
}
public void SetMapSpace(EntityUid uid, bool space, MapAtmosphereComponent? component = null, bool updateTiles = true)
{
if (!Resolve(uid, ref component))
return;
if (component.Space == space)
return;
component.Space = space;
component.Mixture = mixture;
Dirty(uid, component);
if (updateTiles)
RefreshAllGridMapAtmospheres(uid);
}
public void SetMapGasMixture(EntityUid uid, GasMixture? mixture, MapAtmosphereComponent? component = null)
/// <summary>
/// Forces a refresh of all MapAtmosphere tiles on every grid on a map.
/// </summary>
public void RefreshAllGridMapAtmospheres(EntityUid map)
{
if (!Resolve(uid, ref component))
return;
component.Mixture = mixture;
Dirty(uid, component);
DebugTools.Assert(HasComp<MapComponent>(map));
var enumerator = AllEntityQuery<GridAtmosphereComponent, TransformComponent>();
while (enumerator.MoveNext(out var grid, out var atmos, out var xform))
{
if (xform.MapUid == map)
RefreshMapAtmosphereTiles((grid, atmos));
}
}
public void SetMapSpace(EntityUid uid, bool space, MapAtmosphereComponent? component = null)
/// <summary>
/// Forces a refresh of all MapAtmosphere tiles on a given grid.
/// </summary>
private void RefreshMapAtmosphereTiles(Entity<GridAtmosphereComponent?> grid)
{
if (!Resolve(uid, ref component))
if (!Resolve(grid.Owner, ref grid.Comp))
return;
component.Space = space;
Dirty(uid, component);
var atmos = grid.Comp;
foreach (var tile in atmos.MapTiles)
{
RemoveMapAtmos(atmos, tile);
atmos.InvalidatedCoords.Add(tile.GridIndices);
}
atmos.MapTiles.Clear();
}
/// <summary>
/// Handles updating map-atmospheres when grids move across maps.
/// </summary>
private void OnGridParentChanged(Entity<GridAtmosphereComponent> grid, ref EntParentChangedMessage args)
{
// Do nothing if detaching to nullspace
if (!args.Transform.ParentUid.IsValid())
return;
// Avoid doing work if moving from a space-map to another space-map.
if (args.OldParent == null
|| HasComp<MapAtmosphereComponent>(args.OldParent)
|| HasComp<MapAtmosphereComponent>(args.Transform.ParentUid))
{
RefreshMapAtmosphereTiles((grid, grid));
}
}
}

View File

@@ -5,7 +5,6 @@ using Content.Server.Doors.Systems;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Database;
using Content.Shared.Doors.Components;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components;
using Robust.Shared.Random;
@@ -27,7 +26,10 @@ namespace Content.Server.Atmos.EntitySystems
private readonly TileAtmosphere[] _depressurizeSpaceTiles = new TileAtmosphere[Atmospherics.MonstermosHardTileLimit];
private readonly TileAtmosphere[] _depressurizeProgressionOrder = new TileAtmosphere[Atmospherics.MonstermosHardTileLimit * 2];
private void EqualizePressureInZone(Entity<MapGridComponent, GridAtmosphereComponent> ent, TileAtmosphere tile, int cycleNum, GasTileOverlayComponent? visuals)
private void EqualizePressureInZone(
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
TileAtmosphere tile,
int cycleNum)
{
if (tile.Air == null || (tile.MonstermosInfo.LastCycle >= cycleNum))
return; // Already done.
@@ -56,7 +58,7 @@ namespace Content.Server.Atmos.EntitySystems
return;
}
var (_, mapGrid, gridAtmosphere) = ent;
var gridAtmosphere = ent.Comp1;
var queueCycle = ++gridAtmosphere.EqualizationQueueCycleControl;
var totalMoles = 0f;
_equalizeTiles[0] = tile;
@@ -91,7 +93,7 @@ namespace Content.Server.Atmos.EntitySystems
{
// Looks like someone opened an airlock to space!
ExplosivelyDepressurize(ent, tile, cycleNum, visuals);
ExplosivelyDepressurize(ent, tile, cycleNum);
return;
}
}
@@ -216,9 +218,13 @@ namespace Content.Server.Atmos.EntitySystems
for (var k = 0; k < Atmospherics.Directions; k++)
{
var direction = (AtmosDirection) (1 << k);
if (!otherTile.AdjacentBits.IsFlagSet(direction)) continue;
if (!otherTile.AdjacentBits.IsFlagSet(direction))
continue;
if (giver.MonstermosInfo.MoleDelta <= 0)
break; // We're done here now. Let's not do more work than needed.
var otherTile2 = otherTile.AdjacentTiles[k];
if (giver.MonstermosInfo.MoleDelta <= 0) break; // We're done here now. Let's not do more work than needed.
if (otherTile2 == null || otherTile2.MonstermosInfo.LastQueueCycle != queueCycle) continue;
DebugTools.Assert(otherTile2.AdjacentBits.IsFlagSet(direction.GetOpposite()));
if (otherTile2.MonstermosInfo.LastSlowQueueCycle == queueCycleSlow) continue;
@@ -332,7 +338,7 @@ namespace Content.Server.Atmos.EntitySystems
for (var i = 0; i < tileCount; i++)
{
var otherTile = _equalizeTiles[i]!;
FinalizeEq(gridAtmosphere, otherTile, visuals);
FinalizeEq(gridAtmosphere, otherTile, ent);
}
for (var i = 0; i < tileCount; i++)
@@ -341,12 +347,17 @@ namespace Content.Server.Atmos.EntitySystems
for (var j = 0; j < Atmospherics.Directions; j++)
{
var direction = (AtmosDirection) (1 << j);
if (!otherTile.AdjacentBits.IsFlagSet(direction)) continue;
if (!otherTile.AdjacentBits.IsFlagSet(direction))
continue;
var otherTile2 = otherTile.AdjacentTiles[j]!;
if (otherTile2.AdjacentBits == 0)
continue;
DebugTools.Assert(otherTile2.AdjacentBits.IsFlagSet(direction.GetOpposite()));
if (otherTile2.Air != null && CompareExchange(otherTile2.Air, tile.Air) == GasCompareResult.NoExchange) continue;
if (otherTile2.Air != null && CompareExchange(otherTile2.Air, tile.Air) == GasCompareResult.NoExchange)
continue;
AddActiveTile(gridAtmosphere, otherTile2);
break;
}
@@ -359,7 +370,10 @@ namespace Content.Server.Atmos.EntitySystems
Array.Clear(_equalizeQueue, 0, Atmospherics.MonstermosTileLimit);
}
private void ExplosivelyDepressurize(Entity<MapGridComponent, GridAtmosphereComponent> ent, TileAtmosphere tile, int cycleNum, GasTileOverlayComponent? visuals)
private void ExplosivelyDepressurize(
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
TileAtmosphere tile,
int cycleNum)
{
// Check if explosive depressurization is enabled and if the tile is valid.
if (!MonstermosDepressurization || tile.Air == null)
@@ -368,7 +382,7 @@ namespace Content.Server.Atmos.EntitySystems
const int limit = Atmospherics.MonstermosHardTileLimit;
var totalMolesRemoved = 0f;
var (owner, mapGrid, gridAtmosphere) = ent;
var (owner, gridAtmosphere, visuals, mapGrid, _) = ent;
var queueCycle = ++gridAtmosphere.EqualizationQueueCycleControl;
var tileCount = 0;
@@ -388,20 +402,27 @@ namespace Content.Server.Atmos.EntitySystems
{
for (var j = 0; j < Atmospherics.Directions; j++)
{
var direction = (AtmosDirection) (1 << j);
if (!otherTile.AdjacentBits.IsFlagSet(direction)) continue;
var otherTile2 = otherTile.AdjacentTiles[j];
if (otherTile2?.Air == null) continue;
DebugTools.Assert(otherTile2.AdjacentBits.IsFlagSet(direction.GetOpposite()));
if (otherTile2.MonstermosInfo.LastQueueCycle == queueCycle) continue;
if (otherTile2?.Air == null)
continue;
ConsiderFirelocks((owner, gridAtmosphere), otherTile, otherTile2, visuals, mapGrid);
if (otherTile2.MonstermosInfo.LastQueueCycle == queueCycle)
continue;
var direction = (AtmosDirection) (1 << j);
DebugTools.Assert(otherTile.AdjacentBits.IsFlagSet(direction));
DebugTools.Assert(otherTile2.AdjacentBits.IsFlagSet(direction.GetOpposite()));
ConsiderFirelocks(ent, otherTile, otherTile2);
// The firelocks might have closed on us.
if (!otherTile.AdjacentBits.IsFlagSet(direction)) continue;
if (!otherTile.AdjacentBits.IsFlagSet(direction))
continue;
otherTile2.MonstermosInfo = new MonstermosInfo { LastQueueCycle = queueCycle };
_depressurizeTiles[tileCount++] = otherTile2;
if (tileCount >= limit) break;
if (tileCount >= limit)
break;
}
}
else
@@ -437,13 +458,21 @@ namespace Content.Server.Atmos.EntitySystems
// Flood fill into this new direction
var direction = (AtmosDirection) (1 << j);
// Tiles in _depressurizeProgressionOrder cannot have null air.
if (!otherTile.AdjacentBits.IsFlagSet(direction) && !otherTile.Space) continue;
if (!otherTile.AdjacentBits.IsFlagSet(direction) && !otherTile.Space)
continue;
var tile2 = otherTile.AdjacentTiles[j];
if (tile2?.MonstermosInfo.LastQueueCycle != queueCycle) continue;
if (tile2?.MonstermosInfo.LastQueueCycle != queueCycle)
continue;
DebugTools.Assert(tile2.AdjacentBits.IsFlagSet(direction.GetOpposite()));
// If flood fill has already reached this tile, continue.
if (tile2.MonstermosInfo.LastSlowQueueCycle == queueCycleSlow) continue;
if(tile2.Space) continue;
if (tile2.MonstermosInfo.LastSlowQueueCycle == queueCycleSlow)
continue;
if(tile2.Space)
continue;
tile2.MonstermosInfo.CurrentTransferDirection = direction.GetOpposite();
tile2.MonstermosInfo.CurrentTransferAmount = 0.0f;
tile2.PressureSpecificTarget = otherTile.PressureSpecificTarget;
@@ -535,7 +564,7 @@ namespace Content.Server.Atmos.EntitySystems
_physics.ApplyAngularImpulse(owner, Vector2Helpers.Cross(tile.GridIndices - gridPhysics.LocalCenter, direction) * totalMolesRemoved, body: gridPhysics);
}
if(tileCount > 10 && (totalMolesRemoved / tileCount) > 10)
if (tileCount > 10 && (totalMolesRemoved / tileCount) > 10)
_adminLog.Add(LogType.ExplosiveDepressurization, LogImpact.High,
$"Explosive depressurization removed {totalMolesRemoved} moles from {tileCount} tiles starting from position {tile.GridIndices:position} on grid ID {tile.GridIndex:grid}");
@@ -544,36 +573,33 @@ namespace Content.Server.Atmos.EntitySystems
Array.Clear(_depressurizeProgressionOrder, 0, Atmospherics.MonstermosHardTileLimit * 2);
}
private void ConsiderFirelocks(Entity<GridAtmosphereComponent> ent, TileAtmosphere tile, TileAtmosphere other, GasTileOverlayComponent? visuals, MapGridComponent mapGrid)
private void ConsiderFirelocks(
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
TileAtmosphere tile,
TileAtmosphere other)
{
var reconsiderAdjacent = false;
foreach (var entity in mapGrid.GetAnchoredEntities(tile.GridIndices))
var mapGrid = ent.Comp3;
foreach (var entity in _map.GetAnchoredEntities(ent.Owner, mapGrid, tile.GridIndices))
{
if (!TryComp(entity, out FirelockComponent? firelock))
continue;
reconsiderAdjacent |= _firelockSystem.EmergencyPressureStop(entity, firelock);
if (_firelockQuery.TryGetComponent(entity, out var firelock))
reconsiderAdjacent |= _firelockSystem.EmergencyPressureStop(entity, firelock);
}
foreach (var entity in mapGrid.GetAnchoredEntities(other.GridIndices))
foreach (var entity in _map.GetAnchoredEntities(ent.Owner, mapGrid, other.GridIndices))
{
if (!TryComp(entity, out FirelockComponent? firelock))
continue;
reconsiderAdjacent |= _firelockSystem.EmergencyPressureStop(entity, firelock);
if (_firelockQuery.TryGetComponent(entity, out var firelock))
reconsiderAdjacent |= _firelockSystem.EmergencyPressureStop(entity, firelock);
}
if (!reconsiderAdjacent)
return;
var (owner, gridAtmosphere) = ent;
var tileEv = new UpdateAdjacentMethodEvent(owner, tile.GridIndices);
var otherEv = new UpdateAdjacentMethodEvent(owner, other.GridIndices);
GridUpdateAdjacent(owner, gridAtmosphere, ref tileEv);
GridUpdateAdjacent(owner, gridAtmosphere, ref otherEv);
InvalidateVisuals(tile.GridIndex, tile.GridIndices, visuals);
InvalidateVisuals(other.GridIndex, other.GridIndices, visuals);
UpdateAdjacentTiles(ent, tile);
UpdateAdjacentTiles(ent, other);
InvalidateVisuals(tile.GridIndex, tile.GridIndices, ent);
InvalidateVisuals(other.GridIndex, other.GridIndices, ent);
}
private void FinalizeEq(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile, GasTileOverlayComponent? visuals)
@@ -642,7 +668,7 @@ namespace Content.Server.Atmos.EntitySystems
if (adj == null)
{
var nonNull = tile.AdjacentTiles.Where(x => x != null).Count();
Log.Error($"Encountered null adjacent tile in {nameof(AdjustEqMovement)}. Dir: {direction}, Tile: {tile.Tile}, non-null adj count: {nonNull}, Trace: {Environment.StackTrace}");
Log.Error($"Encountered null adjacent tile in {nameof(AdjustEqMovement)}. Dir: {direction}, Tile: ({tile.GridIndex}, {tile.GridIndices}), non-null adj count: {nonNull}, Trace: {Environment.StackTrace}");
return;
}

View File

@@ -1,6 +1,5 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.Piping.Components;
using Content.Server.NodeContainer.NodeGroups;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Maps;
@@ -8,6 +7,7 @@ using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Atmos.EntitySystems
{
@@ -30,118 +30,63 @@ namespace Content.Server.Atmos.EntitySystems
private int _currentRunAtmosphereIndex;
private bool _simulationPaused;
private readonly List<Entity<GridAtmosphereComponent>> _currentRunAtmosphere = new();
private TileAtmosphere GetOrNewTile(EntityUid owner, GridAtmosphereComponent atmosphere, Vector2i index)
{
var tile = atmosphere.Tiles.GetOrNew(index, out var existing);
if (existing)
return tile;
atmosphere.InvalidatedCoords.Add(index);
tile.GridIndex = owner;
tile.GridIndices = index;
return tile;
}
private readonly List<Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent>> _currentRunAtmosphere = new();
/// <summary>
/// Revalidates all invalid coordinates in a grid atmosphere.
/// I.e., process any tiles that have had their airtight blockers modified.
/// </summary>
/// <param name="ent">The grid atmosphere in question.</param>
/// <returns>Whether the process succeeded or got paused due to time constrains.</returns>
private bool ProcessRevalidate(Entity<GridAtmosphereComponent> ent, GasTileOverlayComponent? visuals)
private bool ProcessRevalidate(Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent)
{
var (owner, atmosphere) = ent;
if (!atmosphere.ProcessingPaused)
if (ent.Comp4.MapUid == null)
{
atmosphere.CurrentRunInvalidatedCoordinates.Clear();
atmosphere.CurrentRunInvalidatedCoordinates.EnsureCapacity(atmosphere.InvalidatedCoords.Count);
foreach (var tile in atmosphere.InvalidatedCoords)
{
atmosphere.CurrentRunInvalidatedCoordinates.Enqueue(tile);
}
atmosphere.InvalidatedCoords.Clear();
Log.Error($"Attempted to process atmosphere on a map-less grid? Grid: {ToPrettyString(ent)}");
return true;
}
if (!TryComp(owner, out MapGridComponent? mapGridComp))
return true;
var (uid, atmosphere, visuals, grid, xform) = ent;
var volume = GetVolumeForTiles(grid);
TryComp(xform.MapUid, out MapAtmosphereComponent? mapAtmos);
var mapUid = _mapManager.GetMapEntityIdOrThrow(Transform(owner).MapID);
if (!atmosphere.ProcessingPaused)
{
atmosphere.CurrentRunInvalidatedTiles.Clear();
atmosphere.CurrentRunInvalidatedTiles.EnsureCapacity(atmosphere.InvalidatedCoords.Count);
foreach (var indices in atmosphere.InvalidatedCoords)
{
var tile = GetOrNewTile(uid, atmosphere, indices);
atmosphere.CurrentRunInvalidatedTiles.Enqueue(tile);
var volume = GetVolumeForTiles(mapGridComp);
// Update tile.IsSpace and tile.MapAtmosphere, and tile.AirtightData.
UpdateTileData(ent, mapAtmos, tile);
}
atmosphere.InvalidatedCoords.Clear();
if (_simulationStopwatch.Elapsed.TotalMilliseconds >= AtmosMaxProcessTime)
return false;
}
var number = 0;
while (atmosphere.CurrentRunInvalidatedCoordinates.TryDequeue(out var indices))
while (atmosphere.CurrentRunInvalidatedTiles.TryDequeue(out var tile))
{
if (!atmosphere.Tiles.TryGetValue(indices, out var tile))
{
tile = new TileAtmosphere(owner, indices,
new GasMixture(volume) { Temperature = Atmospherics.T20C });
atmosphere.Tiles[indices] = tile;
}
var airBlockedEv = new IsTileAirBlockedMethodEvent(owner, indices, MapGridComponent:mapGridComp);
GridIsTileAirBlocked(owner, atmosphere, ref airBlockedEv);
var isAirBlocked = airBlockedEv.Result;
var oldBlocked = tile.BlockedAirflow;
var updateAdjacentEv = new UpdateAdjacentMethodEvent(owner, indices, mapGridComp);
GridUpdateAdjacent(owner, atmosphere, ref updateAdjacentEv);
// Blocked airflow changed, rebuild excited groups!
if (tile.Excited && tile.BlockedAirflow != oldBlocked)
{
RemoveActiveTile(atmosphere, tile);
}
// Call this instead of the grid method as the map has a say on whether the tile is space or not.
if ((!mapGridComp.TryGetTileRef(indices, out var t) || t.IsSpace(_tileDefinitionManager)) && !isAirBlocked)
{
tile.Air = GetTileMixture(null, mapUid, indices);
tile.MolesArchived = tile.Air != null ? new float[Atmospherics.AdjustedNumberOfGases] : null;
tile.Space = IsTileSpace(null, mapUid, indices, mapGridComp);
}
else if (isAirBlocked)
{
if (airBlockedEv.NoAir)
{
tile.Air = null;
tile.MolesArchived = null;
tile.ArchivedCycle = 0;
tile.LastShare = 0f;
tile.Hotspot = new Hotspot();
}
}
else
{
if (tile.Air == null && NeedsVacuumFixing(mapGridComp, indices))
{
var vacuumEv = new FixTileVacuumMethodEvent(owner, indices);
GridFixTileVacuum(owner, atmosphere, ref vacuumEv);
}
// Tile used to be space, but isn't anymore.
if (tile.Space || (tile.Air?.Immutable ?? false))
{
tile.Air = null;
tile.MolesArchived = null;
tile.ArchivedCycle = 0;
tile.LastShare = 0f;
tile.Space = false;
}
tile.Air ??= new GasMixture(volume){Temperature = Atmospherics.T20C};
tile.MolesArchived ??= new float[Atmospherics.AdjustedNumberOfGases];
}
// We activate the tile.
AddActiveTile(atmosphere, tile);
// TODO ATMOS: Query all the contents of this tile (like walls) and calculate the correct thermal conductivity and heat capacity
var tileDef = mapGridComp.TryGetTileRef(indices, out var tileRef)
? tileRef.GetContentTileDefinition(_tileDefinitionManager)
: null;
tile.ThermalConductivity = tileDef?.ThermalConductivity ?? 0.5f;
tile.HeatCapacity = tileDef?.HeatCapacity ?? float.PositiveInfinity;
InvalidateVisuals(owner, indices, visuals);
for (var i = 0; i < Atmospherics.Directions; i++)
{
var direction = (AtmosDirection) (1 << i);
var otherIndices = indices.Offset(direction);
if (atmosphere.Tiles.TryGetValue(otherIndices, out var otherTile))
AddActiveTile(atmosphere, otherTile);
}
DebugTools.Assert(atmosphere.Tiles.GetValueOrDefault(tile.GridIndices) == tile);
UpdateAdjacentTiles(ent, tile, activate: true);
UpdateTileAir(ent, tile, volume);
InvalidateVisuals(uid, tile.GridIndices, visuals);
if (number++ < InvalidCoordinatesLagCheckIterations)
continue;
@@ -149,12 +94,185 @@ namespace Content.Server.Atmos.EntitySystems
number = 0;
// Process the rest next time.
if (_simulationStopwatch.Elapsed.TotalMilliseconds >= AtmosMaxProcessTime)
{
return false;
}
TrimDisconnectedMapTiles(ent);
return true;
}
/// <summary>
/// This method queued a tile and all of its neighbours up for processing by <see cref="TrimDisconnectedMapTiles"/>.
/// </summary>
public void QueueTileTrim(GridAtmosphereComponent atmos, TileAtmosphere tile)
{
if (!tile.TrimQueued)
{
tile.TrimQueued = true;
atmos.PossiblyDisconnectedTiles.Add(tile);
}
for (var i = 0; i < Atmospherics.Directions; i++)
{
var direction = (AtmosDirection) (1 << i);
var indices = tile.GridIndices.Offset(direction);
if (atmos.Tiles.TryGetValue(indices, out var adj)
&& adj.NoGridTile
&& !adj.TrimQueued)
{
adj.TrimQueued = true;
atmos.PossiblyDisconnectedTiles.Add(adj);
}
}
}
/// <summary>
/// Tiles in a <see cref="GridAtmosphereComponent"/> are either grid-tiles, or they they should be are tiles
/// adjacent to grid-tiles that represent the map's atmosphere. This method trims any map-tiles that are no longer
/// adjacent to any grid-tiles.
/// </summary>
private void TrimDisconnectedMapTiles(
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent)
{
var atmos = ent.Comp1;
foreach (var tile in atmos.PossiblyDisconnectedTiles)
{
tile.TrimQueued = false;
if (!tile.NoGridTile)
continue;
var connected = false;
for (var i = 0; i < Atmospherics.Directions; i++)
{
var indices = tile.GridIndices.Offset((AtmosDirection) (1 << i));
if (_map.TryGetTile(ent.Comp3, indices, out var gridTile) && !gridTile.IsEmpty)
{
connected = true;
break;
}
}
if (!connected)
{
RemoveActiveTile(atmos, tile);
atmos.Tiles.Remove(tile.GridIndices);
}
}
return true;
atmos.PossiblyDisconnectedTiles.Clear();
}
/// <summary>
/// Checks whether a tile has a corresponding grid-tile, or whether it is a "map" tile. Also checks whether the
/// tile should be considered "space"
/// </summary>
private void UpdateTileData(
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
MapAtmosphereComponent? mapAtmos,
TileAtmosphere tile)
{
var idx = tile.GridIndices;
bool mapAtmosphere;
if (_map.TryGetTile(ent.Comp3, idx, out var gTile) && !gTile.IsEmpty)
{
var contentDef = (ContentTileDefinition) _tileDefinitionManager[gTile.TypeId];
mapAtmosphere = contentDef.MapAtmosphere;
tile.ThermalConductivity = contentDef.ThermalConductivity;
tile.HeatCapacity = contentDef.HeatCapacity;
tile.NoGridTile = false;
}
else
{
mapAtmosphere = true;
tile.ThermalConductivity = 0.5f;
tile.HeatCapacity = float.PositiveInfinity;
if (!tile.NoGridTile)
{
tile.NoGridTile = true;
// This tile just became a non-grid atmos tile.
// It, or one of its neighbours, might now be completely disconnected from the grid.
QueueTileTrim(ent.Comp1, tile);
}
}
UpdateAirtightData(ent.Owner, ent.Comp1, ent.Comp3, tile);
if (mapAtmosphere)
{
if (!tile.MapAtmosphere)
{
(tile.Air, tile.Space) = GetDefaultMapAtmosphere(mapAtmos);
tile.MapAtmosphere = true;
ent.Comp1.MapTiles.Add(tile);
}
DebugTools.AssertNotNull(tile.Air);
DebugTools.Assert(tile.Air?.Immutable ?? false);
return;
}
if (!tile.MapAtmosphere)
return;
// Tile used to be exposed to the map's atmosphere, but isn't anymore.
RemoveMapAtmos(ent.Comp1, tile);
}
private void RemoveMapAtmos(GridAtmosphereComponent atmos, TileAtmosphere tile)
{
DebugTools.Assert(tile.MapAtmosphere);
DebugTools.AssertNotNull(tile.Air);
DebugTools.Assert(tile.Air?.Immutable ?? false);
tile.MapAtmosphere = false;
atmos.MapTiles.Remove(tile);
tile.Air = null;
Array.Clear(tile.MolesArchived);
tile.ArchivedCycle = 0;
tile.LastShare = 0f;
tile.Space = false;
}
/// <summary>
/// Check whether a grid-tile should have an air mixture, and give it one if it doesn't already have one.
/// </summary>
private void UpdateTileAir(
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
TileAtmosphere tile,
float volume)
{
if (tile.MapAtmosphere)
{
DebugTools.AssertNotNull(tile.Air);
DebugTools.Assert(tile.Air?.Immutable ?? false);
return;
}
var data = tile.AirtightData;
var fullyBlocked = data.BlockedDirections == AtmosDirection.All;
if (fullyBlocked && data.NoAirWhenBlocked)
{
if (tile.Air == null)
return;
tile.Air = null;
Array.Clear(tile.MolesArchived);
tile.ArchivedCycle = 0;
tile.LastShare = 0f;
tile.Hotspot = new Hotspot();
return;
}
if (tile.Air != null)
return;
tile.Air = new GasMixture(volume){Temperature = Atmospherics.T20C};
if (data.FixVacuum)
GridFixTileVacuum(tile);
}
private void QueueRunTiles(
@@ -170,19 +288,16 @@ namespace Content.Server.Atmos.EntitySystems
}
}
private bool ProcessTileEqualize(Entity<GridAtmosphereComponent> ent, GasTileOverlayComponent? visuals)
private bool ProcessTileEqualize(Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent)
{
var (uid, atmosphere) = ent;
var atmosphere = ent.Comp1;
if (!atmosphere.ProcessingPaused)
QueueRunTiles(atmosphere.CurrentRunTiles, atmosphere.ActiveTiles);
if (!TryComp(uid, out MapGridComponent? mapGridComp))
throw new Exception("Tried to process a grid atmosphere on an entity that isn't a grid!");
var number = 0;
while (atmosphere.CurrentRunTiles.TryDequeue(out var tile))
{
EqualizePressureInZone((uid, mapGridComp, atmosphere), tile, atmosphere.UpdateCounter, visuals);
EqualizePressureInZone(ent, tile, atmosphere.UpdateCounter);
if (number++ < LagCheckIterations)
continue;
@@ -198,7 +313,7 @@ namespace Content.Server.Atmos.EntitySystems
return true;
}
private bool ProcessActiveTiles(GridAtmosphereComponent atmosphere, GasTileOverlayComponent? visuals)
private bool ProcessActiveTiles(GridAtmosphereComponent atmosphere, GasTileOverlayComponent visuals)
{
if(!atmosphere.ProcessingPaused)
QueueRunTiles(atmosphere.CurrentRunTiles, atmosphere.ActiveTiles);
@@ -240,11 +355,11 @@ namespace Content.Server.Atmos.EntitySystems
excitedGroup.BreakdownCooldown++;
excitedGroup.DismantleCooldown++;
if(excitedGroup.BreakdownCooldown > Atmospherics.ExcitedGroupBreakdownCycles)
if (excitedGroup.BreakdownCooldown > Atmospherics.ExcitedGroupBreakdownCycles)
ExcitedGroupSelfBreakdown(gridAtmosphere, excitedGroup);
else if(excitedGroup.DismantleCooldown > Atmospherics.ExcitedGroupsDismantleCycles)
ExcitedGroupDismantle(gridAtmosphere, excitedGroup);
else if (excitedGroup.DismantleCooldown > Atmospherics.ExcitedGroupsDismantleCycles)
DeactivateGroupTiles(gridAtmosphere, excitedGroup);
// TODO ATMOS. What is the point of this? why is this only de-exciting the group? Shouldn't it also dismantle it?
if (number++ < LagCheckIterations)
continue;
@@ -435,10 +550,10 @@ namespace Content.Server.Atmos.EntitySystems
_currentRunAtmosphereIndex = 0;
_currentRunAtmosphere.Clear();
var query = EntityQueryEnumerator<GridAtmosphereComponent>();
while (query.MoveNext(out var uid, out var grid))
var query = EntityQueryEnumerator<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent>();
while (query.MoveNext(out var uid, out var atmos, out var overlay, out var grid, out var xform ))
{
_currentRunAtmosphere.Add((uid, grid));
_currentRunAtmosphere.Add((uid, atmos, overlay, grid, xform));
}
}
@@ -448,8 +563,7 @@ namespace Content.Server.Atmos.EntitySystems
for (; _currentRunAtmosphereIndex < _currentRunAtmosphere.Count; _currentRunAtmosphereIndex++)
{
var ent = _currentRunAtmosphere[_currentRunAtmosphereIndex];
var (owner, atmosphere) = ent;
TryComp(owner, out GasTileOverlayComponent? visuals);
var (owner, atmosphere, visuals, grid, xform) = ent;
if (!TryComp(owner, out TransformComponent? x)
|| x.MapUid == null
@@ -474,13 +588,14 @@ namespace Content.Server.Atmos.EntitySystems
switch (atmosphere.State)
{
case AtmosphereProcessingState.Revalidate:
if (!ProcessRevalidate(ent, visuals))
if (!ProcessRevalidate(ent))
{
atmosphere.ProcessingPaused = true;
return;
}
atmosphere.ProcessingPaused = 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.
@@ -489,7 +604,7 @@ namespace Content.Server.Atmos.EntitySystems
: AtmosphereProcessingState.ActiveTiles;
continue;
case AtmosphereProcessingState.TileEqualize:
if (!ProcessTileEqualize(ent, visuals))
if (!ProcessTileEqualize(ent))
{
atmosphere.ProcessingPaused = true;
return;
@@ -499,7 +614,7 @@ namespace Content.Server.Atmos.EntitySystems
atmosphere.State = AtmosphereProcessingState.ActiveTiles;
continue;
case AtmosphereProcessingState.ActiveTiles:
if (!ProcessActiveTiles(atmosphere, visuals))
if (!ProcessActiveTiles(ent, ent))
{
atmosphere.ProcessingPaused = true;
return;
@@ -520,7 +635,7 @@ namespace Content.Server.Atmos.EntitySystems
atmosphere.State = AtmosphereProcessingState.HighPressureDelta;
continue;
case AtmosphereProcessingState.HighPressureDelta:
if (!ProcessHighPressureDelta(ent))
if (!ProcessHighPressureDelta((ent, ent)))
{
atmosphere.ProcessingPaused = true;
return;

View File

@@ -1,5 +1,6 @@
using Content.Server.Atmos.Components;
using Content.Shared.Atmos;
using Robust.Shared.Map.Components;
namespace Content.Server.Atmos.EntitySystems
{
@@ -12,7 +13,8 @@ namespace Content.Server.Atmos.EntitySystems
for(var i = 0; i < Atmospherics.Directions; i++)
{
var direction = (AtmosDirection) (1 << i);
if (!directions.IsFlagSet(direction)) continue;
if (!directions.IsFlagSet(direction))
continue;
var adjacent = tile.AdjacentTiles[direction.ToIndex()];
@@ -92,7 +94,9 @@ namespace Content.Server.Atmos.EntitySystems
{
if (tile.Air == null)
{
if (other.Tile != null)
// TODO ATMOS: why does this need to check if a tile exists if it doesn't use the tile?
if (TryComp<MapGridComponent>(other.GridIndex, out var grid)
&& _mapSystem.TryGetTileRef(other.GridIndex, grid, other.GridIndices, out var _))
{
TemperatureShareOpenToSolid(other, tile);
}

View File

@@ -41,20 +41,6 @@ public partial class AtmosphereSystem
_gasTileOverlaySystem.Invalidate(gridUid, tile, comp);
}
public bool NeedsVacuumFixing(MapGridComponent mapGrid, Vector2i indices)
{
var value = false;
var enumerator = GetObstructingComponentsEnumerator(mapGrid, indices);
while (enumerator.MoveNext(out var airtight))
{
value |= airtight.FixVacuum;
}
return value;
}
/// <summary>
/// Gets the volume in liters for a number of tiles, on a specific grid.
/// </summary>
@@ -66,34 +52,45 @@ public partial class AtmosphereSystem
return Atmospherics.CellVolume * mapGrid.TileSize * tiles;
}
/// <summary>
/// Gets all obstructing <see cref="AirtightComponent"/> instances in a specific tile.
/// </summary>
/// <param name="mapGrid">The grid where to get the tile.</param>
/// <param name="tile">The indices of the tile.</param>
/// <returns>The enumerator for the airtight components.</returns>
public AtmosObstructionEnumerator GetObstructingComponentsEnumerator(MapGridComponent mapGrid, Vector2i tile)
{
var ancEnumerator = mapGrid.GetAnchoredEntitiesEnumerator(tile);
var airQuery = GetEntityQuery<AirtightComponent>();
public readonly record struct AirtightData(AtmosDirection BlockedDirections, bool NoAirWhenBlocked,
bool FixVacuum);
var enumerator = new AtmosObstructionEnumerator(ancEnumerator, airQuery);
return enumerator;
private void UpdateAirtightData(EntityUid uid, GridAtmosphereComponent atmos, MapGridComponent grid, TileAtmosphere tile)
{
var oldBlocked = tile.AirtightData.BlockedDirections;
tile.AirtightData = tile.NoGridTile
? default
: GetAirtightData(uid, grid, tile.GridIndices);
if (tile.AirtightData.BlockedDirections != oldBlocked && tile.ExcitedGroup != null)
ExcitedGroupDispose(atmos, tile.ExcitedGroup);
}
private AtmosDirection GetBlockedDirections(MapGridComponent mapGrid, Vector2i indices)
private AirtightData GetAirtightData(EntityUid uid, MapGridComponent grid, Vector2i tile)
{
var value = AtmosDirection.Invalid;
var blockedDirs = AtmosDirection.Invalid;
var noAirWhenBlocked = false;
var fixVacuum = false;
var enumerator = GetObstructingComponentsEnumerator(mapGrid, indices);
while (enumerator.MoveNext(out var airtight))
foreach (var ent in _map.GetAnchoredEntities(uid, grid, tile))
{
if(airtight.AirBlocked)
value |= airtight.AirBlockedDirection;
if (!_airtightQuery.TryGetComponent(ent, out var airtight))
continue;
fixVacuum |= airtight.FixVacuum;
if(!airtight.AirBlocked)
continue;
blockedDirs |= airtight.AirBlockedDirection;
noAirWhenBlocked |= airtight.NoAirWhenFullyAirBlocked;
if (blockedDirs == AtmosDirection.All && noAirWhenBlocked && fixVacuum)
break;
}
return value;
return new AirtightData(blockedDirs, noAirWhenBlocked, fixVacuum);
}
/// <summary>

View File

@@ -4,6 +4,7 @@ using Content.Server.Body.Systems;
using Content.Server.Fluids.EntitySystems;
using Content.Server.NodeContainer.EntitySystems;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Doors.Components;
using Content.Shared.Maps;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
@@ -40,6 +41,9 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
private const float ExposedUpdateDelay = 1f;
private float _exposedTimer = 0f;
private EntityQuery<GridAtmosphereComponent> _atmosQuery;
private EntityQuery<AirtightComponent> _airtightQuery;
private EntityQuery<FirelockComponent> _firelockQuery;
private HashSet<EntityUid> _entSet = new();
public override void Initialize()
@@ -55,6 +59,9 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
InitializeGridAtmosphere();
InitializeMap();
_atmosQuery = GetEntityQuery<GridAtmosphereComponent>();
_airtightQuery = GetEntityQuery<AirtightComponent>();
_firelockQuery = GetEntityQuery<FirelockComponent>();
SubscribeLocalEvent<TileChangedEvent>(OnTileChanged);

View File

@@ -27,8 +27,12 @@ public sealed class AutomaticAtmosSystem : EntitySystem
// Also, these calls are surprisingly slow.
// TODO: Make tiledefmanager cache the IsSpace property, and turn this lookup-through-two-interfaces into
// TODO: a simple array lookup, as tile IDs are likely contiguous, and there's at most 2^16 possibilities anyway.
if (!((ev.OldTile.IsSpace(_tileDefinitionManager) && !ev.NewTile.IsSpace(_tileDefinitionManager)) ||
(!ev.OldTile.IsSpace(_tileDefinitionManager) && ev.NewTile.IsSpace(_tileDefinitionManager))) ||
var oldSpace = ev.OldTile.IsSpace(_tileDefinitionManager);
var newSpace = ev.NewTile.IsSpace(_tileDefinitionManager);
if (!(oldSpace && !newSpace ||
!oldSpace && newSpace) ||
_atmosphereSystem.HasAtmosphere(ev.Entity))
return;

View File

@@ -260,13 +260,13 @@ namespace Content.Server.Atmos.EntitySystems
{
var gas = _atmo.GetGas(i);
if (mixture?.Moles[i] <= UIMinMoles)
if (mixture?[i] <= UIMinMoles)
continue;
if (mixture != null)
{
var gasName = Loc.GetString(gas.Name);
gases.Add(new GasEntry(gasName, mixture.Moles[i], gas.Color));
gases.Add(new GasEntry(gasName, mixture[i], gas.Color));
}
}

View File

@@ -172,7 +172,7 @@ namespace Content.Server.Atmos.EntitySystems
{
var id = VisibleGasId[i];
var gas = _atmosphereSystem.GetGas(id);
var moles = mixture?.Moles[id] ?? 0f;
var moles = mixture?[id] ?? 0f;
ref var opacity = ref data.Opacity[i];
if (moles < gas.GasMolesVisible)
@@ -217,13 +217,13 @@ namespace Content.Server.Atmos.EntitySystems
oldData = new GasOverlayData(tile.Hotspot.State, oldData.Opacity);
}
if (tile.Air != null)
if (tile is {Air: not null, NoGridTile: false})
{
for (var i = 0; i < VisibleGasId.Length; i++)
{
var id = VisibleGasId[i];
var gas = _atmosphereSystem.GetGas(id);
var moles = tile.Air.Moles[id];
var moles = tile.Air[id];
ref var oldOpacity = ref oldData.Opacity[i];
if (moles < gas.GasMolesVisible)

View File

@@ -3,6 +3,7 @@ using System.Linq;
using System.Runtime.CompilerServices;
using Content.Server.Atmos.Reactions;
using Content.Shared.Atmos;
using Content.Shared.Atmos.EntitySystems;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
@@ -17,11 +18,13 @@ namespace Content.Server.Atmos
{
public static GasMixture SpaceGas => new() {Volume = Atmospherics.CellVolume, Temperature = Atmospherics.TCMB, Immutable = true};
// This must always have a length that is a multiple of 4 for SIMD acceleration.
[DataField("moles")]
[ViewVariables(VVAccess.ReadWrite)]
// No access, to ensure immutable mixtures are never accidentally mutated.
[Access(typeof(SharedAtmosphereSystem), typeof(SharedAtmosDebugOverlaySystem), Other = AccessPermissions.None)]
[DataField]
public float[] Moles = new float[Atmospherics.AdjustedNumberOfGases];
public float this[int gas] => Moles[gas];
[DataField("temperature")]
[ViewVariables(VVAccess.ReadWrite)]
private float _temperature = Atmospherics.TCMB;
@@ -59,9 +62,9 @@ namespace Content.Server.Atmos
get => _temperature;
set
{
DebugTools.Assert(!float.IsNaN(_temperature));
if (Immutable) return;
_temperature = MathF.Min(MathF.Max(value, Atmospherics.TCMB), Atmospherics.Tmax);
DebugTools.Assert(!float.IsNaN(value));
if (!Immutable)
_temperature = MathF.Min(MathF.Max(value, Atmospherics.TCMB), Atmospherics.Tmax);
}
}
@@ -80,6 +83,20 @@ namespace Content.Server.Atmos
Volume = volume;
}
public GasMixture(float[] moles, float temp, float volume = Atmospherics.CellVolume)
{
if (moles.Length != Atmospherics.AdjustedNumberOfGases)
throw new InvalidOperationException($"Invalid mole array length");
if (volume < 0)
volume = 0;
DebugTools.Assert(!float.IsNaN(temp));
_temperature = temp;
Moles = moles;
Volume = volume;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MarkImmutable()
{
@@ -117,15 +134,16 @@ namespace Content.Server.Atmos
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AdjustMoles(int gasId, float quantity)
{
if (!Immutable)
{
if (!float.IsFinite(quantity))
throw new ArgumentException($"Invalid quantity \"{quantity}\" specified!", nameof(quantity));
if (Immutable)
return;
// Clamping is needed because x - x can be negative with floating point numbers. If we don't
// clamp here, the caller always has to call GetMoles(), clamp, then SetMoles().
Moles[gasId] = MathF.Max(Moles[gasId] + quantity, 0);
}
if (!float.IsFinite(quantity))
throw new ArgumentException($"Invalid quantity \"{quantity}\" specified!", nameof(quantity));
// Clamping is needed because x - x can be negative with floating point numbers. If we don't
// clamp here, the caller always has to call GetMoles(), clamp, then SetMoles().
ref var moles = ref Moles[gasId];
moles = MathF.Max(moles + quantity, 0);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -163,7 +181,8 @@ namespace Content.Server.Atmos
{
var moles = Moles[i];
var otherMoles = removed.Moles[i];
if (moles < Atmospherics.GasMinMoles || float.IsNaN(moles))
if ((moles < Atmospherics.GasMinMoles || float.IsNaN(moles)) && !Immutable)
Moles[i] = 0;
if (otherMoles < Atmospherics.GasMinMoles || float.IsNaN(otherMoles))
@@ -202,6 +221,9 @@ namespace Content.Server.Atmos
void ISerializationHooks.AfterDeserialization()
{
// ISerializationHooks is obsolete.
// TODO add fixed-length-array serializer
// The arrays MUST have a specific length.
Array.Resize(ref Moles, Atmospherics.AdjustedNumberOfGases);
}
@@ -229,8 +251,12 @@ namespace Content.Server.Atmos
public bool Equals(GasMixture? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
if (ReferenceEquals(this, other))
return true;
if (ReferenceEquals(null, other))
return false;
return Moles.SequenceEqual(other.Moles)
&& _temperature.Equals(other._temperature)
&& ReactionResults.SequenceEqual(other.ReactionResults)
@@ -258,11 +284,13 @@ namespace Content.Server.Atmos
public GasMixture Clone()
{
if (Immutable)
return this;
var newMixture = new GasMixture()
{
Moles = (float[])Moles.Clone(),
_temperature = _temperature,
Immutable = Immutable,
Volume = Volume,
};
return newMixture;

View File

@@ -3,14 +3,12 @@ using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
namespace Content.Server.Atmos.Piping.EntitySystems;
public sealed class AtmosPipeAppearanceSystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
public override void Initialize()
@@ -31,7 +29,7 @@ public sealed class AtmosPipeAppearanceSystem : EntitySystem
if (!Resolve(uid, ref appearance, ref container, ref xform, false))
return;
if (!_mapManager.TryGetGrid(xform.GridUid, out var grid))
if (!TryComp<MapGridComponent>(xform.GridUid, out var grid))
return;
// get connected entities

View File

@@ -136,7 +136,7 @@ public sealed class GasCanisterSystem : EntitySystem
for (int i = 0; i < containedGasArray.Length; i++)
{
containedGasDict.Add((Gas)i, canister.Air.Moles[i]);
containedGasDict.Add((Gas)i, canister.Air[i]);
}
_adminLogger.Add(LogType.CanisterValve, impact, $"{ToPrettyString(args.Session.AttachedEntity.GetValueOrDefault()):player} set the valve on {ToPrettyString(uid):canister} to {args.Valve:valveState} while it contained [{string.Join(", ", containedGasDict)}]");

View File

@@ -45,7 +45,7 @@ public sealed class GasCondenserSystem : EntitySystem
var removed = inlet.Air.Remove(molesToConvert);
for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
{
var moles = removed.Moles[i];
var moles = removed[i];
if (moles <= 0)
continue;

View File

@@ -7,15 +7,14 @@ using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.Construction.Components;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
namespace Content.Server.Atmos.Piping.Unary.EntitySystems
{
[UsedImplicitly]
public sealed class GasPortableSystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
@@ -58,7 +57,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
{
port = null;
if (!_mapManager.TryGetGrid(gridId, out var grid))
if (!TryComp<MapGridComponent>(gridId, out var grid))
return false;
foreach (var entityUid in grid.GetLocal(coordinates))

View File

@@ -183,13 +183,7 @@ public sealed partial class TileAtmosCollectionSerializer : ITypeSerializer<Dict
target.Clear();
foreach (var (key, val) in source)
{
target.Add(key,
new TileAtmosphere(
val.GridIndex,
val.GridIndices,
val.Air?.Clone(),
val.Air?.Immutable ?? false,
val.Space));
target.Add(key, new TileAtmosphere(val));
}
}
}

View File

@@ -1,3 +1,4 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Maps;
@@ -27,6 +28,9 @@ namespace Content.Server.Atmos
[ViewVariables]
public TileAtmosphere? PressureSpecificTarget { get; set; }
/// <summary>
/// This is either the pressure difference, or the quantity of moles transferred if monstermos is enabled.
/// </summary>
[ViewVariables]
public float PressureDifference { get; set; }
@@ -51,6 +55,10 @@ namespace Content.Server.Atmos
[ViewVariables]
public readonly TileAtmosphere?[] AdjacentTiles = new TileAtmosphere[Atmospherics.Directions];
/// <summary>
/// Neighbouring tiles to which air can flow. This is a combination of this tile's unblocked direction, and the
/// unblocked directions on adjacent tiles.
/// </summary>
[ViewVariables]
public AtmosDirection AdjacentBits = AtmosDirection.Invalid;
@@ -72,10 +80,7 @@ namespace Content.Server.Atmos
public EntityUid GridIndex { get; set; }
[ViewVariables]
public TileRef? Tile => GridIndices.GetTileRef(GridIndex);
[ViewVariables]
public Vector2i GridIndices { get; }
public Vector2i GridIndices;
[ViewVariables]
public ExcitedGroup? ExcitedGroup { get; set; }
@@ -92,7 +97,7 @@ namespace Content.Server.Atmos
public float LastShare;
[ViewVariables]
public float[]? MolesArchived;
public readonly float[] MolesArchived = new float[Atmospherics.AdjustedNumberOfGases];
GasMixture IGasMixtureHolder.Air
{
@@ -103,8 +108,31 @@ namespace Content.Server.Atmos
[ViewVariables]
public float MaxFireTemperatureSustained { get; set; }
/// <summary>
/// If true, then this tile is directly exposed to the map's atmosphere, either because the grid has no tile at
/// this position, or because the tile type is not airtight.
/// </summary>
[ViewVariables]
public AtmosDirection BlockedAirflow { get; set; } = AtmosDirection.Invalid;
public bool MapAtmosphere;
/// <summary>
/// If true, this tile does not actually exist on the grid, it only exists to represent the map's atmosphere for
/// adjacent grid tiles.
/// </summary>
[ViewVariables]
public bool NoGridTile;
/// <summary>
/// If true, this tile is queued for processing in <see cref="GridAtmosphereComponent.PossiblyDisconnectedTiles"/>
/// </summary>
[ViewVariables]
public bool TrimQueued;
/// <summary>
/// Cached information about airtight entities on this tile. This gets updated anytime a tile gets invalidated
/// (i.e., gets added to <see cref="GridAtmosphereComponent.InvalidatedCoords"/>).
/// </summary>
public AtmosphereSystem.AirtightData AirtightData;
public TileAtmosphere(EntityUid gridIndex, Vector2i gridIndices, GasMixture? mixture = null, bool immutable = false, bool space = false)
{
@@ -112,10 +140,24 @@ namespace Content.Server.Atmos
GridIndices = gridIndices;
Air = mixture;
Space = space;
MolesArchived = Air != null ? new float[Atmospherics.AdjustedNumberOfGases] : null;
if(immutable)
Air?.MarkImmutable();
}
public TileAtmosphere(TileAtmosphere other)
{
GridIndex = other.GridIndex;
GridIndices = other.GridIndices;
Space = other.Space;
NoGridTile = other.NoGridTile;
MapAtmosphere = other.MapAtmosphere;
Air = other.Air?.Clone();
Array.Copy(other.MolesArchived, MolesArchived, MolesArchived.Length);
}
public TileAtmosphere()
{
}
}
}

View File

@@ -0,0 +1,10 @@
namespace Content.Server.Bed.Sleep;
/// <summary>
/// This is used for the snoring trait.
/// </summary>
[RegisterComponent]
public sealed partial class SnoringComponent : Component
{
}

View File

@@ -67,7 +67,10 @@ namespace Content.Server.Bed.Sleep
if (TryComp<SleepEmitSoundComponent>(uid, out var sleepSound))
{
var emitSound = EnsureComp<SpamEmitSoundComponent>(uid);
emitSound.Sound = sleepSound.Snore;
if (HasComp<SnoringComponent>(uid))
{
emitSound.Sound = sleepSound.Snore;
}
emitSound.PlayChance = sleepSound.Chance;
emitSound.RollInterval = sleepSound.Interval;
emitSound.PopUp = sleepSound.PopUp;

View File

@@ -77,7 +77,7 @@ public sealed class LungSystem : EntitySystem
foreach (var gas in Enum.GetValues<Gas>())
{
var i = (int) gas;
var moles = lung.Air.Moles[i];
var moles = lung.Air[i];
if (moles <= 0)
continue;
var reagent = _atmosphereSystem.GasReagents[i];

View File

@@ -128,9 +128,23 @@ namespace Content.Server.Chat.Managers
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Server message to {player:Player}: {message}");
}
public void SendAdminAnnouncement(string message)
public void SendAdminAnnouncement(string message, AdminFlags? flagBlacklist, AdminFlags? flagWhitelist)
{
var clients = _adminManager.ActiveAdmins.Select(p => p.Channel);
var clients = _adminManager.ActiveAdmins.Where(p =>
{
var adminData = _adminManager.GetAdminData(p);
DebugTools.AssertNotNull(adminData);
if (adminData == null)
return false;
if (flagBlacklist != null && adminData.HasFlag(flagBlacklist.Value))
return false;
return flagWhitelist == null || adminData.HasFlag(flagWhitelist.Value);
}).Select(p => p.Channel);
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")), ("message", FormattedMessage.EscapeText(message)));

View File

@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Administration;
using Content.Shared.Chat;
using Robust.Shared.Network;
using Robust.Shared.Player;
@@ -21,7 +22,7 @@ namespace Content.Server.Chat.Managers
void TrySendOOCMessage(ICommonSession player, string message, OOCChatType type);
void SendHookOOC(string sender, string message);
void SendAdminAnnouncement(string message);
void SendAdminAnnouncement(string message, AdminFlags? flagBlacklist = null, AdminFlags? flagWhitelist = null);
void SendAdminAlert(string message);
void SendAdminAlert(EntityUid player, string message);

View File

@@ -22,8 +22,8 @@ namespace Content.Server.Chemistry.EntitySystems
[UsedImplicitly]
internal sealed class VaporSystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly ThrowingSystem _throwing = default!;
@@ -64,8 +64,8 @@ namespace Content.Server.Chemistry.EntitySystems
// Set Move
if (EntityManager.TryGetComponent(vapor, out PhysicsComponent? physics))
{
_physics.SetLinearDamping(physics, 0f);
_physics.SetAngularDamping(physics, 0f);
_physics.SetLinearDamping(vapor, physics, 0f);
_physics.SetAngularDamping(vapor, physics, 0f);
_throwing.TryThrow(vapor, dir, speed, user: user);
@@ -115,7 +115,7 @@ namespace Content.Server.Chemistry.EntitySystems
{
vapor.ReactTimer = 0;
var tile = gridComp.GetTileRef(xform.Coordinates.ToVector2i(EntityManager, _mapManager));
var tile = _map.GetTileRef(xform.GridUid.Value, gridComp, xform.Coordinates);
foreach (var reagentQuantity in contents.Contents.ToArray())
{
if (reagentQuantity.Quantity == FixedPoint2.Zero) continue;

View File

@@ -16,12 +16,15 @@ public sealed partial class ModifyLungGas : ReagentEffect
public override void Effect(ReagentEffectArgs args)
{
if (args.EntityManager.TryGetComponent<LungComponent>(args.OrganEntity, out var lung))
if (!args.EntityManager.TryGetComponent<LungComponent>(args.OrganEntity, out var lung))
return;
foreach (var (gas, ratio) in _ratios)
{
foreach (var (gas, ratio) in _ratios)
{
lung.Air.Moles[(int) gas] += (ratio * args.Quantity.Float()) / Atmospherics.BreathMolesToReagentMultiplier;
}
var quantity = ratio * args.Quantity.Float() / Atmospherics.BreathMolesToReagentMultiplier;
if (quantity < 0)
quantity = Math.Max(quantity, -lung.Air[(int)gas]);
lung.Air.AdjustMoles(gas, quantity);
}
}
}

View File

@@ -3,6 +3,7 @@ using Content.Shared.Examine;
using Content.Shared.Maps;
using JetBrains.Annotations;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
namespace Content.Server.Construction.Conditions
@@ -46,9 +47,18 @@ namespace Content.Server.Construction.Conditions
if (transform.GridUid == null)
return false;
var indices = transform.Coordinates.ToVector2i(entityManager, IoCManager.Resolve<IMapManager>());
var transformSys = entityManager.System<SharedTransformSystem>();
var indices = transform.Coordinates.ToVector2i(entityManager, IoCManager.Resolve<IMapManager>(), transformSys);
var lookup = entityManager.EntitySysManager.GetEntitySystem<EntityLookupSystem>();
var entities = indices.GetEntitiesInTile(transform.GridUid.Value, LookupFlags.Approximate | LookupFlags.Static, lookup);
if (!entityManager.TryGetComponent<MapGridComponent>(transform.GridUid.Value, out var grid))
return !HasEntity;
if (!entityManager.System<SharedMapSystem>().TryGetTileRef(transform.GridUid.Value, grid, indices, out var tile))
return !HasEntity;
var entities = tile.GetEntitiesInTile(LookupFlags.Approximate | LookupFlags.Static, lookup);
foreach (var ent in entities)
{

View File

@@ -2,7 +2,6 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.Construction.Components;
using Content.Server.Storage.EntitySystems;
using Content.Shared.ActionBlocker;
using Content.Shared.Construction;
using Content.Shared.Construction.Prototypes;
@@ -15,7 +14,6 @@ using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Storage;
using Content.Shared.Tag;
using Robust.Shared.Containers;
using Robust.Shared.Player;
using Robust.Shared.Timing;
@@ -30,8 +28,7 @@ namespace Content.Server.Construction
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly EntityLookupSystem _lookupSystem = default!;
[Dependency] private readonly StorageSystem _storageSystem = default!;
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
// --- WARNING! LEGACY CODE AHEAD! ---
// This entire file contains the legacy code for initial construction.
@@ -465,7 +462,7 @@ namespace Content.Server.Construction
return;
}
var mapPos = location.ToMap(EntityManager);
var mapPos = location.ToMap(EntityManager, _transformSystem);
var predicate = GetPredicate(constructionPrototype.CanBuildInImpassable, mapPos);
if (!_interactionSystem.InRangeUnobstructed(user, mapPos, predicate: predicate))

View File

@@ -111,7 +111,8 @@ public sealed record AdminMessageRecord(
bool Deleted,
PlayerRecord? DeletedBy,
DateTimeOffset? DeletedAt,
bool Seen) : IAdminRemarksRecord;
bool Seen,
bool Dismissed) : IAdminRemarksRecord;
public sealed record PlayerRecord(

View File

@@ -1151,7 +1151,8 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
entity.Deleted,
MakePlayerRecord(entity.DeletedBy),
NormalizeDatabaseTime(entity.DeletedAt),
entity.Seen);
entity.Seen,
entity.Dismissed);
}
public async Task<ServerBanNoteRecord?> GetServerBanAsNoteAsync(int id)
@@ -1430,11 +1431,13 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
return entities.Select(MakeAdminMessageRecord).ToList();
}
public async Task MarkMessageAsSeen(int id)
public async Task MarkMessageAsSeen(int id, bool dismissedToo)
{
await using var db = await GetDb();
var message = await db.DbContext.AdminMessages.SingleAsync(m => m.Id == id);
message.Seen = true;
if (dismissedToo)
message.Dismissed = true;
await db.DbContext.SaveChangesAsync();
}

Some files were not shown because too many files have changed in this diff Show More