diff --git a/.editorconfig b/.editorconfig
index a87e5e0651..fa740a7511 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -351,7 +351,7 @@ resharper_csharp_qualified_using_at_nested_scope = false
resharper_csharp_prefer_qualified_reference = false
resharper_csharp_allow_alias = false
-[*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets,props}]
+[*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets,props,slnx}]
indent_size = 2
[nuget.config]
diff --git a/.github/workflows/build-docfx.yml b/.github/workflows/build-docfx.yml
index 1f010b7291..dee31e9b31 100644
--- a/.github/workflows/build-docfx.yml
+++ b/.github/workflows/build-docfx.yml
@@ -21,7 +21,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
- dotnet-version: 9.0.x
+ dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore
diff --git a/.github/workflows/build-map-renderer.yml b/.github/workflows/build-map-renderer.yml
index e3971024d6..04891d378f 100644
--- a/.github/workflows/build-map-renderer.yml
+++ b/.github/workflows/build-map-renderer.yml
@@ -36,7 +36,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
- dotnet-version: 9.0.x
+ dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore
diff --git a/.github/workflows/build-test-debug.yml b/.github/workflows/build-test-debug.yml
index decc74d255..961145ecb8 100644
--- a/.github/workflows/build-test-debug.yml
+++ b/.github/workflows/build-test-debug.yml
@@ -36,7 +36,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
- dotnet-version: 9.0.x
+ dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 5dfa0358f7..3418834a81 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -23,7 +23,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
- dotnet-version: 9.0.x
+ dotnet-version: 10.0.x
- name: Get Engine Tag
run: |
diff --git a/.github/workflows/test-packaging.yml b/.github/workflows/test-packaging.yml
index 38a6dcd6ed..490e31f735 100644
--- a/.github/workflows/test-packaging.yml
+++ b/.github/workflows/test-packaging.yml
@@ -51,7 +51,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
- dotnet-version: 9.0.x
+ dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore
diff --git a/.github/workflows/yaml-linter.yml b/.github/workflows/yaml-linter.yml
index d7ad9b073d..ca925f4791 100644
--- a/.github/workflows/yaml-linter.yml
+++ b/.github/workflows/yaml-linter.yml
@@ -26,7 +26,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
- dotnet-version: 9.0.x
+ dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore
- name: Build
diff --git a/.gitignore b/.gitignore
index 0cd376f5dc..b033e538d0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
+# MSbuild binlog files
+*.binlog
+
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
diff --git a/BuildChecker/git_helper.py b/BuildChecker/git_helper.py
index 53526ee442..733b8b1c21 100644
--- a/BuildChecker/git_helper.py
+++ b/BuildChecker/git_helper.py
@@ -11,7 +11,7 @@ import time
from pathlib import Path
from typing import List
-SOLUTION_PATH = Path("..") / "SpaceStation14.sln"
+SOLUTION_PATH = Path("..") / "SpaceStation14.slnx"
# If this doesn't match the saved version we overwrite them all.
CURRENT_HOOKS_VERSION = "4"
QUIET = len(sys.argv) == 2 and sys.argv[1] == "--quiet"
diff --git a/Content.Benchmarks/Content.Benchmarks.csproj b/Content.Benchmarks/Content.Benchmarks.csproj
index c3b60a1c69..8d4dfa31bd 100644
--- a/Content.Benchmarks/Content.Benchmarks.csproj
+++ b/Content.Benchmarks/Content.Benchmarks.csproj
@@ -1,17 +1,20 @@
-
- $(TargetFramework)
..\bin\Content.Benchmarks\
- false
false
Exe
true
- 12
+ false
+ disable
+
+
+
+
+
@@ -19,10 +22,12 @@
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/Content.Benchmarks/DestructibleBenchmark.cs b/Content.Benchmarks/DestructibleBenchmark.cs
index 1b54bacca0..aa759c35fc 100644
--- a/Content.Benchmarks/DestructibleBenchmark.cs
+++ b/Content.Benchmarks/DestructibleBenchmark.cs
@@ -51,6 +51,8 @@ public class DestructibleBenchmark
private readonly List> _damageables = new();
private readonly List> _destructbiles = new();
+ private TestMapData _currentMapData = default!;
+
private DamageSpecifier _damage;
private TestPair _pair = default!;
@@ -70,8 +72,6 @@ public class DestructibleBenchmark
_pair = await PoolManager.GetServerClient();
var server = _pair.Server;
- var mapdata = await _pair.CreateTestMap();
-
_entMan = server.ResolveDependency();
_protoMan = server.ResolveDependency();
_random = server.ResolveDependency();
@@ -86,19 +86,25 @@ public class DestructibleBenchmark
_damage = new DamageSpecifier(type, DamageAmount);
_random.SetSeed(69420); // Randomness needs to be deterministic for benchmarking.
+ }
+ [IterationSetup]
+ public void IterationSetup()
+ {
var plating = _tileDefMan[TileRef].TileId;
+ var server = _pair.Server;
+ _currentMapData = _pair.CreateTestMap().GetAwaiter().GetResult();
// We make a rectangular grid of destructible entities, and then damage them all simultaneously to stress test the system.
// Needed for managing the performance of destructive effects and damage application.
- await server.WaitPost(() =>
+ server.WaitPost(() =>
{
// Set up a thin line of tiles to place our objects on. They should be anchored for a "realistic" scenario...
for (var x = 0; x < EntityCount; x++)
{
for (var y = 0; y < _prototypes.Length; y++)
{
- _map.SetTile(mapdata.Grid, mapdata.Grid, new Vector2i(x, y), new Tile(plating));
+ _map.SetTile(_currentMapData.Grid, _currentMapData.Grid, new Vector2i(x, y), new Tile(plating));
}
}
@@ -107,7 +113,7 @@ public class DestructibleBenchmark
var y = 0;
foreach (var protoId in _prototypes)
{
- var coords = new EntityCoordinates(mapdata.Grid, x + 0.5f, y + 0.5f);
+ var coords = new EntityCoordinates(_currentMapData.Grid, x + 0.5f, y + 0.5f);
_entMan.SpawnEntity(protoId, coords);
y++;
}
@@ -115,12 +121,17 @@ public class DestructibleBenchmark
var query = _entMan.EntityQueryEnumerator();
+ _destructbiles.EnsureCapacity(EntityCount);
+ _damageables.EnsureCapacity(EntityCount);
+
while (query.MoveNext(out var uid, out var damageable, out var destructible))
{
_damageables.Add((uid, damageable));
_destructbiles.Add((uid, damageable, destructible));
}
- });
+ })
+ .GetAwaiter()
+ .GetResult();
}
[Benchmark]
@@ -150,6 +161,26 @@ public class DestructibleBenchmark
});
}
+ [IterationCleanup]
+ public void IterationCleanupAsync()
+ {
+ // We need to nuke the entire map and respawn everything as some destructible effects
+ // spawn entities and whatnot.
+ _pair.Server.WaitPost(() =>
+ {
+ _map.QueueDeleteMap(_currentMapData.MapId);
+ })
+ .Wait();
+
+ // Deletion of entities is often queued (QueueDel) which must be processed by running ticks
+ // or else it will grow infinitely and leak memory.
+ _pair.Server.WaitRunTicks(2)
+ .GetAwaiter()
+ .GetResult();
+
+ _destructbiles.Clear();
+ _damageables.Clear();
+ }
[GlobalCleanup]
public async Task CleanupAsync()
diff --git a/Content.Benchmarks/HeatCapacityBenchmark.cs b/Content.Benchmarks/HeatCapacityBenchmark.cs
new file mode 100644
index 0000000000..cef5bc10c7
--- /dev/null
+++ b/Content.Benchmarks/HeatCapacityBenchmark.cs
@@ -0,0 +1,83 @@
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+using Content.IntegrationTests;
+using Content.IntegrationTests.Pair;
+using Content.Server.Atmos.EntitySystems;
+using Content.Shared.Atmos;
+using Robust.Shared;
+using Robust.Shared.Analyzers;
+using Robust.Shared.GameObjects;
+
+namespace Content.Benchmarks;
+
+[Virtual]
+[GcServer(true)]
+[MemoryDiagnoser]
+public class HeatCapacityBenchmark
+{
+ private TestPair _pair = default!;
+ private IEntityManager _sEntMan = default!;
+ private IEntityManager _cEntMan = default!;
+ private Client.Atmos.EntitySystems.AtmosphereSystem _cAtmos = default!;
+ private AtmosphereSystem _sAtmos = default!;
+ private GasMixture _mix;
+
+ [GlobalSetup]
+ public async Task SetupAsync()
+ {
+ ProgramShared.PathOffset = "../../../../";
+ PoolManager.Startup();
+ _pair = await PoolManager.GetServerClient();
+ await _pair.Connect();
+ _cEntMan = _pair.Client.ResolveDependency();
+ _sEntMan = _pair.Server.ResolveDependency();
+ _cAtmos = _cEntMan.System();
+ _sAtmos = _sEntMan.System();
+
+ const float volume = 2500f;
+ const float temperature = 293.15f;
+
+ const float o2 = 12.3f;
+ const float n2 = 45.6f;
+ const float co2 = 0.42f;
+ const float plasma = 0.05f;
+
+ _mix = new GasMixture(volume) { Temperature = temperature };
+
+ _mix.AdjustMoles(Gas.Oxygen, o2);
+ _mix.AdjustMoles(Gas.Nitrogen, n2);
+ _mix.AdjustMoles(Gas.CarbonDioxide, co2);
+ _mix.AdjustMoles(Gas.Plasma, plasma);
+ }
+
+ [Benchmark]
+ public async Task ClientHeatCapacityBenchmark()
+ {
+ await _pair.Client.WaitPost(delegate
+ {
+ for (var i = 0; i < 10000; i++)
+ {
+ _cAtmos.GetHeatCapacity(_mix, applyScaling: true);
+ }
+ });
+ }
+
+ [Benchmark]
+ public async Task ServerHeatCapacityBenchmark()
+ {
+ await _pair.Server.WaitPost(delegate
+ {
+ for (var i = 0; i < 10000; i++)
+ {
+ _sAtmos.GetHeatCapacity(_mix, applyScaling: true);
+ }
+ });
+ }
+
+ [GlobalCleanup]
+ public async Task CleanupAsync()
+ {
+ await _pair.DisposeAsync();
+ PoolManager.Shutdown();
+ }
+}
diff --git a/Content.Benchmarks/MapLoadBenchmark.cs b/Content.Benchmarks/MapLoadBenchmark.cs
index 126ac99774..d43b7d7d19 100644
--- a/Content.Benchmarks/MapLoadBenchmark.cs
+++ b/Content.Benchmarks/MapLoadBenchmark.cs
@@ -5,7 +5,7 @@ using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
-using Content.Server.Maps;
+using Content.Shared.Maps;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.EntitySerialization.Systems;
diff --git a/Content.Client/Access/UI/AccessLevelControl.xaml b/Content.Client/Access/UI/AccessLevelControl.xaml
index 56968d8983..6a3155effa 100644
--- a/Content.Client/Access/UI/AccessLevelControl.xaml
+++ b/Content.Client/Access/UI/AccessLevelControl.xaml
@@ -1,4 +1,4 @@
diff --git a/Content.Client/Access/UI/AccessOverriderWindow.xaml b/Content.Client/Access/UI/AccessOverriderWindow.xaml
index ae482140bc..545ebec494 100644
--- a/Content.Client/Access/UI/AccessOverriderWindow.xaml
+++ b/Content.Client/Access/UI/AccessOverriderWindow.xaml
@@ -2,7 +2,7 @@
MinSize="650 290">
-
+
diff --git a/Content.Client/Access/UI/AccessOverriderWindow.xaml.cs b/Content.Client/Access/UI/AccessOverriderWindow.xaml.cs
index a783dd368f..d0d15d2bd5 100644
--- a/Content.Client/Access/UI/AccessOverriderWindow.xaml.cs
+++ b/Content.Client/Access/UI/AccessOverriderWindow.xaml.cs
@@ -53,6 +53,8 @@ namespace Content.Client.Access.UI
public void UpdateState(IPrototypeManager protoManager, AccessOverriderBoundUserInterfaceState state)
{
+ PrivilegedIdGrid.Visible = state.ShowPrivilegedIdGrid;
+
PrivilegedIdLabel.Text = state.PrivilegedIdName;
PrivilegedIdButton.Text = state.IsPrivilegedIdPresent
? Loc.GetString("access-overrider-window-eject-button")
@@ -77,7 +79,9 @@ namespace Content.Client.Access.UI
missingPrivileges.Add(privilege);
}
- MissingPrivilegesLabel.Text = Loc.GetString("access-overrider-window-missing-privileges");
+ MissingPrivilegesLabel.Text = state.ShowPrivilegedIdGrid ?
+ Loc.GetString("access-overrider-window-missing-privileges") :
+ Loc.GetString("access-overrider-window-missing-privileges-no-id");
MissingPrivilegesText.Text = string.Join(", ", missingPrivileges);
}
diff --git a/Content.Client/Administration/AdminNameOverlay.cs b/Content.Client/Administration/AdminNameOverlay.cs
index dbf7304ce5..1614b4ab67 100644
--- a/Content.Client/Administration/AdminNameOverlay.cs
+++ b/Content.Client/Administration/AdminNameOverlay.cs
@@ -134,7 +134,7 @@ internal sealed class AdminNameOverlay : Overlay
? null
: _prototypeManager.Index(playerInfo.RoleProto.Value);
- var roleName = Loc.GetString(rolePrototype?.Name ?? RoleTypePrototype.FallbackName);
+ var roleName = rolePrototype?.Name ?? RoleTypePrototype.FallbackName;
var roleColor = rolePrototype?.Color ?? RoleTypePrototype.FallbackColor;
var roleSymbol = rolePrototype?.Symbol ?? RoleTypePrototype.FallbackSymbol;
@@ -227,7 +227,7 @@ internal sealed class AdminNameOverlay : Overlay
{
color = Color.GreenYellow;
color.A = alpha;
- args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, Loc.GetString(playerInfo.StartingJob), uiScale, playerInfo.Connected ? color : colorDisconnected);
+ args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.StartingJob, uiScale, playerInfo.Connected ? color : colorDisconnected);
currentOffset += lineoffset;
}
@@ -255,7 +255,7 @@ internal sealed class AdminNameOverlay : Overlay
color = roleColor;
symbol = IsFiltered(playerInfo.RoleProto) ? symbol : string.Empty;
text = IsFiltered(playerInfo.RoleProto)
- ? roleName.ToUpper()
+ ? Loc.GetString(roleName).ToUpper()
: string.Empty;
break;
case AdminOverlayAntagFormat.Subtype:
diff --git a/Content.Client/Administration/Components/KillSignComponent.cs b/Content.Client/Administration/Components/KillSignComponent.cs
deleted file mode 100644
index 91c44ef3f2..0000000000
--- a/Content.Client/Administration/Components/KillSignComponent.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-using Content.Shared.Administration.Components;
-using Robust.Shared.GameStates;
-
-namespace Content.Client.Administration.Components;
-
-[RegisterComponent]
-public sealed partial class KillSignComponent : SharedKillSignComponent;
diff --git a/Content.Client/Administration/Systems/BufferingSystem.cs b/Content.Client/Administration/Systems/BufferingSystem.cs
new file mode 100644
index 0000000000..e511bbff36
--- /dev/null
+++ b/Content.Client/Administration/Systems/BufferingSystem.cs
@@ -0,0 +1,7 @@
+using Content.Shared.Administration.Systems;
+
+namespace Content.Client.Administration.Systems;
+
+public sealed class BufferingSystem : SharedBufferingSystem
+{
+}
diff --git a/Content.Client/Administration/Systems/KillSignSystem.cs b/Content.Client/Administration/Systems/KillSignSystem.cs
index c12f65f1f0..f17c384c1e 100644
--- a/Content.Client/Administration/Systems/KillSignSystem.cs
+++ b/Content.Client/Administration/Systems/KillSignSystem.cs
@@ -1,46 +1,77 @@
using System.Numerics;
-using Content.Client.Administration.Components;
+using Content.Shared.Administration.Components;
using Robust.Client.GameObjects;
-using Robust.Shared.Utility;
+using Robust.Client.Player;
namespace Content.Client.Administration.Systems;
public sealed class KillSignSystem : EntitySystem
{
[Dependency] private readonly SpriteSystem _sprite = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
public override void Initialize()
{
SubscribeLocalEvent(KillSignAdded);
SubscribeLocalEvent(KillSignRemoved);
+ SubscribeLocalEvent(AfterAutoHandleState);
}
- private void KillSignRemoved(EntityUid uid, KillSignComponent component, ComponentShutdown args)
+ private void KillSignRemoved(Entity ent, ref ComponentShutdown args)
{
- if (!TryComp(uid, out var sprite))
- return;
-
- if (!_sprite.LayerMapTryGet((uid, sprite), KillSignKey.Key, out var layer, false))
- return;
-
- _sprite.RemoveLayer((uid, sprite), layer);
+ RemoveKillsign(ent);
}
- private void KillSignAdded(EntityUid uid, KillSignComponent component, ComponentStartup args)
+ private void KillSignAdded(Entity ent, ref ComponentStartup args)
{
- if (!TryComp(uid, out var sprite))
+ AddKillsign(ent);
+ }
+
+ private void AfterAutoHandleState(Entity ent, ref AfterAutoHandleStateEvent args)
+ {
+ // After receiving a new state for the component, we remove the old killsign and build a new one.
+ // This is so changes to the sprite can be displayed live and allowing them to be edited via ViewVariables.
+ // This could just update an existing sprite, but this is both easier and runs rarely anyway.
+ RemoveKillsign(ent);
+ AddKillsign(ent);
+ }
+
+ private void AddKillsign(Entity ent)
+ {
+ // If we hide from owner and we ARE the owner, don't add a killsign.
+ // This could use session specific networking to FULLY hide it, but I am too lazy right now.
+ if (ent.Comp.HideFromOwner && _player.LocalEntity == ent)
return;
- if (_sprite.LayerMapTryGet((uid, sprite), KillSignKey.Key, out var _, false))
+ if (!TryComp(ent, out var sprite))
return;
- var adj = _sprite.GetLocalBounds((uid, sprite)).Height / 2 + ((1.0f / 32) * 6.0f);
+ if (_sprite.LayerMapTryGet((ent, sprite), KillSignKey.Key, out var _, false))
+ return;
- var layer = _sprite.AddLayer((uid, sprite), new SpriteSpecifier.Rsi(new ResPath("Objects/Misc/killsign.rsi"), "sign"));
- _sprite.LayerMapSet((uid, sprite), KillSignKey.Key, layer);
+ if (ent.Comp.Sprite == null)
+ return;
- _sprite.LayerSetOffset((uid, sprite), layer, new Vector2(0.0f, adj));
- sprite.LayerSetShader(layer, "unshaded");
+ var adj = _sprite.GetLocalBounds((ent, sprite)).Height / 2 + ((1.0f / 32) * 6.0f);
+
+ var layer = _sprite.AddLayer((ent, sprite), ent.Comp.Sprite);
+ _sprite.LayerMapSet((ent, sprite), KillSignKey.Key, layer);
+ _sprite.LayerSetScale((ent, sprite), layer, ent.Comp.Scale);
+ _sprite.LayerSetOffset((ent, sprite), layer, ent.Comp.DoOffset ? new Vector2(0.0f, adj) : new Vector2(0.0f, 0.0f));
+
+ if (ent.Comp.ForceUnshaded)
+ sprite.LayerSetShader(layer, "unshaded");
+ }
+
+ private void RemoveKillsign(Entity ent)
+ {
+ if (!TryComp(ent, out var sprite))
+ return;
+
+ if (!_sprite.LayerMapTryGet((ent, sprite), KillSignKey.Key, out var layer, false))
+ return;
+
+ _sprite.RemoveLayer((ent, sprite), layer);
}
private enum KillSignKey
diff --git a/Content.Client/Administration/UI/CustomControls/AdminLogLabel.cs b/Content.Client/Administration/UI/CustomControls/AdminLogLabel.cs
new file mode 100644
index 0000000000..eb2281cf7b
--- /dev/null
+++ b/Content.Client/Administration/UI/CustomControls/AdminLogLabel.cs
@@ -0,0 +1,33 @@
+using Content.Shared.Administration.Logs;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+
+namespace Content.Client.Administration.UI.CustomControls;
+
+public sealed class AdminLogLabel : RichTextLabel
+{
+ public AdminLogLabel(ref SharedAdminLog log, HSeparator separator)
+ {
+ Log = log;
+ Separator = separator;
+
+ SetMessage($"{log.Date:HH:mm:ss}: {log.Message}");
+ OnVisibilityChanged += VisibilityChanged;
+ }
+
+ public new SharedAdminLog Log { get; }
+
+ public HSeparator Separator { get; }
+
+ private void VisibilityChanged(Control control)
+ {
+ Separator.Visible = Visible;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ OnVisibilityChanged -= VisibilityChanged;
+ }
+}
diff --git a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs
index 8027a00c54..c7fbf6c2dc 100644
--- a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs
+++ b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs
@@ -1,15 +1,16 @@
using System.Linq;
-using System.Text.RegularExpressions;
using Content.Client.Administration.Systems;
using Content.Client.UserInterface.Controls;
using Content.Client.Verbs.UI;
using Content.Shared.Administration;
using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Input;
+using Robust.Shared.Utility;
namespace Content.Client.Administration.UI.CustomControls;
@@ -95,26 +96,13 @@ public sealed partial class PlayerListControl : BoxContainer
private void FilterList()
{
_sortedPlayerList.Clear();
-
- Regex filterRegex;
- // There is no neat way to handle invalid regex being submitted other than
- // catching and ignoring the exception which gets thrown when it's invalid.
- try
- {
- filterRegex = new Regex(FilterLineEdit.Text, RegexOptions.IgnoreCase);
- }
- catch (ArgumentException)
- {
- return;
- }
-
foreach (var info in _playerList)
{
var displayName = $"{info.CharacterName} ({info.Username})";
if (info.IdentityName != info.CharacterName)
displayName += $" [{info.IdentityName}]";
if (!string.IsNullOrEmpty(FilterLineEdit.Text)
- && !filterRegex.IsMatch(displayName))
+ && !displayName.ToLowerInvariant().Contains(FilterLineEdit.Text.Trim().ToLowerInvariant()))
continue;
_sortedPlayerList.Add(info);
}
diff --git a/Content.Client/Administration/UI/CustomControls/PlayerListEntry.xaml.cs b/Content.Client/Administration/UI/CustomControls/PlayerListEntry.xaml.cs
index 00e4d06044..cf2e776ea8 100644
--- a/Content.Client/Administration/UI/CustomControls/PlayerListEntry.xaml.cs
+++ b/Content.Client/Administration/UI/CustomControls/PlayerListEntry.xaml.cs
@@ -1,6 +1,7 @@
using Content.Client.Stylesheets;
using Content.Shared.Administration;
using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;
diff --git a/Content.Client/Administration/UI/Logs/AdminLogsControl.xaml b/Content.Client/Administration/UI/Logs/AdminLogsControl.xaml
index 9b5da77801..f92a307de3 100644
--- a/Content.Client/Administration/UI/Logs/AdminLogsControl.xaml
+++ b/Content.Client/Administration/UI/Logs/AdminLogsControl.xaml
@@ -1,6 +1,5 @@
+ xmlns:aui="clr-namespace:Content.Client.Administration.UI.CustomControls">
@@ -55,15 +54,9 @@
+
-
-
-
-
-
diff --git a/Content.Client/Administration/UI/Logs/AdminLogsControl.xaml.cs b/Content.Client/Administration/UI/Logs/AdminLogsControl.xaml.cs
index 00b5e40e9a..e12d598733 100644
--- a/Content.Client/Administration/UI/Logs/AdminLogsControl.xaml.cs
+++ b/Content.Client/Administration/UI/Logs/AdminLogsControl.xaml.cs
@@ -1,8 +1,6 @@
using System.Linq;
using System.Runtime.InteropServices;
-using System.Text.RegularExpressions;
using Content.Client.Administration.UI.CustomControls;
-using Content.Client.Administration.UI.Logs.Entries;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Robust.Client.AutoGenerated;
@@ -40,9 +38,6 @@ public sealed partial class AdminLogsControl : Control
SelectAllPlayersButton.OnPressed += SelectAllPlayers;
SelectNoPlayersButton.OnPressed += SelectNoPlayers;
- RenderRichTextButton.OnPressed += RenderRichTextChanged;
- RemoveMarkupButton.OnPressed += RemoveMarkupChanged;
-
RoundSpinBox.IsValid = i => i > 0 && i <= CurrentRound;
RoundSpinBox.ValueChanged += RoundSpinBoxChanged;
RoundSpinBox.InitDefaultButtons();
@@ -63,8 +58,6 @@ public sealed partial class AdminLogsControl : Control
private int CurrentRound { get; set; }
- private Regex LogSearchRegex { get; set; } = new("");
-
public int SelectedRoundId => RoundSpinBox.Value;
public string Search => LogSearch.Text;
private int ShownLogs { get; set; }
@@ -84,8 +77,6 @@ public sealed partial class AdminLogsControl : Control
///
private List RawAdminLogs { get; set; } = new();
- private bool RenderRichText { get; set; }
- private bool RemoveMarkup { get; set; }
public HashSet SelectedTypes { get; } = new();
public HashSet SelectedPlayers { get; } = new();
@@ -132,19 +123,6 @@ public sealed partial class AdminLogsControl : Control
private void LogSearchChanged(LineEditEventArgs args)
{
- // This exception is thrown if the regex is invalid, which happens often, so we ignore it.
- try
- {
- LogSearchRegex = new Regex(
- "(" + LogSearch.Text + ")",
- RegexOptions.IgnoreCase,
- TimeSpan.FromSeconds(1));
- }
- catch (ArgumentException)
- {
- return;
- }
-
UpdateLogs();
}
@@ -226,26 +204,6 @@ public sealed partial class AdminLogsControl : Control
UpdateLogs();
}
- private void RenderRichTextChanged(ButtonEventArgs args)
- {
- RenderRichText = args.Button.Pressed;
-
- RemoveMarkup = RemoveMarkup && !RenderRichText;
- RemoveMarkupButton.Pressed = RemoveMarkup;
-
- UpdateLogs();
- }
-
- private void RemoveMarkupChanged(ButtonEventArgs args)
- {
- RemoveMarkup = args.Button.Pressed;
-
- RenderRichText = !RemoveMarkup && RenderRichText;
- RenderRichTextButton.Pressed = RenderRichText;
-
- UpdateLogs();
- }
-
public void SetTypesSelection(HashSet selectedTypes, bool invert = false)
{
SelectedTypes.Clear();
@@ -304,15 +262,16 @@ public sealed partial class AdminLogsControl : Control
foreach (var child in LogsContainer.Children)
{
- if (child is not AdminLogEntry log)
+ if (child is not AdminLogLabel log)
+ {
continue;
+ }
child.Visible = ShouldShowLog(log);
- if (!child.Visible)
- continue;
-
- log.RenderResults(LogSearchRegex, RenderRichText, RemoveMarkup);
- ShownLogs++;
+ if (child.Visible)
+ {
+ ShownLogs++;
+ }
}
UpdateCount();
@@ -330,30 +289,30 @@ public sealed partial class AdminLogsControl : Control
button.Text.Contains(PlayerSearch.Text, StringComparison.OrdinalIgnoreCase);
}
- private bool LogMatchesPlayerFilter(AdminLogEntry entry)
+ private bool LogMatchesPlayerFilter(AdminLogLabel label)
{
- if (entry.Log.Players.Length == 0)
+ if (label.Log.Players.Length == 0)
return SelectedPlayers.Count == 0 || IncludeNonPlayerLogs;
- return SelectedPlayers.Overlaps(entry.Log.Players);
+ return SelectedPlayers.Overlaps(label.Log.Players);
}
- private bool ShouldShowLog(AdminLogEntry entry)
+ private bool ShouldShowLog(AdminLogLabel label)
{
// Check log type
- if (!SelectedTypes.Contains(entry.Log.Type))
+ if (!SelectedTypes.Contains(label.Log.Type))
return false;
// Check players
- if (!LogMatchesPlayerFilter(entry))
+ if (!LogMatchesPlayerFilter(label))
return false;
// Check impact
- if (!SelectedImpacts.Contains(entry.Log.Impact))
+ if (!SelectedImpacts.Contains(label.Log.Impact))
return false;
// Check search
- if (!LogSearchRegex.IsMatch(entry.Log.Message))
+ if (!label.Log.Message.Contains(LogSearch.Text, StringComparison.OrdinalIgnoreCase))
return false;
return true;
@@ -427,16 +386,15 @@ public sealed partial class AdminLogsControl : Control
{
var log = tempLogs[i];
ref var logRef = ref log; // It didn't like me doing this as one line lmao
- var entry = new AdminLogEntry(ref log);
- //var label = new AdminLogLabel(ref logRef, separator);
- //label.Visible = ShouldShowLog(label);
+ var separator = new HSeparator();
+ var label = new AdminLogLabel(ref logRef, separator);
+ label.Visible = ShouldShowLog(label);
- TotalLogs++;
- //if (label.Visible) ShownLogs++;
+ if (label.Visible) ShownLogs++;
- //LogsContainer.AddChild(label);
- LogsContainer.AddChild(entry);
+ LogsContainer.AddChild(label);
+ LogsContainer.AddChild(separator);
}
UpdateLogs();
}
@@ -636,7 +594,6 @@ public sealed partial class AdminLogsControl : Control
SelectAllTypesButton.OnPressed -= SelectAllTypes;
SelectNoTypesButton.OnPressed -= SelectNoTypes;
- IncludeNonPlayersButton.OnPressed -= IncludeNonPlayers;
IncludeNonPlayersButton.OnPressed -= IncludeNonPlayers;
SelectAllPlayersButton.OnPressed -= SelectAllPlayers;
SelectNoPlayersButton.OnPressed -= SelectNoPlayers;
diff --git a/Content.Client/Administration/UI/Logs/AdminLogsEui.cs b/Content.Client/Administration/UI/Logs/AdminLogsEui.cs
index 6ad7de347d..28aca23f75 100644
--- a/Content.Client/Administration/UI/Logs/AdminLogsEui.cs
+++ b/Content.Client/Administration/UI/Logs/AdminLogsEui.cs
@@ -1,6 +1,6 @@
using System.IO;
using System.Linq;
-using Content.Client.Administration.UI.Logs.Entries;
+using Content.Client.Administration.UI.CustomControls;
using Content.Client.Eui;
using Content.Shared.Administration.Logs;
using Content.Shared.Eui;
@@ -17,6 +17,16 @@ public sealed class AdminLogsEui : BaseEui
{
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
+ [Dependency] private readonly IFileDialogManager _dialogManager = default!;
+ [Dependency] private readonly ILogManager _log = default!;
+
+ private const char CsvSeparator = ',';
+ private const string CsvQuote = "\"";
+ private const string CsvHeader = "Date,ID,PlayerID,Severity,Type,Message";
+
+ private ISawmill _sawmill;
+
+ private bool _currentlyExportingLogs = false;
public AdminLogsEui()
{
@@ -28,6 +38,9 @@ public sealed class AdminLogsEui : BaseEui
LogsControl.RefreshButton.OnPressed += _ => RequestLogs();
LogsControl.NextButton.OnPressed += _ => NextLogs();
LogsControl.PopOutButton.OnPressed += _ => PopOut();
+ LogsControl.ExportLogs.OnPressed += _ => ExportLogs();
+
+ _sawmill = _log.GetSawmill("admin.logs.ui");
}
private WindowRoot? Root { get; set; }
@@ -76,6 +89,71 @@ public sealed class AdminLogsEui : BaseEui
SendMessage(request);
}
+ private async void ExportLogs()
+ {
+ if (_currentlyExportingLogs)
+ return;
+
+ _currentlyExportingLogs = true;
+ LogsControl.ExportLogs.Disabled = true;
+
+ var file = await _dialogManager.SaveFile(new FileDialogFilters(new FileDialogFilters.Group("csv")));
+
+ if (file == null)
+ return;
+
+ try
+ {
+ // Buffer is set to 4KB for performance reasons. As the average export of 1000 logs is ~200KB
+ await using var writer = new StreamWriter(file.Value.fileStream, bufferSize: 4096);
+ await writer.WriteLineAsync(CsvHeader);
+ foreach (var child in LogsControl.LogsContainer.Children)
+ {
+ if (child is not AdminLogLabel logLabel || !child.Visible)
+ continue;
+
+ var log = logLabel.Log;
+
+ // Date
+ // I swear to god if someone adds ,s or "s to the other fields...
+ await writer.WriteAsync(log.Date.ToString("s", System.Globalization.CultureInfo.InvariantCulture));
+ await writer.WriteAsync(CsvSeparator);
+ // ID
+ await writer.WriteAsync(log.Id.ToString());
+ await writer.WriteAsync(CsvSeparator);
+ // PlayerID
+ var players = log.Players;
+ for (var i = 0; i < players.Length; i++)
+ {
+ await writer.WriteAsync(players[i] + (i == players.Length - 1 ? "" : " "));
+ }
+ await writer.WriteAsync(CsvSeparator);
+ // Severity
+ await writer.WriteAsync(log.Impact.ToString());
+ await writer.WriteAsync(CsvSeparator);
+ // Type
+ await writer.WriteAsync(log.Type.ToString());
+ await writer.WriteAsync(CsvSeparator);
+ // Message
+ await writer.WriteAsync(CsvQuote);
+ await writer.WriteAsync(log.Message.Replace(CsvQuote, CsvQuote + CsvQuote));
+ await writer.WriteAsync(CsvQuote);
+
+ await writer.WriteLineAsync();
+ }
+ }
+ catch (Exception exc)
+ {
+ _sawmill.Error($"Error when exporting admin log:\n{exc.StackTrace}");
+ }
+ finally
+ {
+ await file.Value.fileStream.DisposeAsync();
+ _currentlyExportingLogs = false;
+ LogsControl.ExportLogs.Disabled = false;
+ }
+ }
+
private void PopOut()
{
if (LogsWindow == null)
diff --git a/Content.Client/Administration/UI/Logs/Entries/AdminLogEntry.xaml b/Content.Client/Administration/UI/Logs/Entries/AdminLogEntry.xaml
deleted file mode 100644
index 7733c2b1cb..0000000000
--- a/Content.Client/Administration/UI/Logs/Entries/AdminLogEntry.xaml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Content.Client/Administration/UI/Logs/Entries/AdminLogEntry.xaml.cs b/Content.Client/Administration/UI/Logs/Entries/AdminLogEntry.xaml.cs
deleted file mode 100644
index fc9f960b66..0000000000
--- a/Content.Client/Administration/UI/Logs/Entries/AdminLogEntry.xaml.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-using System.Text.RegularExpressions;
-using Content.Client.Message;
-using Content.Shared.Administration.Logs;
-using Content.Shared.CCVar;
-using Robust.Client.AutoGenerated;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Configuration;
-using Robust.Shared.Utility;
-
-namespace Content.Client.Administration.UI.Logs.Entries;
-
-[GenerateTypedNameReferences]
-public sealed partial class AdminLogEntry : BoxContainer
-{
- private readonly IConfigurationManager _cfgManager;
-
- public SharedAdminLog Log { get; }
-
- private readonly string _rawMessage;
-
- public AdminLogEntry(ref SharedAdminLog log)
- {
- _cfgManager = IoCManager.Resolve();
-
- RobustXamlLoader.Load(this);
-
- Log = log;
-
- _rawMessage = $"{log.Date:HH:mm:ss}: {log.Message}";
- Message.SetMessage(_rawMessage);
-
- DetailsHeading.OnToggled += DetailsToggled;
- }
-
- ///
- /// Sets text to be highlighted from a search result, and renders rich text, or removes all rich text markup.
- ///
- public void RenderResults(Regex highlightRegex, bool renderRichText, bool removeMarkup)
- {
- var color = _cfgManager.GetCVar(CCVars.AdminLogsHighlightColor);
- var formattedMessage = renderRichText
- ? _rawMessage
- : removeMarkup
- ? FormattedMessage.RemoveMarkupPermissive(_rawMessage)
- : FormattedMessage.EscapeText(_rawMessage);
-
- // Want to avoid highlighting smaller strings
- if (highlightRegex.ToString().Length > 4)
- {
- try
- {
- formattedMessage = highlightRegex.Replace(formattedMessage, $"[color={color}]$1[/color]", 3);
- }
- catch (RegexMatchTimeoutException)
- {
- // if we time out then don't bother highlighting results
- }
- }
-
- if (!FormattedMessage.TryFromMarkup(formattedMessage, out var outputMessage))
- return;
-
- Message.SetMessage(outputMessage);
- }
-
- ///
- /// We perform some extra calculations in the dropdown, so we want to render that only when
- /// the dropdown is actually opened.
- /// This also removes itself from the event listener so it doesn't trigger again.
- ///
- private void DetailsToggled(BaseButton.ButtonToggledEventArgs args)
- {
- if (!args.Pressed || DetailsBody.ChildCount > 0)
- return;
- DetailsBody.AddChild(new AdminLogEntryDetails(Log));
- DetailsHeading.OnToggled -= DetailsToggled;
- }
-}
diff --git a/Content.Client/Administration/UI/Logs/Entries/AdminLogEntryDetails.xaml b/Content.Client/Administration/UI/Logs/Entries/AdminLogEntryDetails.xaml
deleted file mode 100644
index 23391046b9..0000000000
--- a/Content.Client/Administration/UI/Logs/Entries/AdminLogEntryDetails.xaml
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Content.Client/Administration/UI/Logs/Entries/AdminLogEntryDetails.xaml.cs b/Content.Client/Administration/UI/Logs/Entries/AdminLogEntryDetails.xaml.cs
deleted file mode 100644
index 611626e544..0000000000
--- a/Content.Client/Administration/UI/Logs/Entries/AdminLogEntryDetails.xaml.cs
+++ /dev/null
@@ -1,98 +0,0 @@
-using System.Linq;
-using Content.Client.Administration.Systems;
-using Content.Client.Administration.UI.CustomControls;
-using Content.Client.UserInterface.Controls;
-using Content.Client.Verbs.UI;
-using Content.Shared.Administration.Logs;
-using Robust.Client.AutoGenerated;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Input;
-using Robust.Shared.Network;
-
-namespace Content.Client.Administration.UI.Logs.Entries;
-
-[GenerateTypedNameReferences]
-public sealed partial class AdminLogEntryDetails : BoxContainer
-{
- private readonly AdminSystem _adminSystem;
- private readonly IUserInterfaceManager _uiManager;
- private readonly IEntityManager _entManager;
-
- public AdminLogEntryDetails(SharedAdminLog log)
- {
- RobustXamlLoader.Load(this);
- _entManager = IoCManager.Resolve();
- _uiManager = IoCManager.Resolve();
- _adminSystem = _entManager.System();
-
- Type.Text = log.Type.ToString();
- Impact.Text = log.Impact.ToString();
-
- LocalTime.Text = $"{log.Date.ToLocalTime():HH:mm:ss}";
- UTCTime.Text = $"{log.Date:HH:mm:ss}";
- // TimeSpan and DateTime use different formatting string conventions for some completely logical reason
- // that mere mortals such as myself will never be able to understand.
- CurTime.Text = new TimeSpan(log.CurTime).ToString(@"hh\:mm\:ss");
-
- PlayerListContainer.ItemKeyBindDown += PlayerListItemKeyBindDown;
- PlayerListContainer.GenerateItem += GenerateButton;
- PopulateList(log.Players);
- }
-
- private void PopulateList(Guid[] players)
- {
- if (players.Length == 0)
- return;
-
- if (_adminSystem.PlayerList is not { } allPlayers || allPlayers.Count == 0)
- return;
-
- var listData = new List();
- foreach (var playerGuid in players)
- {
- var netUserId = new NetUserId(playerGuid);
-
- // Linq here is fine since this runs in response to admin input in the UI and
- // this loop only tends to go through 1-4 iterations.
- if (allPlayers.FirstOrDefault(player => player.SessionId == netUserId) is not { } playerInfo)
- continue;
-
- listData.Add(new PlayerListData(playerInfo));
- }
-
- if (listData.Count == 0)
- return;
-
- PlayerListContainer.PopulateList(listData);
- }
-
- private void PlayerListItemKeyBindDown(GUIBoundKeyEventArgs? args, ListData? data)
- {
- if (args == null || data is not PlayerListData { Info: var selectedPlayer })
- return;
-
- if (!(args.Function == EngineKeyFunctions.UIRightClick
- || args.Function == EngineKeyFunctions.UIClick)
- || selectedPlayer.NetEntity == null)
- return;
-
- _uiManager.GetUIController().OpenVerbMenu(selectedPlayer.NetEntity.Value, true);
- args.Handle();
- }
-
- private void GenerateButton(ListData data, ListContainerButton button)
- {
- if (data is not PlayerListData { Info: var info })
- return;
-
- var entryLabel = new Label();
- entryLabel.Text = $"{info.CharacterName} ({info.Username})";
- var entry = new BoxContainer();
- entry.AddChild(entryLabel);
-
- button.AddChild(entry);
- button.AddStyleClass(ListContainer.StyleClassListContainerButton);
- }
-}
diff --git a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs
index 371b9952e4..d999df6d98 100644
--- a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs
+++ b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs
@@ -56,6 +56,7 @@ public sealed partial class PlayerTab : Control
_config.OnValueChanged(CCVars.AdminPlayerTabMarkGhosted, MarkGhostedChanged, true); // DeltaV
_config.OnValueChanged(CCVars.AdminPlayerTabMarkWatchlisted, MarkWatchlistedChanged, true); // DeltaV
+
OverlayButton.OnPressed += OverlayButtonPressed;
ShowDisconnectedButton.OnPressed += ShowDisconnectedPressed;
@@ -68,6 +69,7 @@ public sealed partial class PlayerTab : Control
SearchList.ItemKeyBindDown += (args, data) => OnEntryKeyBindDown?.Invoke(args, data);
RefreshPlayerList(_adminSystem.PlayerList);
+
}
// DeltaV - mark ghosted, watchlisted START
diff --git a/Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs b/Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs
new file mode 100644
index 0000000000..17b994e64f
--- /dev/null
+++ b/Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs
@@ -0,0 +1,35 @@
+using System.Runtime.CompilerServices;
+using Content.Shared.Atmos;
+
+namespace Content.Client.Atmos.EntitySystems;
+
+public sealed partial class AtmosphereSystem
+{
+ /*
+ Partial class for operations involving GasMixtures.
+
+ Any method that is overridden here is usually because the server-sided implementation contains
+ code that would escape sandbox. As such these methods are overridden here with a safe
+ implementation.
+ */
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override float GetHeatCapacityCalculation(float[] moles, bool space)
+ {
+ // Little hack to make space gas mixtures have heat capacity, therefore allowing them to cool down rooms.
+ if (space && MathHelper.CloseTo(NumericsHelpers.HorizontalAdd(moles), 0f))
+ {
+ return Atmospherics.SpaceHeatCapacity;
+ }
+
+ // explicit stackalloc call is banned on client tragically.
+ // the JIT does not stackalloc this during runtime,
+ // though this isnt the hottest code path so it should be fine
+ // the gc can eat a little as a treat
+ var tmp = new float[moles.Length];
+ NumericsHelpers.Multiply(moles, GasSpecificHeats, tmp);
+ // Adjust heat capacity by speedup, because this is primarily what
+ // determines how quickly gases heat up/cool.
+ return MathF.Max(NumericsHelpers.HorizontalAdd(tmp), Atmospherics.MinimumHeatCapacity);
+ }
+}
diff --git a/Content.Client/Atmos/EntitySystems/AtmosphereSystem.cs b/Content.Client/Atmos/EntitySystems/AtmosphereSystem.cs
index 44759372f4..30567abbf7 100644
--- a/Content.Client/Atmos/EntitySystems/AtmosphereSystem.cs
+++ b/Content.Client/Atmos/EntitySystems/AtmosphereSystem.cs
@@ -5,7 +5,7 @@ using Robust.Shared.GameStates;
namespace Content.Client.Atmos.EntitySystems;
-public sealed class AtmosphereSystem : SharedAtmosphereSystem
+public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
{
public override void Initialize()
{
diff --git a/Content.Client/BarSign/BarSignSystem.cs b/Content.Client/BarSign/BarSignSystem.cs
deleted file mode 100644
index 1ea99864a1..0000000000
--- a/Content.Client/BarSign/BarSignSystem.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using Content.Client.BarSign.Ui;
-using Content.Shared.BarSign;
-using Content.Shared.Power;
-using Robust.Client.GameObjects;
-using Robust.Shared.Prototypes;
-
-namespace Content.Client.BarSign;
-
-public sealed class BarSignSystem : VisualizerSystem
-{
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
- [Dependency] private readonly UserInterfaceSystem _ui = default!;
-
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent(OnAfterAutoHandleState);
- }
-
- private void OnAfterAutoHandleState(EntityUid uid, BarSignComponent component, ref AfterAutoHandleStateEvent args)
- {
- if (_ui.TryGetOpenUi(uid, BarSignUiKey.Key, out var bui))
- bui.Update(component.Current);
-
- UpdateAppearance(uid, component);
- }
-
- protected override void OnAppearanceChange(EntityUid uid, BarSignComponent component, ref AppearanceChangeEvent args)
- {
- UpdateAppearance(uid, component, args.Component, args.Sprite);
- }
-
- private void UpdateAppearance(EntityUid id, BarSignComponent sign, AppearanceComponent? appearance = null, SpriteComponent? sprite = null)
- {
- if (!Resolve(id, ref appearance, ref sprite))
- return;
-
- AppearanceSystem.TryGetData(id, PowerDeviceVisuals.Powered, out var powered, appearance);
-
- if (powered
- && sign.Current != null
- && _prototypeManager.Resolve(sign.Current, out var proto))
- {
- SpriteSystem.LayerSetSprite((id, sprite), 0, proto.Icon);
- sprite.LayerSetShader(0, "unshaded");
- }
- else
- {
- SpriteSystem.LayerSetRsiState((id, sprite), 0, "empty");
- sprite.LayerSetShader(0, null, null);
- }
- }
-}
diff --git a/Content.Client/BarSign/BarSignVisualizerSystem.cs b/Content.Client/BarSign/BarSignVisualizerSystem.cs
new file mode 100644
index 0000000000..3e641fed70
--- /dev/null
+++ b/Content.Client/BarSign/BarSignVisualizerSystem.cs
@@ -0,0 +1,30 @@
+using Content.Shared.BarSign;
+using Content.Shared.Power;
+using Robust.Client.GameObjects;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.BarSign;
+
+public sealed class BarSignVisualizerSystem : VisualizerSystem
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+
+ protected override void OnAppearanceChange(EntityUid uid, BarSignComponent component, ref AppearanceChangeEvent args)
+ {
+ AppearanceSystem.TryGetData(uid, PowerDeviceVisuals.Powered, out var powered, args.Component);
+ AppearanceSystem.TryGetData(uid, BarSignVisuals.BarSignPrototype, out var currentSign, args.Component);
+
+ if (powered
+ && currentSign != null
+ && _prototypeManager.Resolve(currentSign, out var proto))
+ {
+ SpriteSystem.LayerSetSprite((uid, args.Sprite), 0, proto.Icon);
+ args.Sprite?.LayerSetShader(0, "unshaded");
+ }
+ else
+ {
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), 0, "empty");
+ args.Sprite?.LayerSetShader(0, null, null);
+ }
+ }
+}
diff --git a/Content.Client/BarSign/Ui/BarSignBoundUserInterface.cs b/Content.Client/BarSign/Ui/BarSignBoundUserInterface.cs
index fe07f0f1d1..8265877edf 100644
--- a/Content.Client/BarSign/Ui/BarSignBoundUserInterface.cs
+++ b/Content.Client/BarSign/Ui/BarSignBoundUserInterface.cs
@@ -19,32 +19,27 @@ public sealed class BarSignBoundUserInterface(EntityUid owner, Enum uiKey) : Bou
var sign = EntMan.GetComponentOrNull(Owner)?.Current is { } current
? _prototype.Index(current)
: null;
- var allSigns = Shared.BarSign.BarSignSystem.GetAllBarSigns(_prototype)
+ var allSigns = BarSignSystem.GetAllBarSigns(_prototype)
.OrderBy(p => Loc.GetString(p.Name))
.ToList();
_menu = new(sign, allSigns);
_menu.OnSignSelected += id =>
{
- SendMessage(new SetBarSignMessage(id));
+ SendPredictedMessage(new SetBarSignMessage(id));
};
_menu.OnClose += Close;
_menu.OpenCentered();
}
- public void Update(ProtoId? sign)
+ public override void Update()
{
- if (_prototype.Resolve(sign, out var signPrototype))
- _menu?.UpdateState(signPrototype);
- }
-
- protected override void Dispose(bool disposing)
- {
- base.Dispose(disposing);
- if (!disposing)
+ if (!EntMan.TryGetComponent(Owner, out var signComp))
return;
- _menu?.Dispose();
+
+ if (_prototype.Resolve(signComp.Current, out var signPrototype))
+ _menu?.UpdateState(signPrototype);
}
}
diff --git a/Content.Client/Bed/BedSystem.cs b/Content.Client/Bed/BedSystem.cs
deleted file mode 100644
index 9c6f28290f..0000000000
--- a/Content.Client/Bed/BedSystem.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using Content.Shared.Bed;
-
-namespace Content.Client.Bed;
-
-public sealed class BedSystem : SharedBedSystem
-{
-
-}
diff --git a/Content.Client/Cargo/UI/BountyEntry.xaml.cs b/Content.Client/Cargo/UI/BountyEntry.xaml.cs
index d813f70ff4..bac7d84bf7 100644
--- a/Content.Client/Cargo/UI/BountyEntry.xaml.cs
+++ b/Content.Client/Cargo/UI/BountyEntry.xaml.cs
@@ -7,7 +7,6 @@ using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
-using Serilog;
namespace Content.Client.Cargo.UI;
diff --git a/Content.Client/CartridgeLoader/Cartridges/NewsReaderUiFragment.xaml.cs b/Content.Client/CartridgeLoader/Cartridges/NewsReaderUiFragment.xaml.cs
index cf667ff7e9..f76adfb866 100644
--- a/Content.Client/CartridgeLoader/Cartridges/NewsReaderUiFragment.xaml.cs
+++ b/Content.Client/CartridgeLoader/Cartridges/NewsReaderUiFragment.xaml.cs
@@ -1,11 +1,12 @@
using Content.Client.Message;
-using Content.Client.UserInterface.RichText; // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
+using Content.Client.RichText;
+using Content.Client.UserInterface.RichText;
using Content.Shared.MassMedia.Systems;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.RichText; // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
+using Robust.Client.UserInterface.RichText;
using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Utility; // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
+using Robust.Shared.Utility;
namespace Content.Client.CartridgeLoader.Cartridges;
@@ -17,18 +18,6 @@ public sealed partial class NewsReaderUiFragment : BoxContainer
public event Action? OnNotificationSwithPressed;
- // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
- private static readonly Type[] AllowedTags =
- [
- typeof(BoldItalicTag),
- typeof(BoldTag),
- typeof(BulletTag),
- typeof(ColorTag),
- typeof(HeadingTag),
- typeof(ItalicTag),
- typeof(MonoTag),
- ];
-
public NewsReaderUiFragment()
{
RobustXamlLoader.Load(this);
@@ -46,7 +35,7 @@ public sealed partial class NewsReaderUiFragment : BoxContainer
Author.Visible = true;
PageName.Text = article.Title;
- PageText.SetMessage(FormattedMessage.FromMarkupPermissive(article.Content), AllowedTags); // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
+ PageText.SetMessage(FormattedMessage.FromMarkupPermissive(article.Content), UserFormattableTags.BaseAllowedTags);
PageNum.Text = $"{targetNum}/{totalNum}";
@@ -55,8 +44,8 @@ public sealed partial class NewsReaderUiFragment : BoxContainer
var shareTime = article.ShareTime.ToString(@"hh\:mm\:ss");
ShareTime.SetMarkup(Loc.GetString("news-read-ui-time-prefix-text") + " " + shareTime);
- var author = Loc.GetString("news-read-ui-author-prefix") + " " + (article.Author ?? Loc.GetString("news-read-ui-no-author")); // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
- Author.SetMessage(FormattedMessage.FromMarkupPermissive(author), AllowedTags); // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
+ var author = Loc.GetString("news-read-ui-author-prefix") + " " + (article.Author ?? Loc.GetString("news-read-ui-no-author"));
+ Author.SetMessage(FormattedMessage.FromMarkupPermissive(author), UserFormattableTags.BaseAllowedTags);
Prev.Disabled = targetNum <= 1;
Next.Disabled = targetNum >= totalNum;
diff --git a/Content.Client/Chemistry/UI/ChemMasterBoundUserInterface.cs b/Content.Client/Chemistry/UI/ChemMasterBoundUserInterface.cs
index dcff475c3a..f6b494869a 100644
--- a/Content.Client/Chemistry/UI/ChemMasterBoundUserInterface.cs
+++ b/Content.Client/Chemistry/UI/ChemMasterBoundUserInterface.cs
@@ -44,6 +44,10 @@ namespace Content.Client.Chemistry.UI
(uint) _window.BottleDosage.Value, _window.LabelLine));
_window.BufferSortButton.OnPressed += _ => SendMessage(
new ChemMasterSortingTypeCycleMessage());
+ _window.OutputBufferDraw.OnPressed += _ => SendMessage(
+ new ChemMasterOutputDrawSourceMessage(ChemMasterDrawSource.Internal));
+ _window.OutputBeakerDraw.OnPressed += _ => SendMessage(
+ new ChemMasterOutputDrawSourceMessage(ChemMasterDrawSource.External));
for (uint i = 0; i < _window.PillTypeButtons.Length; i++)
{
diff --git a/Content.Client/Chemistry/UI/ChemMasterWindow.xaml b/Content.Client/Chemistry/UI/ChemMasterWindow.xaml
index 0fe7daff36..23d730cefa 100644
--- a/Content.Client/Chemistry/UI/ChemMasterWindow.xaml
+++ b/Content.Client/Chemistry/UI/ChemMasterWindow.xaml
@@ -83,10 +83,13 @@
-
+
+
+
-
-
+
+
+
diff --git a/Content.Client/Chemistry/UI/ChemMasterWindow.xaml.cs b/Content.Client/Chemistry/UI/ChemMasterWindow.xaml.cs
index c9353485b8..2152a0be1c 100644
--- a/Content.Client/Chemistry/UI/ChemMasterWindow.xaml.cs
+++ b/Content.Client/Chemistry/UI/ChemMasterWindow.xaml.cs
@@ -150,7 +150,17 @@ namespace Content.Client.Chemistry.UI
// Ensure the Panel Info is updated, including UI elements for Buffer Volume, Output Container and so on
UpdatePanelInfo(castState);
- BufferCurrentVolume.Text = $" {castState.InputContainerInfo?.CurrentVolume.Int() ?? 0}u"; // DeltaV
+ switch (castState.DrawSource)
+ {
+ case ChemMasterDrawSource.Internal:
+ SetBufferText(castState.BufferCurrentVolume, "chem-master-output-buffer-draw");
+ break;
+ case ChemMasterDrawSource.External:
+ SetBufferText(castState.InputContainerInfo?.CurrentVolume, "chem-master-output-beaker-draw");
+ break;
+ default:
+ throw new($"Chemmaster {castState.OutputContainerInfo} draw source is not set");
+ }
InputEjectButton.Disabled = castState.InputContainerInfo is null;
OutputEjectButton.Disabled = castState.OutputContainerInfo is null;
@@ -168,9 +178,14 @@ namespace Content.Client.Chemistry.UI
var holdsReagents = output?.Reagents != null;
var pillNumberMax = holdsReagents ? 0 : remainingCapacity;
var bottleAmountMax = holdsReagents ? remainingCapacity : 0;
- var bufferVolume = castState.BufferCurrentVolume?.Int() ?? 0;
+ var outputVolume = castState.DrawSource switch
+ {
+ ChemMasterDrawSource.Internal => castState.BufferCurrentVolume?.Int() ?? 0,
+ ChemMasterDrawSource.External => castState.InputContainerInfo?.CurrentVolume.Int() ?? 0,
+ _ => 0,
+ };
- PillDosage.Value = (int)Math.Min(bufferVolume, castState.PillDosageLimit);
+ PillDosage.Value = (int)Math.Min(outputVolume, castState.PillDosageLimit);
PillTypeButtons[castState.SelectedPillType].Pressed = true;
@@ -186,25 +201,35 @@ namespace Content.Client.Chemistry.UI
// Avoid division by zero
if (PillDosage.Value > 0)
{
- PillNumber.Value = Math.Min(bufferVolume / PillDosage.Value, pillNumberMax);
+ PillNumber.Value = Math.Min(outputVolume / PillDosage.Value, pillNumberMax);
}
else
{
PillNumber.Value = 0;
}
- BottleDosage.Value = Math.Min(bottleAmountMax, bufferVolume);
+ BottleDosage.Value = Math.Min(bottleAmountMax, outputVolume);
}
///
- /// Generate a product label based on reagents in the buffer.
+ /// Generate a product label based on reagents in the buffer or beaker.
///
/// State data sent by the server.
private string GenerateLabel(ChemMasterBoundUserInterfaceState state)
{
- if (state.BufferCurrentVolume == 0)
+ if (
+ state.BufferCurrentVolume == 0 && state.DrawSource == ChemMasterDrawSource.Internal ||
+ state.InputContainerInfo?.CurrentVolume == 0 && state.DrawSource == ChemMasterDrawSource.External ||
+ state.InputContainerInfo?.Reagents == null
+ )
return "";
- var reagent = state.BufferReagents.OrderBy(r => r.Quantity).First().Reagent;
+ var reagent = (state.DrawSource switch
+ {
+ ChemMasterDrawSource.Internal => state.BufferReagents,
+ ChemMasterDrawSource.External => state.InputContainerInfo.Reagents ?? [],
+ _ => throw new($"Chemmaster {state.OutputContainerInfo} draw source is not set"),
+ }).MinBy(r => r.Quantity)
+ .Reagent;
_prototypeManager.TryIndex(reagent.Prototype, out ReagentPrototype? proto);
return proto?.LocalizedName ?? "";
}
@@ -230,6 +255,8 @@ namespace Content.Client.Chemistry.UI
_ => Loc.GetString("chem-master-window-sort-type-none")
};
+ OutputBufferDraw.Pressed = state.DrawSource == ChemMasterDrawSource.Internal;
+ OutputBeakerDraw.Pressed = state.DrawSource == ChemMasterDrawSource.External;
if (!state.BufferReagents.Any())
{
@@ -411,6 +438,12 @@ namespace Content.Client.Chemistry.UI
get => LabelLineEdit.Text;
set => LabelLineEdit.Text = value;
}
+
+ private void SetBufferText(FixedPoint2? volume, string text)
+ {
+ BufferCurrentVolume.Text = $" {volume ?? FixedPoint2.Zero}u";
+ DrawSource.Text = Loc.GetString(text);
+ }
}
public sealed class ReagentButton : Button
diff --git a/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs b/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs
index 1bc1c0dba9..884a5db9da 100644
--- a/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs
+++ b/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs
@@ -2,41 +2,31 @@ using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
using Content.Shared.FixedPoint;
using JetBrains.Annotations;
-using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
-namespace Content.Client.Chemistry.UI
+namespace Content.Client.Chemistry.UI;
+
+[UsedImplicitly]
+public sealed class TransferAmountBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
- [UsedImplicitly]
- public sealed class TransferAmountBoundUserInterface : BoundUserInterface
+ [ViewVariables]
+ private TransferAmountWindow? _window;
+
+ protected override void Open()
{
- private IEntityManager _entManager;
- private EntityUid _owner;
- [ViewVariables]
- private TransferAmountWindow? _window;
+ base.Open();
+ _window = this.CreateWindow();
- public TransferAmountBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ if (EntMan.TryGetComponent(Owner, out var comp))
+ _window.SetBounds(comp.MinimumTransferAmount.Int(), comp.MaximumTransferAmount.Int());
+
+ _window.ApplyButton.OnPressed += _ =>
{
- _owner = owner;
- _entManager = IoCManager.Resolve();
- }
-
- protected override void Open()
- {
- base.Open();
- _window = this.CreateWindow();
-
- if (_entManager.TryGetComponent(_owner, out var comp))
- _window.SetBounds(comp.MinimumTransferAmount.Int(), comp.MaximumTransferAmount.Int());
-
- _window.ApplyButton.OnPressed += _ =>
+ if (int.TryParse(_window.AmountLineEdit.Text, out var i))
{
- if (int.TryParse(_window.AmountLineEdit.Text, out var i))
- {
- SendMessage(new TransferAmountSetValueMessage(FixedPoint2.New(i)));
- _window.Close();
- }
- };
- }
+ SendPredictedMessage(new TransferAmountSetValueMessage(FixedPoint2.New(i)));
+ _window.Close();
+ }
+ };
}
}
diff --git a/Content.Client/Chemistry/UI/TransferAmountWindow.xaml.cs b/Content.Client/Chemistry/UI/TransferAmountWindow.xaml.cs
index 6bae044441..2d01098213 100644
--- a/Content.Client/Chemistry/UI/TransferAmountWindow.xaml.cs
+++ b/Content.Client/Chemistry/UI/TransferAmountWindow.xaml.cs
@@ -3,34 +3,33 @@ using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
-namespace Content.Client.Chemistry.UI
+namespace Content.Client.Chemistry.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class TransferAmountWindow : DefaultWindow
{
- [GenerateTypedNameReferences]
- public sealed partial class TransferAmountWindow : DefaultWindow
+ private int _max = Int32.MaxValue;
+ private int _min = 1;
+
+ public TransferAmountWindow()
{
- private int _max = Int32.MaxValue;
- private int _min = 1;
+ RobustXamlLoader.Load(this);
+ AmountLineEdit.OnTextChanged += OnValueChanged;
+ }
- public TransferAmountWindow()
- {
- RobustXamlLoader.Load(this);
- AmountLineEdit.OnTextChanged += OnValueChanged;
- }
+ public void SetBounds(int min, int max)
+ {
+ _min = min;
+ _max = max;
+ MinimumAmount.Text = Loc.GetString("comp-solution-transfer-set-amount-min", ("amount", _min));
+ MaximumAmount.Text = Loc.GetString("comp-solution-transfer-set-amount-max", ("amount", _max));
+ }
- public void SetBounds(int min, int max)
- {
- _min = min;
- _max = max;
- MinimumAmount.Text = Loc.GetString("comp-solution-transfer-set-amount-min", ("amount", _min));
- MaximumAmount.Text = Loc.GetString("comp-solution-transfer-set-amount-max", ("amount", _max));
- }
-
- private void OnValueChanged(LineEdit.LineEditEventArgs args)
- {
- if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount > _max || amount < _min)
- ApplyButton.Disabled = true;
- else
- ApplyButton.Disabled = false;
- }
+ private void OnValueChanged(LineEdit.LineEditEventArgs args)
+ {
+ if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount > _max || amount < _min)
+ ApplyButton.Disabled = true;
+ else
+ ApplyButton.Disabled = false;
}
}
diff --git a/Content.Client/CombatMode/CombatModeIndicatorsOverlay.cs b/Content.Client/CombatMode/CombatModeIndicatorsOverlay.cs
index b2bdf2893d..d852e20c45 100644
--- a/Content.Client/CombatMode/CombatModeIndicatorsOverlay.cs
+++ b/Content.Client/CombatMode/CombatModeIndicatorsOverlay.cs
@@ -4,10 +4,8 @@ using Content.Shared.Weapons.Ranged.Components;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
-using Robust.Client.Serialization;
using Robust.Client.UserInterface;
using Robust.Shared.Enums;
-using Robust.Shared.Graphics;
using Robust.Shared.Utility;
namespace Content.Client.CombatMode;
diff --git a/Content.Client/Commands/ShowWallmountsCommand.cs b/Content.Client/Commands/ShowWallmountsCommand.cs
new file mode 100644
index 0000000000..d44c13911b
--- /dev/null
+++ b/Content.Client/Commands/ShowWallmountsCommand.cs
@@ -0,0 +1,24 @@
+using Content.Client.Wall;
+using Robust.Client.Graphics;
+using Robust.Shared.Console;
+
+namespace Content.Client.Commands;
+
+///
+/// Shows the area in which entities with can be interacted from.
+///
+public sealed class ShowWallmountsCommand : LocalizedCommands
+{
+ [Dependency] private readonly IOverlayManager _overlay = default!;
+
+ public override string Command => "showwallmounts";
+
+ public override void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ var existing = _overlay.RemoveOverlay();
+ if (!existing)
+ _overlay.AddOverlay(new WallmountDebugOverlay());
+
+ shell.WriteLine(Loc.GetString("cmd-showwallmounts-status", ("status", !existing)));
+ }
+}
diff --git a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs
index d5fee2bdda..6041b405c9 100644
--- a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs
+++ b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs
@@ -30,7 +30,10 @@ namespace Content.Client.Construction.UI
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
+ [Dependency] private readonly ILogManager _logManager = default!;
+
private readonly SpriteSystem _spriteSystem;
+ private readonly ISawmill _sawmill;
private readonly IConstructionMenuView _constructionView;
private readonly EntityWhitelistSystem _whitelistSystem;
@@ -90,6 +93,7 @@ namespace Content.Client.Construction.UI
_constructionView = new ConstructionMenu();
_whitelistSystem = _entManager.System();
_spriteSystem = _entManager.System();
+ _sawmill = _logManager.GetSawmill("construction.ui");
// This is required so that if we load after the system is initialized, we can bind to it immediately
if (_systemManager.TryGetEntitySystem(out var constructionSystem))
@@ -284,7 +288,7 @@ namespace Content.Client.Construction.UI
if (!_constructionSystem!.TryGetRecipePrototype(recipe.ID, out var targetProtoId))
{
- Logger.Error("Cannot find the target prototype in the recipe cache with the id \"{0}\" of {1}.",
+ _sawmill.Error("Cannot find the target prototype in the recipe cache with the id \"{0}\" of {1}.",
recipe.ID,
nameof(ConstructionPrototype));
continue;
diff --git a/Content.Client/Construction/UI/FlatpackCreatorMenu.xaml b/Content.Client/Construction/UI/FlatpackCreatorMenu.xaml
index eec5c229cb..322e4e66a9 100644
--- a/Content.Client/Construction/UI/FlatpackCreatorMenu.xaml
+++ b/Content.Client/Construction/UI/FlatpackCreatorMenu.xaml
@@ -8,7 +8,7 @@
-
+
diff --git a/Content.Client/Construction/UI/FlatpackCreatorMenu.xaml.cs b/Content.Client/Construction/UI/FlatpackCreatorMenu.xaml.cs
index 427193ba06..7db15596bd 100644
--- a/Content.Client/Construction/UI/FlatpackCreatorMenu.xaml.cs
+++ b/Content.Client/Construction/UI/FlatpackCreatorMenu.xaml.cs
@@ -1,13 +1,11 @@
using System.Linq;
using Content.Client.Materials;
-using Content.Client.Materials.UI;
using Content.Client.Message;
using Content.Client.UserInterface.Controls;
using Content.Shared.Construction.Components;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Materials;
using Robust.Client.AutoGenerated;
-using Robust.Client.GameObjects;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
@@ -24,14 +22,12 @@ public sealed partial class FlatpackCreatorMenu : FancyWindow
private readonly ItemSlotsSystem _itemSlots;
private readonly FlatpackSystem _flatpack;
private readonly MaterialStorageSystem _materialStorage;
- private readonly SpriteSystem _spriteSystem;
private EntityUid _owner;
public static readonly EntProtoId NoBoardEffectId = "FlatpackerNoBoardEffect";
private EntityUid? _currentBoard = EntityUid.Invalid;
- private EntityUid? _machinePreview;
public event Action? PackButtonPressed;
@@ -43,7 +39,6 @@ public sealed partial class FlatpackCreatorMenu : FancyWindow
_itemSlots = _entityManager.System();
_flatpack = _entityManager.System();
_materialStorage = _entityManager.System();
- _spriteSystem = _entityManager.System();
PackButton.OnPressed += _ => PackButtonPressed?.Invoke();
@@ -60,76 +55,54 @@ public sealed partial class FlatpackCreatorMenu : FancyWindow
{
base.FrameUpdate(args);
- if (_machinePreview is not { } && _entityManager.Deleted(_machinePreview))
- {
- _machinePreview = null;
- MachineSprite.SetEntity(_machinePreview);
- }
-
if (!_entityManager.TryGetComponent(_owner, out var flatpacker) ||
!_itemSlots.TryGetSlot(_owner, flatpacker.SlotId, out var itemSlot))
return;
- MachineBoardComponent? machineBoardComp = null;
+ var flatpackerEnt = (_owner, flatpacker);
+
if (flatpacker.Packing)
{
PackButton.Disabled = true;
}
else if (_currentBoard != null)
{
- Dictionary cost;
- if (_entityManager.TryGetComponent(_currentBoard, out machineBoardComp))
- cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), (_currentBoard.Value, machineBoardComp));
- else
- cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), null);
-
- PackButton.Disabled = !_materialStorage.CanChangeMaterialAmount(_owner, cost);
+ PackButton.Disabled = !_flatpack.TryGetFlatpackCreationCost(flatpackerEnt, _currentBoard.Value, out var curCost)
+ || !_materialStorage.CanChangeMaterialAmount(_owner, curCost);
}
if (_currentBoard == itemSlot.Item)
return;
- if (_machinePreview != null)
- _entityManager.DeleteEntity(_machinePreview);
-
_currentBoard = itemSlot.Item;
- CostHeaderLabel.Visible = _currentBoard != null;
+ CostHeaderLabel.Visible = false;
InsertLabel.Visible = _currentBoard == null;
- if (_currentBoard is not null)
+ if (_currentBoard is null)
{
- string? prototype = null;
- Dictionary? cost = null;
+ MachineSprite.SetPrototype(NoBoardEffectId);
+ CostLabel.SetMessage(Loc.GetString("flatpacker-ui-no-board-label"));
+ MachineNameLabel.SetMessage(string.Empty);
+ PackButton.Disabled = true;
+ return;
+ }
- if (machineBoardComp != null || _entityManager.TryGetComponent(_currentBoard, out machineBoardComp))
- {
- prototype = machineBoardComp.Prototype;
- cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), (_currentBoard.Value, machineBoardComp));
- }
- else if (_entityManager.TryGetComponent(_currentBoard, out var computerBoard))
- {
- prototype = computerBoard.Prototype;
- cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), null);
- }
-
- if (prototype is not null && cost is not null)
- {
- var proto = _prototypeManager.Index(prototype);
- _machinePreview = _entityManager.Spawn(proto.ID);
- _spriteSystem.ForceUpdate(_machinePreview.Value);
- MachineNameLabel.SetMessage(proto.Name);
- CostLabel.SetMarkup(GetCostString(cost));
- }
+ if (_flatpack.TryGetFlatpackResultPrototype(_currentBoard.Value, out var prototype) &&
+ _flatpack.TryGetFlatpackCreationCost(flatpackerEnt, _currentBoard.Value, out var cost))
+ {
+ var proto = _prototypeManager.Index(prototype);
+ MachineSprite.SetPrototype(prototype);
+ MachineNameLabel.SetMessage(proto.Name);
+ CostLabel.SetMarkup(GetCostString(cost));
+ CostHeaderLabel.Visible = true;
}
else
{
- _machinePreview = _entityManager.Spawn(NoBoardEffectId);
- CostLabel.SetMessage(Loc.GetString("flatpacker-ui-no-board-label"));
- MachineNameLabel.SetMessage(" ");
+ MachineSprite.SetPrototype(NoBoardEffectId);
+ CostLabel.SetMarkup(Loc.GetString("flatpacker-ui-board-invalid-label"));
+ MachineNameLabel.SetMessage(string.Empty);
PackButton.Disabled = true;
}
-
- MachineSprite.SetEntity(_machinePreview);
}
private string GetCostString(Dictionary costs)
@@ -151,7 +124,7 @@ public sealed partial class FlatpackCreatorMenu : FancyWindow
("amount", amountText),
("material", Loc.GetString(matProto.Name)));
- msg.AddMarkup(text);
+ msg.TryAddMarkup(text, out _);
if (i != orderedCosts.Length - 1)
msg.PushNewline();
@@ -159,12 +132,4 @@ public sealed partial class FlatpackCreatorMenu : FancyWindow
return msg.ToMarkup();
}
-
- public override void Close()
- {
- base.Close();
-
- _entityManager.DeleteEntity(_machinePreview);
- _machinePreview = null;
- }
}
diff --git a/Content.Client/Content.Client.csproj b/Content.Client/Content.Client.csproj
index d8855ce508..e849d9313b 100644
--- a/Content.Client/Content.Client.csproj
+++ b/Content.Client/Content.Client.csproj
@@ -1,26 +1,24 @@
-
- $(TargetFramework)
- 12
- false
false
..\bin\Content.Client\
Exe
RA0032;nullable
- enable
- Debug;Release;Tools;DebugOpt
- AnyCPU
+
+
+
+
+
+
+
+
+
-
-
-
-
diff --git a/Content.Client/DeviceLinking/Systems/RandomGateSystem.cs b/Content.Client/DeviceLinking/Systems/RandomGateSystem.cs
new file mode 100644
index 0000000000..db0d956c72
--- /dev/null
+++ b/Content.Client/DeviceLinking/Systems/RandomGateSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared.DeviceLinking.Systems;
+
+namespace Content.Client.DeviceLinking.Systems;
+
+public sealed class RandomGateSystem : SharedRandomGateSystem;
diff --git a/Content.Client/DeviceLinking/UI/RandomGateBoundUserInterface.cs b/Content.Client/DeviceLinking/UI/RandomGateBoundUserInterface.cs
new file mode 100644
index 0000000000..c8c97b84c6
--- /dev/null
+++ b/Content.Client/DeviceLinking/UI/RandomGateBoundUserInterface.cs
@@ -0,0 +1,37 @@
+using Content.Shared.DeviceLinking;
+using JetBrains.Annotations;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.DeviceLinking.UI;
+
+[UsedImplicitly]
+public sealed class RandomGateBoundUserInterface : BoundUserInterface
+{
+ private RandomGateSetupWindow? _window;
+
+ public RandomGateBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { }
+
+ protected override void Open()
+ {
+ base.Open();
+ _window = this.CreateWindow();
+ _window.OnApplyPressed += OnProbabilityChanged;
+ }
+
+ private void OnProbabilityChanged(string value)
+ {
+ if (!float.TryParse(value, out var probability))
+ return;
+
+ SendPredictedMessage(new RandomGateProbabilityChangedMessage(probability));
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+ if (state is not RandomGateBoundUserInterfaceState castState || _window == null)
+ return;
+
+ _window.SetProbability(castState.SuccessProbability * 100);
+ }
+}
diff --git a/Content.Client/DeviceLinking/UI/RandomGateSetupWindow.xaml b/Content.Client/DeviceLinking/UI/RandomGateSetupWindow.xaml
new file mode 100644
index 0000000000..d9273c9110
--- /dev/null
+++ b/Content.Client/DeviceLinking/UI/RandomGateSetupWindow.xaml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/DeviceLinking/UI/RandomGateSetupWindow.xaml.cs b/Content.Client/DeviceLinking/UI/RandomGateSetupWindow.xaml.cs
new file mode 100644
index 0000000000..64e0690072
--- /dev/null
+++ b/Content.Client/DeviceLinking/UI/RandomGateSetupWindow.xaml.cs
@@ -0,0 +1,28 @@
+using Content.Client.UserInterface.Controls;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.DeviceLinking.UI;
+
+///
+/// Window for setting up the random gate probability.
+///
+[GenerateTypedNameReferences]
+public sealed partial class RandomGateSetupWindow : FancyWindow
+{
+ ///
+ /// Event triggered when the "Apply" button is pressed.
+ ///
+ public event Action? OnApplyPressed;
+
+ public RandomGateSetupWindow()
+ {
+ RobustXamlLoader.Load(this);
+ ApplyButton.OnPressed += _ => OnApplyPressed?.Invoke(ProbabilityInput.Text);
+ }
+
+ public void SetProbability(float probability)
+ {
+ ProbabilityInput.Text = probability.ToString("0.00");
+ }
+}
diff --git a/Content.Client/Guidebook/Controls/GuidebookRichPrototypeLink.cs b/Content.Client/Guidebook/Controls/GuidebookRichPrototypeLink.cs
index b54dd8b701..1e8b524997 100644
--- a/Content.Client/Guidebook/Controls/GuidebookRichPrototypeLink.cs
+++ b/Content.Client/Guidebook/Controls/GuidebookRichPrototypeLink.cs
@@ -42,7 +42,7 @@ public sealed class GuidebookRichPrototypeLink : Control, IPrototypeLinkControl
public void SetMessage(FormattedMessage message)
{
_message = message;
- _richTextLabel.SetMessage(_message);
+ _richTextLabel.SetMessage(_message, tagsAllowed: null);
}
public IPrototype? LinkedPrototype { get; set; }
diff --git a/Content.Client/Guidebook/DocumentParsingManager.static.cs b/Content.Client/Guidebook/DocumentParsingManager.static.cs
index 3e37942381..c702ac97ad 100644
--- a/Content.Client/Guidebook/DocumentParsingManager.static.cs
+++ b/Content.Client/Guidebook/DocumentParsingManager.static.cs
@@ -82,7 +82,7 @@ public sealed partial class DocumentParsingManager
}
msg.Pop();
- rt.SetMessage(msg);
+ rt.SetMessage(msg, tagsAllowed: null);
return rt;
},
TextParser)
diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerBoundUserInterface.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerBoundUserInterface.cs
index b054b7a14f..98e42ef886 100644
--- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerBoundUserInterface.cs
+++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerBoundUserInterface.cs
@@ -21,9 +21,9 @@ namespace Content.Client.HealthAnalyzer.UI
base.Open();
_window = this.CreateWindow();
- _window.OnBodyPartSelected += SendBodyPartMessage; // Shitmed Change
- _window.OnTriageStatusChanged += SendTriageStatusMessage; // DeltaV - Medical Records
- _window.OnClaimPatient += SendTriageClaimMessage; // DeltaV - Medical Records
+ _window.HealthAnalyzer.OnBodyPartSelected += SendBodyPartMessage; // Shitmed Change
+ _window.HealthAnalyzer.OnTriageStatusChanged += SendTriageStatusMessage; // DeltaV - Medical Records
+ _window.HealthAnalyzer.OnClaimPatient += SendTriageClaimMessage; // DeltaV - Medical Records
_window.Title = EntMan.GetComponent(Owner).EntityName;
}
@@ -48,7 +48,7 @@ namespace Content.Client.HealthAnalyzer.UI
return;
if (_window != null)
- _window.OnBodyPartSelected -= SendBodyPartMessage;
+ _window.HealthAnalyzer.OnBodyPartSelected -= SendBodyPartMessage;
_window?.Dispose();
}
diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml
new file mode 100644
index 0000000000..2df80d0f40
--- /dev/null
+++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml
@@ -0,0 +1,219 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml.cs
new file mode 100644
index 0000000000..1368b6580d
--- /dev/null
+++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml.cs
@@ -0,0 +1,441 @@
+using System.Linq;
+using System.Numerics;
+using Content.Shared.Atmos;
+using Content.Shared.Damage.Components;
+using Content.Shared.Damage.Prototypes;
+using Content.Shared.FixedPoint;
+using Content.Shared.Humanoid;
+using Content.Shared.Humanoid.Prototypes;
+using Content.Shared.IdentityManagement;
+using Content.Shared.MedicalScanner;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Components;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+namespace Content.Client.HealthAnalyzer.UI;
+
+using Content.Client._DV.Traits.Assorted; // DeltaV
+using Content.Shared._DV.Traits.Assorted; // DeltaV
+using Content.Shared._DV.Medical; // DeltaV - Uncloneable
+using Content.Shared._DV.MedicalRecords; // DeltaV - Medical Records
+using Content.Shared._Shitmed.Targeting; // Shitmed
+
+// Health analyzer UI is split from its window because it's used by both the
+// health analyzer item and the cryo pod UI.
+
+[GenerateTypedNameReferences]
+public sealed partial class HealthAnalyzerControl : BoxContainer
+{
+ private readonly IEntityManager _entityManager;
+ private readonly SpriteSystem _spriteSystem;
+ private readonly IPrototypeManager _prototypes;
+ private readonly IResourceCache _cache;
+
+ private readonly UnborgableSystem _unborgable; // DeltaV
+ private readonly RedshirtSystem _redshirt; // DeltaV
+ private readonly UncloneableSystem _uncloneable; // DeltaV
+
+ // Shitmed Change Start
+ public event Action? OnBodyPartSelected;
+ private EntityUid _spriteViewEntity;
+
+ private readonly EntProtoId _bodyView = "AlertSpriteView";
+
+ private readonly Dictionary _bodyPartControls;
+ private EntityUid? _target;
+ // Shitmed Change End
+
+ // Begin DeltaV - Medical Records
+ private readonly ButtonGroup _triageStatusGroup = new();
+ private readonly Dictionary _triageControls = new();
+
+ public event Action? OnTriageStatusChanged;
+ public event Action? OnClaimPatient;
+ // End DeltaV - Medical Records
+
+ public HealthAnalyzerControl()
+ {
+ RobustXamlLoader.Load(this);
+
+ var dependencies = IoCManager.Instance!;
+ _entityManager = dependencies.Resolve();
+ _spriteSystem = _entityManager.System();
+ _prototypes = dependencies.Resolve();
+ _cache = dependencies.Resolve();
+
+ _unborgable = _entityManager.System(); // DeltaV
+ _redshirt = _entityManager.System(); // DeltaV
+ _uncloneable = _entityManager.System(); // DeltaV
+ // Shitmed Change Start
+ _bodyPartControls = new Dictionary
+ {
+ { TargetBodyPart.Head, HeadButton },
+ { TargetBodyPart.Torso, ChestButton },
+ { TargetBodyPart.Groin, GroinButton },
+ { TargetBodyPart.LeftArm, LeftArmButton },
+ { TargetBodyPart.LeftHand, LeftHandButton },
+ { TargetBodyPart.RightArm, RightArmButton },
+ { TargetBodyPart.RightHand, RightHandButton },
+ { TargetBodyPart.LeftLeg, LeftLegButton },
+ { TargetBodyPart.LeftFoot, LeftFootButton },
+ { TargetBodyPart.RightLeg, RightLegButton },
+ { TargetBodyPart.RightFoot, RightFootButton },
+ };
+
+ foreach (var bodyPartButton in _bodyPartControls)
+ {
+ bodyPartButton.Value.MouseFilter = MouseFilterMode.Stop;
+ bodyPartButton.Value.OnPressed += _ => SetActiveBodyPart(bodyPartButton.Key, bodyPartButton.Value);
+ }
+ ReturnButton.OnPressed += _ => ResetBodyPart();
+ // Shitmed Change End
+
+ // Begin DeltaV - Medical Records
+ foreach (var item in Enum.GetValues())
+ {
+ var btn = new Button();
+ var ftlKey = item.ToString();
+ btn.Group = _triageStatusGroup;
+ btn.Text = Loc.GetString($"health-analyzer-window-triage-status-{ftlKey}");
+ btn.ToolTip = Loc.GetString($"health-analyzer-window-triage-status-{ftlKey}.ToolTip");
+ btn.OnPressed += _ => OnTriageStatusChanged?.Invoke(item);
+ btn.AddStyleClass("ButtonSquare");
+ StatusBox.AddChild(btn);
+ _triageControls[item] = btn;
+ }
+ StatusBox.Children.First().AddStyleClass("OpenRight");
+ StatusBox.Children.First().RemoveStyleClass("ButtonSquare");
+ StatusBox.Children.Last().AddStyleClass("OpenLeft");
+ StatusBox.Children.Last().RemoveStyleClass("ButtonSquare");
+ ClaimButton.OnPressed += _ => OnClaimPatient?.Invoke();
+ // End DeltaV - Medical Records
+ }
+
+ public void Populate(HealthAnalyzerUiState state)
+ {
+ var target = _entityManager.GetEntity(state.TargetEntity);
+ // Begin Shitmed
+ _target = target;
+ EntityUid? part = state.Part != null ? _entityManager.GetEntity(state.Part.Value) : null;
+ var isPart = part != null;
+ // End Shitmed
+
+ if (target == null
+ || !_entityManager.TryGetComponent(isPart ? part : target, out var damageable)) // Shitmed
+ {
+ NoPatientDataText.Visible = true;
+ return;
+ }
+
+ // Begin Shitmed
+ ReturnButton.Visible = isPart;
+ PartNameLabel.Visible = isPart;
+
+ if (part != null)
+ PartNameLabel.Text = _entityManager.HasComponent(part)
+ ? Identity.Name(part.Value, _entityManager)
+ : Loc.GetString("health-analyzer-window-entity-unknown-value-text");
+ // End Shitmed
+
+ NoPatientDataText.Visible = false;
+
+ // Scan Mode
+
+ ScanModeLabel.Text = state.ScanMode.HasValue
+ ? state.ScanMode.Value
+ ? Loc.GetString("health-analyzer-window-scan-mode-active")
+ : Loc.GetString("health-analyzer-window-scan-mode-inactive")
+ : Loc.GetString("health-analyzer-window-entity-unknown-text");
+
+ ScanModeLabel.FontColorOverride = state.ScanMode.HasValue && state.ScanMode.Value ? Color.Green : Color.Red;
+
+ // Patient Information
+
+ SpriteView.SetEntity(SetupIcon(state.Body) ?? target.Value); // Shitmed Change
+ SpriteView.Visible = state.ScanMode.HasValue && state.ScanMode.Value;
+ PartView.Visible = SpriteView.Visible; // Shitmed Change
+ NoDataTex.Visible = !SpriteView.Visible;
+
+ var name = new FormattedMessage();
+ name.PushColor(Color.White);
+ name.AddText(_entityManager.HasComponent(target.Value)
+ ? Identity.Name(target.Value, _entityManager)
+ : Loc.GetString("health-analyzer-window-entity-unknown-text"));
+ NameLabel.SetMessage(name);
+
+ SpeciesLabel.Text =
+ _entityManager.TryGetComponent(target.Value,
+ out var humanoidAppearanceComponent)
+ ? Loc.GetString(_prototypes.Index(humanoidAppearanceComponent.Species).Name)
+ : Loc.GetString("health-analyzer-window-entity-unknown-species-text");
+
+ // Basic Diagnostic
+
+ TemperatureLabel.Text = !float.IsNaN(state.Temperature)
+ ? $"{state.Temperature - Atmospherics.T0C:F1} °C ({state.Temperature:F1} K)"
+ : Loc.GetString("health-analyzer-window-entity-unknown-value-text");
+
+ BloodLabel.Text = !float.IsNaN(state.BloodLevel)
+ ? $"{state.BloodLevel * 100:F1} %"
+ : Loc.GetString("health-analyzer-window-entity-unknown-value-text");
+
+ StatusLabel.Text =
+ _entityManager.TryGetComponent(target.Value, out var mobStateComponent)
+ ? GetStatus(mobStateComponent.CurrentState)
+ : Loc.GetString("health-analyzer-window-entity-unknown-text");
+
+ // Total Damage
+
+ DamageLabel.Text = damageable.TotalDamage.ToString();
+
+ // Alerts
+ // DeltaV traits - This is going to be horrid if we just keep adding things like this.
+ var unborgable = _unborgable.IsUnborgable(target.Value);
+ var redshirt = _redshirt.IsRedshirt(target.Value) && mobStateComponent?.CurrentState == MobState.Dead; // DeltaV - Redshirt
+ var uncloneable = _uncloneable.IsUncloneable(target.Value) && mobStateComponent?.CurrentState == MobState.Dead; // DeltaV - Unclonable
+ // END DeltaV
+
+ var showAlerts = state.Unrevivable == true || state.Bleeding == true;
+
+ AlertsDivider.Visible = showAlerts;
+ AlertsContainer.Visible = showAlerts;
+
+ if (showAlerts)
+ AlertsContainer.RemoveAllChildren();
+
+ if (state.Unrevivable == true)
+ AlertsContainer.AddChild(new RichTextLabel
+ {
+ Text = Loc.GetString("health-analyzer-window-entity-unrevivable-text"),
+ Margin = new Thickness(0, 4),
+ MaxWidth = 300
+ });
+
+ if (state.Bleeding == true)
+ AlertsContainer.AddChild(new RichTextLabel
+ {
+ Text = Loc.GetString("health-analyzer-window-entity-bleeding-text"),
+ Margin = new Thickness(0, 4),
+ MaxWidth = 300
+ });
+
+ if (unborgable) // DeltaV
+ AlertsContainer.AddChild(new RichTextLabel
+ {
+ Text = Loc.GetString("health-analyzer-window-entity-unborgable-text"),
+ Margin = new Thickness(0, 4),
+ MaxWidth = 300
+ });
+
+ if (redshirt) // DeltaV
+ AlertsContainer.AddChild(new RichTextLabel
+ {
+ Text = Loc.GetString("health-analyzer-window-entity-redshirt-text"),
+ Margin = new Thickness(0, 4),
+ MaxWidth = 300
+ });
+
+ if (uncloneable) // DeltaV - Uncloneable
+ AlertsContainer.AddChild(new RichTextLabel
+ {
+ Text = Loc.GetString("health-analyzer-window-entity-uncloneable-text"),
+ Margin = new Thickness(0, 4),
+ MaxWidth = 300
+ });
+
+ // Damage Groups
+
+ var damageSortedGroups =
+ damageable.DamagePerGroup.OrderByDescending(damage => damage.Value)
+ .ToDictionary(x => x.Key, x => x.Value);
+
+ IReadOnlyDictionary damagePerType = damageable.Damage.DamageDict;
+
+ DrawDiagnosticGroups(damageSortedGroups, damagePerType);
+
+ // Begin DeltaV - Medical Records
+ if (state.MedicalRecord is not { } records)
+ {
+ TriageControls.Visible = false;
+ return;
+ }
+
+ TriageControls.Visible = true;
+ _triageControls[records.Status].Pressed = true;
+
+ // Update claim button based on claimed status
+ if (records.ClaimedName != null)
+ {
+ ClaimButton.Text = Loc.GetString("health-analyzer-window-triage-unclaim", ("claimedBy", records.ClaimedName));
+ }
+ else
+ {
+ ClaimButton.Text = Loc.GetString("health-analyzer-window-triage-claim");
+ }
+ // End DeltaV - Medical Records
+ }
+
+ private static string GetStatus(MobState mobState)
+ {
+ return mobState switch
+ {
+ MobState.Alive => Loc.GetString("health-analyzer-window-entity-alive-text"),
+ MobState.Critical => Loc.GetString("health-analyzer-window-entity-critical-text"),
+ MobState.Dead => Loc.GetString("health-analyzer-window-entity-dead-text"),
+ _ => Loc.GetString("health-analyzer-window-entity-unknown-text"),
+ };
+ }
+
+ private void DrawDiagnosticGroups(
+ Dictionary groups,
+ IReadOnlyDictionary damageDict)
+ {
+ GroupsContainer.RemoveAllChildren();
+
+ foreach (var (damageGroupId, damageAmount) in groups)
+ {
+ if (damageAmount == 0)
+ continue;
+
+ var groupTitleText = $"{Loc.GetString(
+ "health-analyzer-window-damage-group-text",
+ ("damageGroup", _prototypes.Index(damageGroupId).LocalizedName),
+ ("amount", damageAmount)
+ )}";
+
+ var groupContainer = new BoxContainer
+ {
+ Align = AlignMode.Begin,
+ Orientation = LayoutOrientation.Vertical,
+ };
+
+ groupContainer.AddChild(CreateDiagnosticGroupTitle(groupTitleText, damageGroupId));
+
+ GroupsContainer.AddChild(groupContainer);
+
+ // Show the damage for each type in that group.
+ var group = _prototypes.Index(damageGroupId);
+
+ foreach (var type in group.DamageTypes)
+ {
+ if (!damageDict.TryGetValue(type, out var typeAmount) || typeAmount <= 0)
+ continue;
+
+ var damageString = Loc.GetString(
+ "health-analyzer-window-damage-type-text",
+ ("damageType", _prototypes.Index(type).LocalizedName),
+ ("amount", typeAmount)
+ );
+
+ groupContainer.AddChild(CreateDiagnosticItemLabel(damageString.Insert(0, " · ")));
+ }
+ }
+ }
+
+ // Shitmed Change Start
+ public void SetActiveBodyPart(TargetBodyPart part, TextureButton button)
+ {
+ if (_target == null)
+ return;
+
+ // Bit of the ole shitcode until we have Groins in the prototypes.
+ OnBodyPartSelected?.Invoke(part == TargetBodyPart.Groin ? TargetBodyPart.Torso : part, _target.Value);
+ }
+
+ public void ResetBodyPart()
+ {
+ if (_target == null)
+ return;
+
+ OnBodyPartSelected?.Invoke(null, _target.Value);
+ }
+
+ public void SetActiveButtons(bool isHumanoid)
+ {
+ foreach (var button in _bodyPartControls)
+ button.Value.Visible = isHumanoid;
+ }
+
+ ///
+ /// Sets up the Body Doll using Alert Entity to use in Health Analyzer.
+ ///
+ private EntityUid? SetupIcon(Dictionary? body)
+ {
+ if (body is null)
+ return null;
+
+ if (!_entityManager.Deleted(_spriteViewEntity))
+ _entityManager.QueueDeleteEntity(_spriteViewEntity);
+
+ _spriteViewEntity = _entityManager.Spawn(_bodyView);
+
+ if (!_entityManager.TryGetComponent(_spriteViewEntity, out var sprite))
+ return null;
+
+ int layer = 0;
+ foreach (var (bodyPart, integrity) in body)
+ {
+ // TODO: PartStatusUIController and make it use layers instead of TextureRects when EE refactors alerts.
+ string enumName = Enum.GetName(typeof(TargetBodyPart), bodyPart) ?? "Unknown";
+ int enumValue = (int) integrity;
+ var rsi = new SpriteSpecifier.Rsi(new ResPath($"/Textures/_Shitmed/Interface/Targeting/Status/{enumName.ToLowerInvariant()}.rsi"), $"{enumName.ToLowerInvariant()}_{enumValue}");
+ // Shitcode with love from Russia :)
+ if (!_spriteSystem.TryGetLayer(_spriteViewEntity, layer, out _, false))
+ _spriteSystem.AddTextureLayer(_spriteViewEntity, _spriteSystem.Frame0(rsi));
+ else
+ _spriteSystem.LayerSetTexture(_spriteViewEntity, layer, _spriteSystem.Frame0(rsi));
+ _spriteSystem.LayerSetScale(_spriteViewEntity, layer, new Vector2(3f, 3f));
+ layer++;
+ }
+ return _spriteViewEntity;
+ }
+ // Shitmed Change End
+
+ private Texture GetTexture(string texture)
+ {
+ var rsiPath = new ResPath("/Textures/Objects/Devices/health_analyzer.rsi");
+ var rsiSprite = new SpriteSpecifier.Rsi(rsiPath, texture);
+
+ var rsi = _cache.GetResource(rsiSprite.RsiPath).RSI;
+ if (!rsi.TryGetState(rsiSprite.RsiState, out var state))
+ {
+ rsiSprite = new SpriteSpecifier.Rsi(rsiPath, "unknown");
+ }
+
+ return _spriteSystem.Frame0(rsiSprite);
+ }
+
+ private static Label CreateDiagnosticItemLabel(string text)
+ {
+ return new Label
+ {
+ Text = text,
+ };
+ }
+
+ private BoxContainer CreateDiagnosticGroupTitle(string text, string id)
+ {
+ var rootContainer = new BoxContainer
+ {
+ Margin = new Thickness(0, 6, 0, 0),
+ VerticalAlignment = VAlignment.Bottom,
+ Orientation = LayoutOrientation.Horizontal,
+ };
+
+ rootContainer.AddChild(new TextureRect
+ {
+ SetSize = new Vector2(30, 30),
+ Texture = GetTexture(id.ToLower())
+ });
+
+ rootContainer.AddChild(CreateDiagnosticItemLabel(text));
+
+ return rootContainer;
+ }
+}
diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml
index 015d607265..fde542d64a 100644
--- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml
+++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml
@@ -1,253 +1,16 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
index 74c73a9006..6c0ed360b0 100644
--- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
+++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
@@ -1,440 +1,20 @@
-using System.Linq;
-using System.Numerics;
-using Content.Client._DV.Traits.Assorted; // DeltaV
-using Content.Shared._DV.Traits.Assorted; // DeltaV
-using Content.Shared._DV.Medical; // DeltaV - Uncloneable
-using Content.Shared.Atmos;
using Content.Client.UserInterface.Controls;
-using Content.Shared._DV.MedicalRecords; // DeltaV - Medical Records
-using Content.Shared._Shitmed.Targeting; // Shitmed
-using Content.Shared.Damage.Components;
-using Content.Shared.Damage.Prototypes;
-using Content.Shared.FixedPoint;
-using Content.Shared.Humanoid;
-using Content.Shared.Humanoid.Prototypes;
-using Content.Shared.IdentityManagement;
using Content.Shared.MedicalScanner;
-using Content.Shared.Mobs;
-using Content.Shared.Mobs.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
-using Robust.Client.GameObjects;
-using Robust.Client.Graphics;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.ResourceManagement;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Utility;
+namespace Content.Client.HealthAnalyzer.UI;
-namespace Content.Client.HealthAnalyzer.UI
+[GenerateTypedNameReferences]
+public sealed partial class HealthAnalyzerWindow : FancyWindow
{
- [GenerateTypedNameReferences]
- public sealed partial class HealthAnalyzerWindow : FancyWindow
+ public HealthAnalyzerWindow()
{
- private readonly IEntityManager _entityManager;
- private readonly SpriteSystem _spriteSystem;
- private readonly IPrototypeManager _prototypes;
- private readonly IResourceCache _cache;
- private readonly UnborgableSystem _unborgable; // DeltaV
- private readonly RedshirtSystem _redshirt; // DeltaV
- private readonly UncloneableSystem _uncloneable; // DeltaV
+ RobustXamlLoader.Load(this);
+ }
- // Shitmed Change Start
- public event Action? OnBodyPartSelected;
- private EntityUid _spriteViewEntity;
-
- [ValidatePrototypeId]
- private readonly EntProtoId _bodyView = "AlertSpriteView";
-
- private readonly Dictionary _bodyPartControls;
- private EntityUid? _target;
- // Shitmed Change End
-
- // Begin DeltaV - Medical Records
- private readonly ButtonGroup _triageStatusGroup = new();
- private readonly Dictionary _triageControls = new();
-
- public event Action? OnTriageStatusChanged;
- public event Action? OnClaimPatient;
- // End DeltaV - Medical Records
-
- public HealthAnalyzerWindow()
- {
- RobustXamlLoader.Load(this);
-
- var dependencies = IoCManager.Instance!;
- _entityManager = dependencies.Resolve();
- _spriteSystem = _entityManager.System();
- _prototypes = dependencies.Resolve();
- _cache = dependencies.Resolve();
- _unborgable = _entityManager.System(); // DeltaV
- _redshirt = _entityManager.System(); // DeltaV
- _uncloneable = _entityManager.System(); // DeltaV
- // Shitmed Change Start
- _bodyPartControls = new Dictionary
- {
- { TargetBodyPart.Head, HeadButton },
- { TargetBodyPart.Torso, ChestButton },
- { TargetBodyPart.Groin, GroinButton },
- { TargetBodyPart.LeftArm, LeftArmButton },
- { TargetBodyPart.LeftHand, LeftHandButton },
- { TargetBodyPart.RightArm, RightArmButton },
- { TargetBodyPart.RightHand, RightHandButton },
- { TargetBodyPart.LeftLeg, LeftLegButton },
- { TargetBodyPart.LeftFoot, LeftFootButton },
- { TargetBodyPart.RightLeg, RightLegButton },
- { TargetBodyPart.RightFoot, RightFootButton },
- };
-
- foreach (var bodyPartButton in _bodyPartControls)
- {
- bodyPartButton.Value.MouseFilter = MouseFilterMode.Stop;
- bodyPartButton.Value.OnPressed += _ => SetActiveBodyPart(bodyPartButton.Key, bodyPartButton.Value);
- }
- ReturnButton.OnPressed += _ => ResetBodyPart();
- // Shitmed Change End
-
- // Begin DeltaV - Medical Records
- foreach (var item in Enum.GetValues())
- {
- var btn = new Button();
- var ftlKey = item.ToString();
- btn.Group = _triageStatusGroup;
- btn.Text = Loc.GetString($"health-analyzer-window-triage-status-{ftlKey}");
- btn.ToolTip = Loc.GetString($"health-analyzer-window-triage-status-{ftlKey}.ToolTip");
- btn.OnPressed += _ => OnTriageStatusChanged?.Invoke(item);
- btn.AddStyleClass("ButtonSquare");
- StatusBox.AddChild(btn);
- _triageControls[item] = btn;
- }
- StatusBox.Children.First().AddStyleClass("OpenRight");
- StatusBox.Children.First().RemoveStyleClass("ButtonSquare");
- StatusBox.Children.Last().AddStyleClass("OpenLeft");
- StatusBox.Children.Last().RemoveStyleClass("ButtonSquare");
- ClaimButton.OnPressed += _ => OnClaimPatient?.Invoke();
- // End DeltaV - Medical Records
- }
-
- // Shitmed Change Start
- public void SetActiveBodyPart(TargetBodyPart part, TextureButton button)
- {
- if (_target == null)
- return;
-
- // Bit of the ole shitcode until we have Groins in the prototypes.
- OnBodyPartSelected?.Invoke(part == TargetBodyPart.Groin ? TargetBodyPart.Torso : part, _target.Value);
- }
-
- public void ResetBodyPart()
- {
- if (_target == null)
- return;
-
- OnBodyPartSelected?.Invoke(null, _target.Value);
- }
-
- public void SetActiveButtons(bool isHumanoid)
- {
- foreach (var button in _bodyPartControls)
- button.Value.Visible = isHumanoid;
- }
-
- // Not all of this function got messed with, but it was spread enough to warrant being covered entirely by a Shitmed Change
- public void Populate(HealthAnalyzerScannedUserMessage msg)
- {
- // Start-Shitmed
- _target = _entityManager.GetEntity(msg.TargetEntity);
- EntityUid? part = msg.Part != null ? _entityManager.GetEntity(msg.Part.Value) : null;
- var isPart = part != null;
-
- if (_target == null
- || !_entityManager.TryGetComponent(isPart ? part : _target, out var damageable))
- {
- NoPatientDataText.Visible = true;
- return;
- }
-
- SetActiveButtons(_entityManager.HasComponent(_target.Value));
-
- ReturnButton.Visible = isPart;
- PartNameLabel.Visible = isPart;
-
- if (part != null)
- PartNameLabel.Text = _entityManager.HasComponent(part)
- ? Identity.Name(part.Value, _entityManager)
- : Loc.GetString("health-analyzer-window-entity-unknown-value-text");
-
- NoPatientDataText.Visible = false;
-
- // Scan Mode
-
- ScanModeLabel.Text = msg.ScanMode.HasValue
- ? msg.ScanMode.Value
- ? Loc.GetString("health-analyzer-window-scan-mode-active")
- : Loc.GetString("health-analyzer-window-scan-mode-inactive")
- : Loc.GetString("health-analyzer-window-entity-unknown-text");
-
- ScanModeLabel.FontColorOverride = msg.ScanMode.HasValue && msg.ScanMode.Value ? Color.Green : Color.Red;
-
- // Patient Information
-
- SpriteView.SetEntity(SetupIcon(msg.Body) ?? _target.Value);
- SpriteView.Visible = msg.ScanMode.HasValue && msg.ScanMode.Value;
- PartView.Visible = SpriteView.Visible;
- NoDataTex.Visible = !SpriteView.Visible;
-
- var name = new FormattedMessage();
- name.PushColor(Color.White);
- name.AddText(_entityManager.HasComponent(_target.Value)
- ? Identity.Name(_target.Value, _entityManager)
- : Loc.GetString("health-analyzer-window-entity-unknown-text"));
- NameLabel.SetMessage(name);
-
- SpeciesLabel.Text =
- _entityManager.TryGetComponent(_target.Value,
- out var humanoidAppearanceComponent)
- ? Loc.GetString(_prototypes.Index(humanoidAppearanceComponent.Species).Name)
- : Loc.GetString("health-analyzer-window-entity-unknown-species-text");
-
- // Basic Diagnostic
-
- TemperatureLabel.Text = !float.IsNaN(msg.Temperature)
- ? $"{msg.Temperature - Atmospherics.T0C:F1} °C ({msg.Temperature:F1} K)"
- : Loc.GetString("health-analyzer-window-entity-unknown-value-text");
-
- BloodLabel.Text = !float.IsNaN(msg.BloodLevel)
- ? $"{msg.BloodLevel * 100:F1} %"
- : Loc.GetString("health-analyzer-window-entity-unknown-value-text");
-
- StatusLabel.Text =
- _entityManager.TryGetComponent(_target.Value, out var mobStateComponent)
- ? GetStatus(mobStateComponent.CurrentState)
- : Loc.GetString("health-analyzer-window-entity-unknown-text");
-
- // Total Damage
-
- DamageLabel.Text = damageable.TotalDamage.ToString();
-
- // Alerts
- // DeltaV traits - This is going to be horrid if we just keep adding things like this.
- var unborgable = _unborgable.IsUnborgable(_target.Value);
- var redshirt = _redshirt.IsRedshirt(_target.Value) && mobStateComponent?.CurrentState == MobState.Dead; // DeltaV - Redshirt
- var uncloneable = _uncloneable.IsUncloneable(_target.Value) && mobStateComponent?.CurrentState == MobState.Dead; // DeltaV - Unclonable
- // END DeltaV
-
- var showAlerts = msg.Unrevivable == true || msg.Bleeding == true || unborgable || redshirt || uncloneable; // DeltaV
-
- AlertsDivider.Visible = showAlerts;
- AlertsContainer.Visible = showAlerts;
-
- if (showAlerts)
- AlertsContainer.RemoveAllChildren();
-
- if (msg.Unrevivable == true)
- AlertsContainer.AddChild(new RichTextLabel
- {
- Text = Loc.GetString("health-analyzer-window-entity-unrevivable-text"),
- Margin = new Thickness(0, 4),
- MaxWidth = 300
- });
-
- if (msg.Bleeding == true)
- AlertsContainer.AddChild(new RichTextLabel
- {
- Text = Loc.GetString("health-analyzer-window-entity-bleeding-text"),
- Margin = new Thickness(0, 4),
- MaxWidth = 300
- });
-
- if (unborgable) // DeltaV
- AlertsContainer.AddChild(new RichTextLabel
- {
- Text = Loc.GetString("health-analyzer-window-entity-unborgable-text"),
- Margin = new Thickness(0, 4),
- MaxWidth = 300
- });
-
- if (redshirt) // DeltaV
- AlertsContainer.AddChild(new RichTextLabel
- {
- Text = Loc.GetString("health-analyzer-window-entity-redshirt-text"),
- Margin = new Thickness(0, 4),
- MaxWidth = 300
- });
-
- if (uncloneable) // DeltaV - Uncloneable
- AlertsContainer.AddChild(new RichTextLabel
- {
- Text = Loc.GetString("health-analyzer-window-entity-uncloneable-text"),
- Margin = new Thickness(0, 4),
- MaxWidth = 300
- });
-
- // Damage Groups
-
- var damageSortedGroups =
- damageable.DamagePerGroup.OrderByDescending(damage => damage.Value)
- .ToDictionary(x => x.Key, x => x.Value);
-
- IReadOnlyDictionary damagePerType = damageable.Damage.DamageDict;
-
- DrawDiagnosticGroups(damageSortedGroups, damagePerType);
-
- // Begin DeltaV - Medical Records
- if (msg.MedicalRecord is not {} records)
- {
- TriageControls.Visible = false;
- return;
- }
-
- TriageControls.Visible = true;
- _triageControls[records.Status].Pressed = true;
-
- // Update claim button based on claimed status
- if (records.ClaimedName != null)
- {
- ClaimButton.Text = Loc.GetString("health-analyzer-window-triage-unclaim", ("claimedBy", records.ClaimedName));
- }
- else
- {
- ClaimButton.Text = Loc.GetString("health-analyzer-window-triage-claim");
- }
- // End DeltaV - Medical Records
- }
- // Shitmed Change End
- private static string GetStatus(MobState mobState)
- {
- return mobState switch
- {
- MobState.Alive => Loc.GetString("health-analyzer-window-entity-alive-text"),
- MobState.Critical => Loc.GetString("health-analyzer-window-entity-critical-text"),
- MobState.Dead => Loc.GetString("health-analyzer-window-entity-dead-text"),
- _ => Loc.GetString("health-analyzer-window-entity-unknown-text"),
- };
- }
-
- private void DrawDiagnosticGroups(
- Dictionary groups,
- IReadOnlyDictionary damageDict)
- {
- GroupsContainer.RemoveAllChildren();
-
- foreach (var (damageGroupId, damageAmount) in groups)
- {
- if (damageAmount == 0)
- continue;
-
- var groupTitleText = $"{Loc.GetString(
- "health-analyzer-window-damage-group-text",
- ("damageGroup", _prototypes.Index(damageGroupId).LocalizedName),
- ("amount", damageAmount)
- )}";
-
- var groupContainer = new BoxContainer
- {
- Align = BoxContainer.AlignMode.Begin,
- Orientation = BoxContainer.LayoutOrientation.Vertical,
- };
-
- groupContainer.AddChild(CreateDiagnosticGroupTitle(groupTitleText, damageGroupId));
-
- GroupsContainer.AddChild(groupContainer);
-
- // Show the damage for each type in that group.
- var group = _prototypes.Index(damageGroupId);
-
- foreach (var type in group.DamageTypes)
- {
- if (!damageDict.TryGetValue(type, out var typeAmount) || typeAmount <= 0)
- continue;
-
- var damageString = Loc.GetString(
- "health-analyzer-window-damage-type-text",
- ("damageType", _prototypes.Index(type).LocalizedName),
- ("amount", typeAmount)
- );
-
- groupContainer.AddChild(CreateDiagnosticItemLabel(damageString.Insert(0, " · ")));
- }
- }
- }
-
- private Texture GetTexture(string texture)
- {
- var rsiPath = new ResPath("/Textures/Objects/Devices/health_analyzer.rsi");
- var rsiSprite = new SpriteSpecifier.Rsi(rsiPath, texture);
-
- var rsi = _cache.GetResource(rsiSprite.RsiPath).RSI;
- if (!rsi.TryGetState(rsiSprite.RsiState, out var state))
- {
- rsiSprite = new SpriteSpecifier.Rsi(rsiPath, "unknown");
- }
-
- return _spriteSystem.Frame0(rsiSprite);
- }
-
- private static Label CreateDiagnosticItemLabel(string text)
- {
- return new Label
- {
- Text = text,
- };
- }
-
- private BoxContainer CreateDiagnosticGroupTitle(string text, string id)
- {
- var rootContainer = new BoxContainer
- {
- Margin = new Thickness(0, 6, 0, 0),
- VerticalAlignment = VAlignment.Bottom,
- Orientation = BoxContainer.LayoutOrientation.Horizontal,
- };
-
- rootContainer.AddChild(new TextureRect
- {
- SetSize = new Vector2(30, 30),
- Texture = GetTexture(id.ToLower())
- });
-
- rootContainer.AddChild(CreateDiagnosticItemLabel(text));
-
- return rootContainer;
- }
-
- // Shitmed Change Start
- ///
- /// Sets up the Body Doll using Alert Entity to use in Health Analyzer.
- ///
- private EntityUid? SetupIcon(Dictionary? body)
- {
- if (body is null)
- return null;
-
- if (!_entityManager.Deleted(_spriteViewEntity))
- _entityManager.QueueDeleteEntity(_spriteViewEntity);
-
- _spriteViewEntity = _entityManager.Spawn(_bodyView);
-
- if (!_entityManager.TryGetComponent(_spriteViewEntity, out var sprite))
- return null;
-
- int layer = 0;
- foreach (var (bodyPart, integrity) in body)
- {
- // TODO: PartStatusUIController and make it use layers instead of TextureRects when EE refactors alerts.
- string enumName = Enum.GetName(typeof(TargetBodyPart), bodyPart) ?? "Unknown";
- int enumValue = (int) integrity;
- var rsi = new SpriteSpecifier.Rsi(new ResPath($"/Textures/_Shitmed/Interface/Targeting/Status/{enumName.ToLowerInvariant()}.rsi"), $"{enumName.ToLowerInvariant()}_{enumValue}");
- // Shitcode with love from Russia :)
- if (!_spriteSystem.TryGetLayer(_spriteViewEntity, layer, out _, false))
- _spriteSystem.AddTextureLayer(_spriteViewEntity, _spriteSystem.Frame0(rsi));
- else
- _spriteSystem.LayerSetTexture(_spriteViewEntity, layer, _spriteSystem.Frame0(rsi));
- _spriteSystem.LayerSetScale(_spriteViewEntity, layer, new Vector2(3f, 3f));
- layer++;
- }
- return _spriteViewEntity;
- }
- // Shitmed Change End
+ public void Populate(HealthAnalyzerScannedUserMessage msg)
+ {
+ HealthAnalyzer.Populate(msg.State);
}
}
diff --git a/Content.Client/Info/InfoSection.xaml.cs b/Content.Client/Info/InfoSection.xaml.cs
index 9e10a4d7b4..95a74c72c7 100644
--- a/Content.Client/Info/InfoSection.xaml.cs
+++ b/Content.Client/Info/InfoSection.xaml.cs
@@ -18,7 +18,7 @@ public sealed partial class InfoSection : BoxContainer
{
TitleLabel.Text = title;
if (markup)
- Content.SetMessage(FormattedMessage.FromMarkupOrThrow(text.Trim()));
+ Content.SetMessage(FormattedMessage.FromMarkupOrThrow(text.Trim()), tagsAllowed: null);
else
Content.SetMessage(text);
}
diff --git a/Content.Client/Info/ServerInfo.cs b/Content.Client/Info/ServerInfo.cs
index 901fc91337..a28a3d4a6e 100644
--- a/Content.Client/Info/ServerInfo.cs
+++ b/Content.Client/Info/ServerInfo.cs
@@ -24,7 +24,7 @@ namespace Content.Client.Info
}
public void SetInfoBlob(string markup)
{
- _richTextLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(markup));
+ _richTextLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(markup), tagsAllowed: null);
}
}
}
diff --git a/Content.Client/Kitchen/EntitySystems/ReagentGrinderSystem.cs b/Content.Client/Kitchen/EntitySystems/ReagentGrinderSystem.cs
new file mode 100644
index 0000000000..0aaa8ba8d8
--- /dev/null
+++ b/Content.Client/Kitchen/EntitySystems/ReagentGrinderSystem.cs
@@ -0,0 +1,7 @@
+using Content.Shared.Kitchen.EntitySystems;
+using JetBrains.Annotations;
+
+namespace Content.Client.Kitchen.EntitySystems;
+
+[UsedImplicitly]
+public sealed class ReagentGrinderSystem : SharedReagentGrinderSystem;
diff --git a/Content.Client/Launcher/LauncherConnecting.cs b/Content.Client/Launcher/LauncherConnecting.cs
index 33d31cc52d..9b9c472781 100644
--- a/Content.Client/Launcher/LauncherConnecting.cs
+++ b/Content.Client/Launcher/LauncherConnecting.cs
@@ -20,8 +20,10 @@ namespace Content.Client.Launcher
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IClipboardManager _clipboard = default!;
+ [Dependency] private readonly ILogManager _logManager = default!;
private LauncherConnectingGui? _control;
+ private ISawmill _sawmill = default!;
private Page _currentPage;
private string? _connectFailReason;
@@ -61,6 +63,8 @@ namespace Content.Client.Launcher
{
_control = new LauncherConnectingGui(this, _random, _prototypeManager, _cfg, _clipboard);
+ _sawmill = _logManager.GetSawmill("launcher-ui");
+
_userInterfaceManager.StateRoot.AddChild(_control);
_clientNetManager.ConnectFailed += OnConnectFailed;
@@ -115,12 +119,12 @@ namespace Content.Client.Launcher
}
else
{
- Logger.InfoS("launcher-ui", $"Redial not possible, no Ss14Address");
+ _sawmill.Info($"Redial not possible, no Ss14Address");
}
}
catch (Exception ex)
{
- Logger.ErrorS("launcher-ui", $"Redial exception: {ex}");
+ _sawmill.Error($"Redial exception: {ex}");
}
return false;
}
diff --git a/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs
index 9b1e7d50f8..988ece124f 100644
--- a/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs
+++ b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs
@@ -1,6 +1,5 @@
using System.Numerics;
using Content.Client.UserInterface.Controls;
-using Prometheus;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
diff --git a/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs b/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs
index 84c53e2140..6278e1b2b5 100644
--- a/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs
+++ b/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs
@@ -1,12 +1,13 @@
using Content.Client.Message;
+using Content.Client.RichText;
using Content.Client.Stylesheets;
-using Content.Client.UserInterface.RichText; // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
+using Content.Client.UserInterface.RichText;
using Content.Shared.MassMedia.Systems;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
-using Robust.Client.UserInterface.RichText; // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
+using Robust.Client.UserInterface.RichText;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;
@@ -20,18 +21,6 @@ public sealed partial class ArticleEditorPanel : Control
private bool _preview;
- // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
- private static readonly Type[] AllowedTags =
- [
- typeof(BoldItalicTag),
- typeof(BoldTag),
- typeof(BulletTag),
- typeof(ColorTag),
- typeof(HeadingTag),
- typeof(ItalicTag),
- typeof(MonoTag),
- ];
-
public ArticleEditorPanel()
{
RobustXamlLoader.Load(this);
@@ -96,8 +85,8 @@ public sealed partial class ArticleEditorPanel : Control
TextEditPanel.Visible = !_preview;
PreviewPanel.Visible = _preview;
- var articleBody = Rope.Collapse(ContentField.TextRope); // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
- PreviewLabel.SetMessage(FormattedMessage.FromMarkupPermissive(articleBody), AllowedTags); // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
+ var articleBody = Rope.Collapse(ContentField.TextRope);
+ PreviewLabel.SetMessage(FormattedMessage.FromMarkupPermissive(articleBody), UserFormattableTags.BaseAllowedTags);
}
private void OnCancel(BaseButton.ButtonEventArgs eventArgs)
diff --git a/Content.Client/Medical/Cryogenics/BeakerBarChart.cs b/Content.Client/Medical/Cryogenics/BeakerBarChart.cs
new file mode 100644
index 0000000000..25301b5268
--- /dev/null
+++ b/Content.Client/Medical/Cryogenics/BeakerBarChart.cs
@@ -0,0 +1,285 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+// ReSharper disable CompareOfFloatsByEqualityOperator
+
+namespace Content.Client.Medical.Cryogenics;
+
+
+public sealed class BeakerBarChart : Control
+{
+ private sealed class Entry
+ {
+ public float WidthFraction; // This entry's width as a fraction of the chart's total width (between 0 and 1)
+ public float TargetAmount;
+ public string Uid; // This UID is used to track entries between frames, for animation.
+ public string? Tooltip;
+ public Color Color;
+ public Label Label;
+
+ public Entry(string uid, Label label)
+ {
+ Uid = uid;
+ Label = label;
+ }
+ }
+
+ public float Capacity = 50;
+
+ public Color NotchColor = new(1, 1, 1, 0.25f);
+ public Color BackgroundColor = new(0.1f, 0.1f, 0.1f);
+
+ public int MediumNotchInterval = 5;
+ public int BigNotchInterval = 10;
+
+ // When we have a very large beaker (i.e. bluespace beaker) we might need to increase the distance between notches.
+ // The distance between notches is increased by ScaleMultiplier when the distance between notches is less than
+ // MinSmallNotchScreenDistance in UI units.
+ public int MinSmallNotchScreenDistance = 2;
+ public int ScaleMultiplier = 10;
+
+ public float SmallNotchHeight = 0.1f;
+ public float MediumNotchHeight = 0.25f;
+ public float BigNotchHeight = 1f;
+
+ // We don't animate new entries until this control has been drawn at least once.
+ private bool _hasBeenDrawn = false;
+
+ // This is used to keep the segments of the chart in the same order as the SetEntry calls.
+ // For example: In update 1 we might get cryox, alox, bic (in that order), and in update 2 we get alox, cryox, bic.
+ // To keep the order of the entries the same as the order of the SetEntry calls, we let the old cryox entry
+ // disappear and create a new cryox entry behind the alox entry.
+ private int _nextUpdateableEntry = 0;
+
+ private readonly List _entries = new();
+
+
+ public BeakerBarChart()
+ {
+ MouseFilter = MouseFilterMode.Pass;
+ TooltipSupplier = SupplyTooltip;
+ }
+
+ public void Clear()
+ {
+ foreach (var entry in _entries)
+ {
+ entry.TargetAmount = 0;
+ }
+
+ _nextUpdateableEntry = 0;
+ }
+
+ ///
+ /// Either adds a new entry to the chart if the UID doesn't appear yet, or updates the amount of an existing entry.
+ ///
+ public void SetEntry(
+ string uid,
+ string label,
+ float amount,
+ Color color,
+ Color? textColor = null,
+ string? tooltip = null)
+ {
+ // If we can find an old entry we're allowed to update, update that one.
+ if (TryFindUpdateableEntry(uid, out var index))
+ {
+ _entries[index].TargetAmount = amount;
+ _entries[index].Tooltip = tooltip;
+ _entries[index].Label.Text = label;
+ _nextUpdateableEntry = index + 1;
+ return;
+ }
+
+ // Otherwise create a new entry.
+ if (amount <= 0)
+ return;
+
+ // If no text color is provided, use either white or black depending on how dark the background is.
+ textColor ??= (color.R + color.G + color.B < 1.5f ? Color.White : Color.Black);
+
+ var childLabel = new Label
+ {
+ Text = label,
+ ClipText = true,
+ FontColorOverride = textColor,
+ Margin = new Thickness(4, 0, 0, 0)
+ };
+ AddChild(childLabel);
+
+ _entries.Insert(
+ _nextUpdateableEntry,
+ new Entry(uid, childLabel)
+ {
+ WidthFraction = (_hasBeenDrawn ? 0 : amount / Capacity),
+ TargetAmount = amount,
+ Tooltip = tooltip,
+ Color = color
+ }
+ );
+
+ _nextUpdateableEntry += 1;
+ }
+
+ private bool TryFindUpdateableEntry(string uid, out int index)
+ {
+ for (int i = _nextUpdateableEntry; i < _entries.Count; i++)
+ {
+ if (_entries[i].Uid == uid)
+ {
+ index = i;
+ return true;
+ }
+ }
+
+ index = -1;
+ return false;
+ }
+
+ private IEnumerable<(Entry, float xMin, float xMax)> EntryRanges(float? pixelWidth = null)
+ {
+ float chartWidth = pixelWidth ?? PixelWidth;
+ var xStart = 0f;
+
+ foreach (var entry in _entries)
+ {
+ var entryWidth = entry.WidthFraction * chartWidth;
+ var xEnd = MathF.Min(xStart + entryWidth, chartWidth);
+
+ yield return (entry, xStart, xEnd);
+
+ xStart = xEnd;
+ }
+ }
+
+ private bool TryFindEntry(float x, [NotNullWhen(true)] out Entry? entry)
+ {
+ foreach (var (currentEntry, xMin, xMax) in EntryRanges())
+ {
+ if (xMin <= x && x < xMax)
+ {
+ entry = currentEntry;
+ return true;
+ }
+ }
+
+ entry = null;
+ return false;
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ // Tween the amounts to their target amounts.
+ const float tweenInverseHalfLife = 8; // Half life of tween is 1/n
+ var hasChanged = false;
+
+ foreach (var entry in _entries)
+ {
+ var targetWidthFraction = entry.TargetAmount / Capacity;
+
+ if (entry.WidthFraction == targetWidthFraction)
+ continue;
+
+ // Tween with lerp abuse interpolation
+ entry.WidthFraction = MathHelper.Lerp(
+ entry.WidthFraction,
+ targetWidthFraction,
+ MathHelper.Clamp01(tweenInverseHalfLife * args.DeltaSeconds)
+ );
+ hasChanged = true;
+
+ if (MathF.Abs(entry.WidthFraction - targetWidthFraction) < 0.0001f)
+ entry.WidthFraction = targetWidthFraction;
+ }
+
+ if (!hasChanged)
+ return;
+
+ InvalidateArrange();
+
+ // Remove old entries whose animations have finished.
+ foreach (var entry in _entries)
+ {
+ if (entry.WidthFraction == 0 && entry.TargetAmount == 0)
+ RemoveChild(entry.Label);
+ }
+
+ _entries.RemoveAll(entry => entry.WidthFraction == 0 && entry.TargetAmount == 0);
+ }
+
+ protected override void MouseMove(GUIMouseMoveEventArgs args)
+ {
+ HideTooltip();
+ }
+
+ protected override void Draw(DrawingHandleScreen handle)
+ {
+ handle.DrawRect(PixelSizeBox, BackgroundColor);
+
+ // Draw the entry backgrounds
+ foreach (var (entry, xMin, xMax) in EntryRanges())
+ {
+ if (xMin != xMax)
+ handle.DrawRect(new(xMin, 0, xMax, PixelHeight), entry.Color);
+ }
+
+ // Draw notches
+ var unitWidth = PixelWidth / Capacity;
+ var unitsPerNotch = 1;
+
+ while (unitWidth < MinSmallNotchScreenDistance)
+ {
+ // This is here for 1000u bluespace beakers. If the distance between small notches is so small that it would
+ // be very ugly, we reduce the amount of notches by ScaleMultiplier (currently a factor of 10).
+ // (I could use an analytical algorithm here, but it would be more difficult to read with pretty much no
+ // performance benefit, since it loops zero times normally and one time for the bluespace beaker)
+ unitWidth *= ScaleMultiplier;
+ unitsPerNotch *= ScaleMultiplier;
+ }
+
+ for (int i = 0; i <= Capacity / unitsPerNotch; i++)
+ {
+ var x = i * unitWidth;
+ var height = (i % BigNotchInterval == 0 ? BigNotchHeight :
+ i % MediumNotchInterval == 0 ? MediumNotchHeight :
+ SmallNotchHeight) * PixelHeight;
+ var start = new Vector2(x, PixelHeight);
+ var end = new Vector2(x, PixelHeight - height);
+ handle.DrawLine(start, end, NotchColor);
+ }
+
+ _hasBeenDrawn = true;
+ }
+
+ protected override Vector2 ArrangeOverride(Vector2 finalSize)
+ {
+ foreach (var (entry, xMin, xMax) in EntryRanges(finalSize.X))
+ {
+ entry.Label.Arrange(new((int)xMin, 0, (int)xMax, (int)finalSize.Y));
+ }
+
+ return finalSize;
+ }
+
+ private Control? SupplyTooltip(Control sender)
+ {
+ var globalMousePos = UserInterfaceManager.MousePositionScaled.Position;
+ var mousePos = globalMousePos - GlobalPosition;
+
+ if (!TryFindEntry(mousePos.X, out var entry) || entry.Tooltip == null)
+ return null;
+
+ var msg = new FormattedMessage();
+ msg.AddText(entry.Tooltip);
+
+ var tooltip = new Tooltip();
+ tooltip.SetMessage(msg);
+ return tooltip;
+ }
+}
diff --git a/Content.Client/Medical/Cryogenics/CryoPodBoundUserInterface.cs b/Content.Client/Medical/Cryogenics/CryoPodBoundUserInterface.cs
new file mode 100644
index 0000000000..5e64cea720
--- /dev/null
+++ b/Content.Client/Medical/Cryogenics/CryoPodBoundUserInterface.cs
@@ -0,0 +1,53 @@
+using Content.Shared.FixedPoint;
+using Content.Shared.Medical.Cryogenics;
+using JetBrains.Annotations;
+using Robust.Client.UserInterface;
+namespace Content.Client.Medical.Cryogenics;
+
+[UsedImplicitly]
+public sealed class CryoPodBoundUserInterface : BoundUserInterface
+{
+ private CryoPodWindow? _window;
+
+ public CryoPodBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+ _window = this.CreateWindowCenteredLeft();
+ _window.Title = EntMan.GetComponent(Owner).EntityName;
+ _window.OnEjectPatientPressed += EjectPatientPressed;
+ _window.OnEjectBeakerPressed += EjectBeakerPressed;
+ _window.OnInjectPressed += InjectPressed;
+ }
+
+ private void EjectPatientPressed()
+ {
+ var isLocked =
+ EntMan.TryGetComponent(Owner, out var cryoComp)
+ && cryoComp.Locked;
+
+ _window?.SetEjectErrorVisible(isLocked);
+ SendMessage(new CryoPodSimpleUiMessage(CryoPodSimpleUiMessage.MessageType.EjectPatient));
+ }
+
+ private void EjectBeakerPressed()
+ {
+ SendMessage(new CryoPodSimpleUiMessage(CryoPodSimpleUiMessage.MessageType.EjectBeaker));
+ }
+
+ private void InjectPressed(FixedPoint2 transferAmount)
+ {
+ SendMessage(new CryoPodInjectUiMessage(transferAmount));
+ }
+
+ protected override void ReceiveMessage(BoundUserInterfaceMessage message)
+ {
+ if (_window != null && message is CryoPodUserMessage cryoMsg)
+ {
+ _window.Populate(cryoMsg);
+ }
+ }
+}
diff --git a/Content.Client/Medical/Cryogenics/CryoPodSystem.cs b/Content.Client/Medical/Cryogenics/CryoPodSystem.cs
index c1cbfc573e..63c95a63d8 100644
--- a/Content.Client/Medical/Cryogenics/CryoPodSystem.cs
+++ b/Content.Client/Medical/Cryogenics/CryoPodSystem.cs
@@ -6,7 +6,6 @@ namespace Content.Client.Medical.Cryogenics;
public sealed class CryoPodSystem : SharedCryoPodSystem
{
- [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
@@ -46,8 +45,8 @@ public sealed class CryoPodSystem : SharedCryoPodSystem
return;
}
- if (!_appearance.TryGetData(uid, CryoPodVisuals.ContainsEntity, out var isOpen, args.Component)
- || !_appearance.TryGetData(uid, CryoPodVisuals.IsOn, out var isOn, args.Component))
+ if (!Appearance.TryGetData(uid, CryoPodVisuals.ContainsEntity, out var isOpen, args.Component)
+ || !Appearance.TryGetData(uid, CryoPodVisuals.IsOn, out var isOn, args.Component))
{
return;
}
@@ -64,6 +63,11 @@ public sealed class CryoPodSystem : SharedCryoPodSystem
_sprite.LayerSetVisible((uid, args.Sprite), CryoPodVisualLayers.Cover, true);
}
}
+
+ protected override void UpdateUi(Entity cryoPod)
+ {
+ // Atmos and health scanner aren't predicted currently...
+ }
}
public enum CryoPodVisualLayers : byte
diff --git a/Content.Client/Medical/Cryogenics/CryoPodWindow.xaml b/Content.Client/Medical/Cryogenics/CryoPodWindow.xaml
new file mode 100644
index 0000000000..9bea37d582
--- /dev/null
+++ b/Content.Client/Medical/Cryogenics/CryoPodWindow.xaml
@@ -0,0 +1,232 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Medical/Cryogenics/CryoPodWindow.xaml.cs b/Content.Client/Medical/Cryogenics/CryoPodWindow.xaml.cs
new file mode 100644
index 0000000000..ad5ab9d9ea
--- /dev/null
+++ b/Content.Client/Medical/Cryogenics/CryoPodWindow.xaml.cs
@@ -0,0 +1,260 @@
+using System.Linq;
+using System.Numerics;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Atmos;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.Damage.Components;
+using Content.Shared.EntityConditions.Conditions;
+using Content.Shared.FixedPoint;
+using Content.Shared.Medical.Cryogenics;
+using Content.Shared.Temperature;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+namespace Content.Client.Medical.Cryogenics;
+
+[GenerateTypedNameReferences]
+public sealed partial class CryoPodWindow : FancyWindow
+{
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+
+ public event Action? OnEjectPatientPressed;
+ public event Action? OnEjectBeakerPressed;
+ public event Action? OnInjectPressed;
+
+ public CryoPodWindow()
+ {
+ IoCManager.InjectDependencies(this);
+ RobustXamlLoader.Load(this);
+ EjectPatientButton.OnPressed += _ => OnEjectPatientPressed?.Invoke();
+ EjectBeakerButton.OnPressed += _ => OnEjectBeakerPressed?.Invoke();
+ Inject1.OnPressed += _ => OnInjectPressed?.Invoke(1);
+ Inject5.OnPressed += _ => OnInjectPressed?.Invoke(5);
+ Inject10.OnPressed += _ => OnInjectPressed?.Invoke(10);
+ Inject20.OnPressed += _ => OnInjectPressed?.Invoke(20);
+ }
+
+ public void Populate(CryoPodUserMessage msg)
+ {
+ // Loading screen
+ if (LoadingPlaceHolder.Visible)
+ {
+ LoadingPlaceHolder.Visible = false;
+ Sections.Visible = true;
+ }
+
+ // Atmosphere
+ var hasCorrectPressure = (msg.GasMix.Pressure > Atmospherics.WarningLowPressure);
+ var hasGas = (msg.GasMix.Pressure > Atmospherics.GasMinMoles);
+ var showsPressureWarning = !hasCorrectPressure;
+ LowPressureWarning.Visible = showsPressureWarning;
+ Pressure.Text = Loc.GetString("gas-analyzer-window-pressure-val-text",
+ ("pressure", $"{msg.GasMix.Pressure:0.00}"));
+ Temperature.Text = Loc.GetString("generic-not-available-shorthand");
+
+ if (hasGas)
+ {
+ var celsius = TemperatureHelpers.KelvinToCelsius(msg.GasMix.Temperature);
+ Temperature.Text = Loc.GetString("gas-analyzer-window-temperature-val-text",
+ ("tempK", $"{msg.GasMix.Temperature:0.0}"),
+ ("tempC", $"{celsius:0.0}"));
+ }
+
+ // Gas mix segmented bar chart
+ GasMixChart.Clear();
+ GasMixChart.Visible = hasGas;
+
+ if (msg.GasMix.Gases != null)
+ {
+ var totalGasAmount = msg.GasMix.Gases.Sum(gas => gas.Amount);
+
+ foreach (var gas in msg.GasMix.Gases)
+ {
+ var color = Color.FromHex($"#{gas.Color}", Color.White);
+ var percent = gas.Amount / totalGasAmount * 100;
+ var localizedName = Loc.GetString(gas.Name);
+ var tooltip = Loc.GetString("gas-analyzer-window-molarity-percentage-text",
+ ("gasName", localizedName),
+ ("amount", $"{gas.Amount:0.##}"),
+ ("percentage", $"{percent:0.#}"));
+ GasMixChart.AddEntry(gas.Amount, color, tooltip: tooltip);
+ }
+ }
+
+ // Health analyzer
+ var maybePatient = _entityManager.GetEntity(msg.Health.TargetEntity);
+ var hasPatient = msg.Health.TargetEntity.HasValue;
+ var hasDamage = (hasPatient
+ && _entityManager.TryGetComponent(maybePatient, out DamageableComponent? damageable)
+ && damageable.TotalDamage > 0);
+
+ NoDamageText.Visible = (hasPatient && !hasDamage);
+ HealthSection.Visible = hasPatient;
+ EjectPatientButton.Disabled = !hasPatient;
+
+ if (hasPatient)
+ HealthAnalyzer.Populate(msg.Health);
+
+ // Reagents
+ float? lowestTempRequirement = null;
+ ReagentId? lowestTempReagent = null;
+ var totalBeakerCapacity = msg.BeakerCapacity ?? 0;
+ var availableQuantity = new FixedPoint2();
+ var injectingQuantity =
+ msg.Injecting?.Aggregate(new FixedPoint2(), (sum, r) => sum + r.Quantity)
+ ?? new FixedPoint2(); // Either the sum of the reagent quantities in `msg.Injecting` or zero.
+ var hasBeaker = (msg.Beaker != null);
+
+ ChemicalsChart.Clear();
+ ChemicalsChart.Capacity = (totalBeakerCapacity < 1 ? 50 : (int)totalBeakerCapacity);
+
+ var chartMaxChemsQuantity = ChemicalsChart.Capacity - injectingQuantity; // Ensure space for injection buffer
+
+ if (hasBeaker)
+ {
+ foreach (var (reagent, quantity) in msg.Beaker!)
+ {
+ availableQuantity += quantity;
+
+ // Make sure we don't add too many chemicals to the chart, so that there's still enough space to
+ // visualize the injection buffer.
+ var chemsQuantityOvershoot = FixedPoint2.Max(0, availableQuantity - chartMaxChemsQuantity);
+ var chartQuantity = FixedPoint2.Max(0, quantity - chemsQuantityOvershoot);
+
+ var reagentProto = _prototypeManager.Index(reagent.Prototype);
+ ChemicalsChart.SetEntry(
+ reagent.Prototype,
+ reagentProto.LocalizedName,
+ (float)chartQuantity,
+ reagentProto.SubstanceColor,
+ tooltip: $"{quantity}u {reagentProto.LocalizedName}"
+ );
+
+ var temp = TryFindMaxTemperatureRequirement(reagent);
+ if (lowestTempRequirement == null
+ || temp < lowestTempRequirement)
+ {
+ lowestTempRequirement = temp;
+ lowestTempReagent = reagent;
+ }
+ }
+ }
+
+ if (injectingQuantity != 0)
+ {
+ var injectingText = (injectingQuantity > 1 ? $"{injectingQuantity}u" : "");
+ ChemicalsChart.SetEntry(
+ "injecting",
+ injectingText,
+ (float)injectingQuantity,
+ Color.MediumSpringGreen,
+ tooltip: Loc.GetString("cryo-pod-window-chems-injecting-tooltip",
+ ("quantity", injectingQuantity))
+ );
+ }
+
+ var isBeakerEmpty = (injectingQuantity + availableQuantity == 0);
+ var isChemicalsChartVisible = (hasBeaker || injectingQuantity != 0);
+ NoBeakerText.Visible = !isChemicalsChartVisible;
+ ChemicalsChart.Visible = isChemicalsChartVisible;
+ Inject1.Disabled = (!hasPatient || availableQuantity < 0.1f);
+ Inject5.Disabled = (!hasPatient || availableQuantity <= 1);
+ Inject10.Disabled = (!hasPatient || availableQuantity <= 5);
+ Inject20.Disabled = (!hasPatient || availableQuantity <= 10);
+ EjectBeakerButton.Disabled = !hasBeaker;
+
+ // Temperature warning
+ var hasCorrectTemperature = (lowestTempRequirement == null || lowestTempRequirement > msg.GasMix.Temperature);
+ var showsTemperatureWarning = (!showsPressureWarning && !hasCorrectTemperature);
+
+ HighTemperatureWarning.Visible = showsTemperatureWarning;
+
+ if (showsTemperatureWarning)
+ {
+ var reagentName = _prototypeManager.Index(lowestTempReagent!.Value.Prototype)
+ .LocalizedName;
+ HighTemperatureWarningText.Text = Loc.GetString("cryo-pod-window-high-temperature-warning",
+ ("reagent", reagentName),
+ ("temperature", lowestTempRequirement!));
+ }
+
+ // Status checklist
+ const float fallbackTemperatureRequirement = 213;
+ var hasTemperatureCheck = (hasGas && hasCorrectTemperature
+ && (lowestTempRequirement != null || msg.GasMix.Temperature < fallbackTemperatureRequirement));
+ var hasChemicals = (hasBeaker && !isBeakerEmpty);
+
+ UpdateChecklistItem(PressureCheck, Loc.GetString("cryo-pod-window-checklist-pressure"), hasCorrectPressure);
+ UpdateChecklistItem(ChemicalsCheck, Loc.GetString("cryo-pod-window-checklist-chemicals"), hasChemicals);
+ UpdateChecklistItem(TemperatureCheck, Loc.GetString("cryo-pod-window-checklist-temperature"), hasTemperatureCheck);
+
+ var isReady = (hasCorrectPressure && hasChemicals && hasTemperatureCheck);
+ var isCooling = (lowestTempRequirement != null && hasPatient
+ && msg.Health.Temperature > lowestTempRequirement);
+ var isInjecting = (injectingQuantity > 0);
+ StatusLabel.Text = (!isReady ? Loc.GetString("cryo-pod-window-status-not-ready") :
+ isCooling ? Loc.GetString("cryo-pod-window-status-cooling") :
+ isInjecting ? Loc.GetString("cryo-pod-window-status-injecting") :
+ hasPatient ? Loc.GetString("cryo-pod-window-status-ready-to-inject") :
+ Loc.GetString("cryo-pod-window-status-ready-for-patient"));
+ StatusLabel.FontColorOverride = (isReady ? Color.DeepSkyBlue : Color.Orange);
+ }
+
+ private void UpdateChecklistItem(Label label, string text, bool isOkay)
+ {
+ label.Text = (isOkay ? text : Loc.GetString("cryo-pod-window-checklist-fail", ("item", text)));
+ label.FontColorOverride = (isOkay ? null : Color.Orange);
+ }
+
+ private float? TryFindMaxTemperatureRequirement(ReagentId reagent)
+ {
+ var reagentProto = _prototypeManager.Index(reagent.Prototype);
+ if (reagentProto.Metabolisms == null)
+ return null;
+
+ float? result = null;
+
+ foreach (var (_, metabolism) in reagentProto.Metabolisms)
+ {
+ foreach (var effect in metabolism.Effects)
+ {
+ if (effect.Conditions == null)
+ continue;
+
+ foreach (var condition in effect.Conditions)
+ {
+ // If there are multiple temperature conditions in the same reagent (which could hypothetically
+ // happen, although it currently doesn't), we return the lowest max temperature.
+ if (condition is TemperatureCondition tempCondition
+ && float.IsFinite(tempCondition.Max)
+ && (result == null || tempCondition.Max < result))
+ {
+ result = tempCondition.Max;
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+
+ public void SetEjectErrorVisible(bool isVisible)
+ {
+ EjectError.Visible = isVisible;
+ }
+
+ protected override Vector2 MeasureOverride(Vector2 availableSize)
+ {
+ const float antiJiggleSlackSpace = 80;
+ var oldSize = DesiredSize;
+ var newSize = base.MeasureOverride(availableSize);
+
+ // Reduce how often the height of the window jiggles
+ if (newSize.Y < oldSize.Y && newSize.Y + antiJiggleSlackSpace > oldSize.Y)
+ newSize.Y = oldSize.Y;
+
+ return newSize;
+ }
+}
diff --git a/Content.Client/Medical/DefibrillatorSystem.cs b/Content.Client/Medical/DefibrillatorSystem.cs
new file mode 100644
index 0000000000..834bcbcc5b
--- /dev/null
+++ b/Content.Client/Medical/DefibrillatorSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared.Medical;
+
+namespace Content.Client.Medical;
+
+public sealed class DefibrillatorSystem : SharedDefibrillatorSystem;
diff --git a/Content.Client/NetworkConfigurator/NetworkConfiguratorLinkOverlay.cs b/Content.Client/NetworkConfigurator/NetworkConfiguratorLinkOverlay.cs
index 19da5aa959..cd57c3b5fb 100644
--- a/Content.Client/NetworkConfigurator/NetworkConfiguratorLinkOverlay.cs
+++ b/Content.Client/NetworkConfigurator/NetworkConfiguratorLinkOverlay.cs
@@ -41,9 +41,9 @@ public sealed class NetworkConfiguratorLinkOverlay : Overlay
if (!Colors.TryGetValue(uid, out var color))
{
color = new Color(
- _random.Next(0, 255),
- _random.Next(0, 255),
- _random.Next(0, 255));
+ _random.NextByte(0, 255),
+ _random.NextByte(0, 255),
+ _random.NextByte(0, 255));
Colors.Add(uid, color);
}
diff --git a/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml b/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml
index 53e6e29cee..dfa9a89b77 100644
--- a/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml
+++ b/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml
@@ -11,9 +11,6 @@
-
-
diff --git a/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml.cs b/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml.cs
index 1b6ef25f87..f6e55c4ddf 100644
--- a/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml.cs
+++ b/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml.cs
@@ -51,8 +51,6 @@ public sealed partial class AdminOptionsTab : Control
playerTabSymbolSettings.Add(new OptionDropDownCVar.ValueOption(setting.ToString()!, Loc.GetString($"ui-options-admin-player-tab-symbol-setting-{setting.ToString()!.ToLower()}")));
}
- Control.AddOptionColorSlider(CCVars.AdminLogsHighlightColor, ColorSliderLogsHighlight);
-
Control.AddOptionDropDown(CCVars.AdminPlayerTabSymbolSetting, DropDownPlayerTabSymbolSetting, playerTabSymbolSettings);
Control.AddOptionDropDown(CCVars.AdminPlayerTabRoleSetting, DropDownPlayerTabRoleSetting, playerTabRoleSettings);
Control.AddOptionDropDown(CCVars.AdminPlayerTabColorSetting, DropDownPlayerTabColorSetting, playerTabColorSettings);
diff --git a/Content.Client/Overlays/ShowHealthBarsSystem.cs b/Content.Client/Overlays/ShowHealthBarsSystem.cs
index 9fefe93094..a25e08d1fe 100644
--- a/Content.Client/Overlays/ShowHealthBarsSystem.cs
+++ b/Content.Client/Overlays/ShowHealthBarsSystem.cs
@@ -1,8 +1,6 @@
using Content.Shared.Inventory.Events;
using Content.Shared.Overlays;
using Robust.Client.Graphics;
-using System.Linq;
-using Robust.Client.Player;
using Robust.Shared.Prototypes;
namespace Content.Client.Overlays;
@@ -35,6 +33,9 @@ public sealed class ShowHealthBarsSystem : EquipmentHudSystem x.DamageContainers))
+ DamageContainers.Clear();
+ foreach (var comp in component.Components)
{
- DamageContainers.Add(damageContainerId);
+ foreach (var damageContainerId in comp.DamageContainers)
+ {
+ DamageContainers.Add(damageContainerId);
+ }
}
}
diff --git a/Content.Client/Paper/UI/PaperWindow.xaml.cs b/Content.Client/Paper/UI/PaperWindow.xaml.cs
index 0570fde00b..ea1e5e296e 100644
--- a/Content.Client/Paper/UI/PaperWindow.xaml.cs
+++ b/Content.Client/Paper/UI/PaperWindow.xaml.cs
@@ -1,4 +1,5 @@
using System.Numerics;
+using Content.Client.RichText;
using Content.Shared.Paper;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
@@ -38,16 +39,6 @@ namespace Content.Client.Paper.UI
// we're able to resize this UI or not. Default to everything enabled:
private DragMode _allowedResizeModes = ~DragMode.None;
- private readonly Type[] _allowedTags = new Type[] {
- typeof(BoldItalicTag),
- typeof(BoldTag),
- typeof(BulletTag),
- typeof(ColorTag),
- typeof(HeadingTag),
- typeof(ItalicTag),
- typeof(MonoTag)
- };
-
public event Action? OnSaved;
public event Action? Typing; // DeltaV
public event Action? SubmitPressed; // DeltaV
@@ -284,7 +275,7 @@ namespace Content.Client.Paper.UI
{
msg.AddMarkupPermissive("\r\n");
}
- WrittenTextLabel.SetMessage(msg, _allowedTags, DefaultTextColor);
+ WrittenTextLabel.SetMessage(msg, UserFormattableTags.BaseAllowedTags, DefaultTextColor);
WrittenTextLabel.Visible = !isEditing && state.Text.Length > 0;
BlankPaperIndicator.Visible = !isEditing && state.Text.Length == 0;
diff --git a/Content.Client/Parallax/Managers/ParallaxManager.cs b/Content.Client/Parallax/Managers/ParallaxManager.cs
index bc7d7d60d6..bd46288ebf 100644
--- a/Content.Client/Parallax/Managers/ParallaxManager.cs
+++ b/Content.Client/Parallax/Managers/ParallaxManager.cs
@@ -98,10 +98,13 @@ public sealed class ParallaxManager : IParallaxManager
}
else
{
- layers = await Task.WhenAll(
+ // Explicitly allocate params array to avoid sandbox violation since C# 14.
+ var tasks = new[]
+ {
LoadParallaxLayers(parallaxPrototype.Layers, loadedLayers, cancel),
- LoadParallaxLayers(parallaxPrototype.LayersLQ, loadedLayers, cancel)
- );
+ LoadParallaxLayers(parallaxPrototype.LayersLQ, loadedLayers, cancel),
+ };
+ layers = await Task.WhenAll(tasks);
}
cancel.ThrowIfCancellationRequested();
diff --git a/Content.Client/Pinpointer/UI/StationMapBoundUserInterface.cs b/Content.Client/Pinpointer/UI/StationMapBoundUserInterface.cs
index 3d1eb1723c..49b383a7d7 100644
--- a/Content.Client/Pinpointer/UI/StationMapBoundUserInterface.cs
+++ b/Content.Client/Pinpointer/UI/StationMapBoundUserInterface.cs
@@ -17,7 +17,11 @@ public sealed class StationMapBoundUserInterface : BoundUserInterface
base.Open();
EntityUid? gridUid = null;
- if (EntMan.TryGetComponent(Owner, out var xform))
+ if (EntMan.TryGetComponent(Owner, out var comp) && comp.TargetGrid != null)
+ {
+ gridUid = comp.TargetGrid;
+ }
+ else if (EntMan.TryGetComponent(Owner, out var xform))
{
gridUid = xform.GridUid;
}
@@ -30,8 +34,8 @@ public sealed class StationMapBoundUserInterface : BoundUserInterface
{
stationName = gridMetaData.EntityName;
}
-
- if (EntMan.TryGetComponent(Owner, out var comp) && comp.ShowLocation)
+
+ if (comp != null && comp.ShowLocation)
_window.Set(stationName, gridUid, Owner);
else
_window.Set(stationName, gridUid, null);
diff --git a/Content.Client/Power/Visualizers/PowerDeviceVisuals.cs b/Content.Client/Power/Visualizers/PowerDeviceVisuals.cs
index 5cc86d203d..057dabae5d 100644
--- a/Content.Client/Power/Visualizers/PowerDeviceVisuals.cs
+++ b/Content.Client/Power/Visualizers/PowerDeviceVisuals.cs
@@ -3,5 +3,6 @@ namespace Content.Client.Power;
/// Remains in use by portable scrubbers and lathes.
public enum PowerDeviceVisualLayers : byte
{
- Powered
+ Powered,
+ Charging
}
diff --git a/Content.Client/Remotes/UI/DoorRemoteStatusControl.cs b/Content.Client/Remotes/UI/DoorRemoteStatusControl.cs
index e96cce5b25..9db604ff80 100644
--- a/Content.Client/Remotes/UI/DoorRemoteStatusControl.cs
+++ b/Content.Client/Remotes/UI/DoorRemoteStatusControl.cs
@@ -35,6 +35,7 @@ public sealed class DoorRemoteStatusControl(Entity ent) : C
OperatingMode.OpenClose => "door-remote-open-close-text",
OperatingMode.ToggleBolts => "door-remote-toggle-bolt-text",
OperatingMode.ToggleEmergencyAccess => "door-remote-emergency-access-text",
+ OperatingMode.ToggleOvercharge => "door-remote-toggle-eletrify-text",
_ => "door-remote-invalid-text"
});
diff --git a/Content.Client/RichText/UserFormattableTags.cs b/Content.Client/RichText/UserFormattableTags.cs
new file mode 100644
index 0000000000..09be4fa2e7
--- /dev/null
+++ b/Content.Client/RichText/UserFormattableTags.cs
@@ -0,0 +1,25 @@
+using Content.Client.UserInterface.RichText;
+using Robust.Client.UserInterface.RichText;
+
+namespace Content.Client.RichText;
+
+///
+/// Contains rules for what markup tags are allowed to be used by players.
+///
+public static class UserFormattableTags
+{
+ ///
+ /// The basic set of "rich text" formatting tags that shouldn't cause any issues.
+ /// Limit user rich text to these by default.
+ ///
+ public static readonly Type[] BaseAllowedTags =
+ [
+ typeof(BoldItalicTag),
+ typeof(BoldTag),
+ typeof(BulletTag),
+ typeof(ColorTag),
+ typeof(HeadingTag),
+ typeof(ItalicTag),
+ typeof(MonoTag),
+ ];
+}
diff --git a/Content.Client/Rootable/RootableSystem.cs b/Content.Client/Rootable/RootableSystem.cs
deleted file mode 100644
index 33e68ae594..0000000000
--- a/Content.Client/Rootable/RootableSystem.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-using Content.Shared.Rootable;
-
-namespace Content.Client.Rootable;
-
-public sealed class RootableSystem : SharedRootableSystem;
diff --git a/Content.Client/SSDIndicator/SSDIndicatorSystem.cs b/Content.Client/SSDIndicator/SSDIndicatorSystem.cs
index 3880596626..9b0060d956 100644
--- a/Content.Client/SSDIndicator/SSDIndicatorSystem.cs
+++ b/Content.Client/SSDIndicator/SSDIndicatorSystem.cs
@@ -33,8 +33,7 @@ public sealed class SSDIndicatorSystem : EntitySystem
_cfg.GetCVar(CCVars.ICShowSSDIndicator) &&
!_mobState.IsDead(uid) &&
!HasComp(uid) &&
- TryComp(uid, out var mindContainer) &&
- mindContainer.ShowExamineInfo)
+ HasComp(uid))
{
// Begin DeltaV Addition
var ev = new ShowSSDIndicatorEvent();
diff --git a/Content.Client/Security/Ui/GenpopLockerBoundUserInterface.cs b/Content.Client/Security/Ui/GenpopLockerBoundUserInterface.cs
index a546fa6fc6..63276e1a90 100644
--- a/Content.Client/Security/Ui/GenpopLockerBoundUserInterface.cs
+++ b/Content.Client/Security/Ui/GenpopLockerBoundUserInterface.cs
@@ -16,7 +16,7 @@ public sealed class GenpopLockerBoundUserInterface(EntityUid owner, Enum uiKey)
_menu.OnConfigurationComplete += (name, time, crime) =>
{
- SendMessage(new GenpopLockerIdConfiguredMessage(name, time, crime));
+ SendPredictedMessage(new GenpopLockerIdConfiguredMessage(name, time, crime));
Close();
};
diff --git a/Content.Client/Shuttles/BUI/IFFConsoleBoundUserInterface.cs b/Content.Client/Shuttles/BUI/IFFConsoleBoundUserInterface.cs
index 8d84abed8a..704307f06b 100644
--- a/Content.Client/Shuttles/BUI/IFFConsoleBoundUserInterface.cs
+++ b/Content.Client/Shuttles/BUI/IFFConsoleBoundUserInterface.cs
@@ -23,7 +23,6 @@ public sealed class IFFConsoleBoundUserInterface : BoundUserInterface
_window = this.CreateWindowCenteredLeft();
_window.ShowIFF += SendIFFMessage;
- _window.ShowVessel += SendVesselMessage;
}
protected override void UpdateState(BoundUserInterfaceState state)
@@ -44,14 +43,6 @@ public sealed class IFFConsoleBoundUserInterface : BoundUserInterface
});
}
- private void SendVesselMessage(bool obj)
- {
- SendMessage(new IFFShowVesselMessage()
- {
- Show = obj,
- });
- }
-
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
diff --git a/Content.Client/Shuttles/UI/IFFConsoleWindow.xaml b/Content.Client/Shuttles/UI/IFFConsoleWindow.xaml
index dab11a7b62..90684889a6 100644
--- a/Content.Client/Shuttles/UI/IFFConsoleWindow.xaml
+++ b/Content.Client/Shuttles/UI/IFFConsoleWindow.xaml
@@ -9,12 +9,6 @@
-
-
-
-
-
-
diff --git a/Content.Client/Shuttles/UI/IFFConsoleWindow.xaml.cs b/Content.Client/Shuttles/UI/IFFConsoleWindow.xaml.cs
index f14ef22c3c..d61486d2d8 100644
--- a/Content.Client/Shuttles/UI/IFFConsoleWindow.xaml.cs
+++ b/Content.Client/Shuttles/UI/IFFConsoleWindow.xaml.cs
@@ -13,9 +13,7 @@ public sealed partial class IFFConsoleWindow : FancyWindow,
IComputerWindow
{
private readonly ButtonGroup _showIFFButtonGroup = new();
- private readonly ButtonGroup _showVesselButtonGroup = new();
public event Action? ShowIFF;
- public event Action? ShowVessel;
public IFFConsoleWindow()
{
@@ -24,11 +22,6 @@ public sealed partial class IFFConsoleWindow : FancyWindow,
ShowIFFOnButton.Group = _showIFFButtonGroup;
ShowIFFOnButton.OnPressed += args => ShowIFFPressed(true);
ShowIFFOffButton.OnPressed += args => ShowIFFPressed(false);
-
- ShowVesselOffButton.Group = _showVesselButtonGroup;
- ShowVesselOnButton.Group = _showVesselButtonGroup;
- ShowVesselOnButton.OnPressed += args => ShowVesselPressed(true);
- ShowVesselOffButton.OnPressed += args => ShowVesselPressed(false);
}
private void ShowIFFPressed(bool pressed)
@@ -36,19 +29,14 @@ public sealed partial class IFFConsoleWindow : FancyWindow,
ShowIFF?.Invoke(pressed);
}
- private void ShowVesselPressed(bool pressed)
- {
- ShowVessel?.Invoke(pressed);
- }
-
public void UpdateState(IFFConsoleBoundUserInterfaceState state)
{
- if ((state.AllowedFlags & IFFFlags.HideLabel) != 0x0)
+ if ((state.AllowedFlags & IFFFlags.HideLabel) != 0x0 || (state.AllowedFlags & IFFFlags.Hide) != 0x0)
{
ShowIFFOffButton.Disabled = false;
ShowIFFOnButton.Disabled = false;
- if ((state.Flags & IFFFlags.HideLabel) != 0x0)
+ if ((state.Flags & IFFFlags.HideLabel) != 0x0 || (state.Flags & IFFFlags.Hide) != 0x0)
{
ShowIFFOffButton.Pressed = true;
}
@@ -62,25 +50,5 @@ public sealed partial class IFFConsoleWindow : FancyWindow,
ShowIFFOffButton.Disabled = true;
ShowIFFOnButton.Disabled = true;
}
-
- if ((state.AllowedFlags & IFFFlags.Hide) != 0x0)
- {
- ShowVesselOffButton.Disabled = false;
- ShowVesselOnButton.Disabled = false;
-
- if ((state.Flags & IFFFlags.Hide) != 0x0)
- {
- ShowVesselOffButton.Pressed = true;
- }
- else
- {
- ShowVesselOnButton.Pressed = true;
- }
- }
- else
- {
- ShowVesselOffButton.Disabled = true;
- ShowVesselOnButton.Disabled = true;
- }
}
}
diff --git a/Content.Client/Shuttles/UI/MapScreen.xaml.cs b/Content.Client/Shuttles/UI/MapScreen.xaml.cs
index 72ad3c28b1..ff55c0b462 100644
--- a/Content.Client/Shuttles/UI/MapScreen.xaml.cs
+++ b/Content.Client/Shuttles/UI/MapScreen.xaml.cs
@@ -153,7 +153,7 @@ public sealed partial class MapScreen : BoxContainer
break;
}
- if (IsFTLBlocked())
+ if (IsPingBlocked())
{
MapRebuildButton.Disabled = true;
ClearMapObjects();
@@ -408,9 +408,21 @@ public sealed partial class MapScreen : BoxContainer
}
}
+ ///
+ /// Returns true if we shouldn't be able to select the Scan for Objects button.
+ ///
+ private bool IsPingBlocked()
+ {
+ return _state switch
+ {
+ FTLState.Available or FTLState.Cooldown => false,
+ _ => true,
+ };
+ }
+
private void OnMapObjectPress(IMapObject mapObject)
{
- if (IsFTLBlocked())
+ if (IsPingBlocked())
return;
var coordinates = _shuttles.GetMapCoordinates(mapObject);
@@ -506,7 +518,7 @@ public sealed partial class MapScreen : BoxContainer
BumpMapDequeue();
}
- if (!IsFTLBlocked() && _nextPing < curTime)
+ if (!IsPingBlocked() && _nextPing < curTime)
{
MapRebuildButton.Disabled = false;
}
diff --git a/Content.Client/Shuttles/UI/ShuttleMapControl.xaml.cs b/Content.Client/Shuttles/UI/ShuttleMapControl.xaml.cs
index 70ac02bda9..daf2622d81 100644
--- a/Content.Client/Shuttles/UI/ShuttleMapControl.xaml.cs
+++ b/Content.Client/Shuttles/UI/ShuttleMapControl.xaml.cs
@@ -124,7 +124,7 @@ public sealed partial class ShuttleMapControl : BaseShuttleControl
else
{
// We'll send the "adjusted" position and server will adjust it back when relevant.
- var mapCoords = new MapCoordinates(InverseMapPosition(args.RelativePosition), ViewingMap);
+ var mapCoords = new MapCoordinates(InverseMapPosition(args.RelativePixelPosition), ViewingMap);
RequestFTL?.Invoke(mapCoords, _ftlAngle);
}
}
@@ -180,7 +180,7 @@ public sealed partial class ShuttleMapControl : BaseShuttleControl
// Remove offset so we can floor.
var botLeft = new Vector2(0f, 0f);
- var topRight = botLeft + Size;
+ var topRight = botLeft + PixelSize;
var flooredBL = botLeft - originBL;
diff --git a/Content.Client/Silicons/Borgs/BorgModuleControl.xaml b/Content.Client/Silicons/Borgs/BorgModuleControl.xaml
index 2f6e25f983..1be7cea8d9 100644
--- a/Content.Client/Silicons/Borgs/BorgModuleControl.xaml
+++ b/Content.Client/Silicons/Borgs/BorgModuleControl.xaml
@@ -3,10 +3,10 @@
StyleClasses="PanelLight"
Margin="5 5 5 0">
+
-
diff --git a/Content.Client/Storage/Systems/EntityStorageSystem.cs b/Content.Client/Storage/Systems/EntityStorageSystem.cs
index ca2b986667..96dc353c48 100644
--- a/Content.Client/Storage/Systems/EntityStorageSystem.cs
+++ b/Content.Client/Storage/Systems/EntityStorageSystem.cs
@@ -1,43 +1,5 @@
-using System.Diagnostics.CodeAnalysis;
-using Content.Client.Storage.Components;
-using Content.Shared.Destructible;
-using Content.Shared.Foldable;
-using Content.Shared.Interaction;
-using Content.Shared.Lock;
-using Content.Shared.Movement.Events;
-using Content.Shared.Storage.Components;
-using Content.Shared.Storage.EntitySystems;
-using Content.Shared.Verbs;
-using Robust.Shared.GameStates;
+using Content.Shared.Storage.EntitySystems;
namespace Content.Client.Storage.Systems;
-public sealed class EntityStorageSystem : SharedEntityStorageSystem
-{
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent(OnEntityUnpausedEvent);
- SubscribeLocalEvent(OnComponentInit);
- SubscribeLocalEvent(OnComponentStartup);
- SubscribeLocalEvent(OnInteract, after: new[] { typeof(LockSystem) });
- SubscribeLocalEvent(OnLockToggleAttempt);
- SubscribeLocalEvent(OnDestruction);
- SubscribeLocalEvent>(AddToggleOpenVerb);
- SubscribeLocalEvent(OnRelayMovement);
- SubscribeLocalEvent(OnFoldAttempt);
-
- SubscribeLocalEvent(OnGetState);
- SubscribeLocalEvent(OnHandleState);
- }
-
- public override bool ResolveStorage(EntityUid uid, [NotNullWhen(true)] ref EntityStorageComponent? component)
- {
- if (component != null)
- return true;
-
- TryComp(uid, out var storage);
- component = storage;
- return component != null;
- }
-}
+public sealed class EntityStorageSystem : SharedEntityStorageSystem;
diff --git a/Content.Client/Stylesheets/Colorspace/ColorExtensions.cs b/Content.Client/Stylesheets/Colorspace/ColorExtensions.cs
index 1a191992b1..6af9308bbc 100644
--- a/Content.Client/Stylesheets/Colorspace/ColorExtensions.cs
+++ b/Content.Client/Stylesheets/Colorspace/ColorExtensions.cs
@@ -14,10 +14,10 @@ public static class ColorExtensions
{
DebugTools.Assert(lightness is >= 0.0f and <= 1.0f);
- var oklab = Color.ToLab(c);
+ var oklab = c.LabFromSrgb();
oklab.X = lightness;
- return Color.FromLab(oklab);
+ return oklab.LabToSrgb();
}
///
@@ -25,10 +25,10 @@ public static class ColorExtensions
///
public static Color NudgeLightness(this Color c, float lightnessShift)
{
- var oklab = Color.ToLab(c);
+ var oklab = c.LabFromSrgb();
oklab.X = Math.Clamp(oklab.X + lightnessShift, 0, 1);
- return Color.FromLab(oklab);
+ return oklab.LabToSrgb();
}
///
@@ -39,12 +39,12 @@ public static class ColorExtensions
///
public static Color NudgeChroma(this Color c, float chromaShift)
{
- var oklab = Color.ToLab(c);
+ var oklab = c.LabFromSrgb();
var oklch = Color.ToLch(oklab);
oklch.Y = Math.Clamp(oklch.Y + chromaShift, 0, 1);
- return Color.FromLab(Color.FromLch(oklch));
+ return Color.FromLch(oklch).LabToSrgb();
}
///
@@ -54,10 +54,43 @@ public static class ColorExtensions
{
DebugTools.Assert(factor is >= 0.0f and <= 1.0f);
- var okFrom = Color.ToLab(from);
- var okTo = Color.ToLab(to);
+ var okFrom = from.LabFromSrgb();
+ var okTo = to.LabFromSrgb();
var blended = Vector4.Lerp(okFrom, okTo, factor);
- return Color.FromLab(blended);
+ return blended.LabToSrgb();
+ }
+
+ ///
+ /// Converts a nonlinear sRGB ("normal") color to OkLAB.
+ ///
+ public static Vector4 LabFromSrgb(this Color from)
+ {
+ return Color.ToLab(Color.FromSrgb(from));
+ }
+
+ ///
+ /// Converts OkLAB to a nonlinear sRGB ("normal") color.
+ ///
+ public static Color LabToSrgb(this Vector4 from)
+ {
+ return Color.ToSrgb(Color.FromLab(from).SimpleClipGamut());
+ }
+
+ ///
+ /// Clips the gamut of the color so that all color channels are in the range 0 -> 1.
+ ///
+ ///
+ /// This uses no clever perceptual techniques, it literally just clamps the individual channels.
+ ///
+ public static Color SimpleClipGamut(this Color from)
+ {
+ return new Color
+ {
+ R = Math.Clamp(from.R, 0, 1),
+ G = Math.Clamp(from.G, 0, 1),
+ B = Math.Clamp(from.B, 0, 1),
+ A = from.A,
+ };
}
}
diff --git a/Content.Client/Stylesheets/Sheetlets/LabelSheetlet.cs b/Content.Client/Stylesheets/Sheetlets/LabelSheetlet.cs
index 77c554a9f3..fc23f9c7fb 100644
--- a/Content.Client/Stylesheets/Sheetlets/LabelSheetlet.cs
+++ b/Content.Client/Stylesheets/Sheetlets/LabelSheetlet.cs
@@ -69,10 +69,10 @@ public sealed class LabelSheetlet : Sheetlet
.Class(StyleClass.LabelMonospaceText)
.Prop(Label.StylePropertyFont, robotoMonoBold11),
E
[UsedImplicitly]
-public sealed class ScrambleTag : IMarkupTag
+public sealed class ScrambleTag : IMarkupTagHandler
{
[Dependency] private readonly IGameTiming _timing = default!;
diff --git a/Content.Client/UserInterface/Systems/Actions/Controls/ActionButton.cs b/Content.Client/UserInterface/Systems/Actions/Controls/ActionButton.cs
index ec66e4a9da..b77d81dc6e 100644
--- a/Content.Client/UserInterface/Systems/Actions/Controls/ActionButton.cs
+++ b/Content.Client/UserInterface/Systems/Actions/Controls/ActionButton.cs
@@ -198,8 +198,8 @@ public sealed class ActionButton : Control, IEntityControl
if (!_entities.TryGetComponent(Action, out MetaDataComponent? metadata))
return null;
- var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityName));
- var desc = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityDescription));
+ var name = FormattedMessage.FromMarkupPermissive(metadata.EntityName);
+ var desc = FormattedMessage.FromMarkupPermissive(metadata.EntityDescription);
if (_player.LocalEntity is null)
return null;
diff --git a/Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs b/Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs
index 7ab18a586c..512bd7b436 100644
--- a/Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs
+++ b/Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs
@@ -200,7 +200,7 @@ public partial class ChatBox : UIWidget
}
// End EE - Chat stacking
- Contents.AddMessage(formatted);
+ Contents.AddMessage(formatted, tagsAllowed: null);
}
public void Focus(ChatSelectChannel? channel = null)
diff --git a/Content.Client/UserInterface/Systems/DamageOverlays/DamageOverlayUiController.cs b/Content.Client/UserInterface/Systems/DamageOverlays/DamageOverlayUiController.cs
index 20db76554d..f709df4b77 100644
--- a/Content.Client/UserInterface/Systems/DamageOverlays/DamageOverlayUiController.cs
+++ b/Content.Client/UserInterface/Systems/DamageOverlays/DamageOverlayUiController.cs
@@ -3,6 +3,7 @@ using Content.Shared.FixedPoint;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
+using Content.Shared.StatusEffectNew;
using Content.Shared.Traits.Assorted;
using JetBrains.Annotations;
using Robust.Client.Graphics;
@@ -20,6 +21,7 @@ public sealed class DamageOverlayUiController : UIController
[Dependency] private readonly IPlayerManager _playerManager = default!;
[UISystemDependency] private readonly MobThresholdSystem _mobThresholdSystem = default!;
+ [UISystemDependency] private readonly StatusEffectsSystem _statusEffects = default!;
private Overlays.DamageOverlay _overlay = default!;
public override void Initialize()
@@ -98,7 +100,7 @@ public sealed class DamageOverlayUiController : UIController
FixedPoint2 painLevel = 0;
_overlay.PainLevel = 0;
- if (!EntityManager.HasComponent(entity))
+ if (!_statusEffects.TryEffectsWithComp(entity, out _))
{
foreach (var painDamageType in damageable.PainDamageGroups)
{
diff --git a/Content.Client/VoiceMask/VoiceMaskBoundUserInterface.cs b/Content.Client/VoiceMask/VoiceMaskBoundUserInterface.cs
index e76ca1cf8f..b2b374cac5 100644
--- a/Content.Client/VoiceMask/VoiceMaskBoundUserInterface.cs
+++ b/Content.Client/VoiceMask/VoiceMaskBoundUserInterface.cs
@@ -26,6 +26,8 @@ public sealed class VoiceMaskBoundUserInterface : BoundUserInterface
_window.OnNameChange += OnNameSelected;
_window.OnVerbChange += verb => SendMessage(new VoiceMaskChangeVerbMessage(verb));
+ _window.OnToggle += OnToggle;
+ _window.OnAccentToggle += OnAccentToggle;
}
private void OnNameSelected(string name)
@@ -33,6 +35,16 @@ public sealed class VoiceMaskBoundUserInterface : BoundUserInterface
SendMessage(new VoiceMaskChangeNameMessage(name));
}
+ private void OnToggle()
+ {
+ SendMessage(new VoiceMaskToggleMessage());
+ }
+
+ private void OnAccentToggle()
+ {
+ SendMessage(new VoiceMaskAccentToggleMessage());
+ }
+
protected override void UpdateState(BoundUserInterfaceState state)
{
if (state is not VoiceMaskBuiState cast || _window == null)
@@ -40,7 +52,7 @@ public sealed class VoiceMaskBoundUserInterface : BoundUserInterface
return;
}
- _window.UpdateState(cast.Name, cast.Verb);
+ _window.UpdateState(cast.Name, cast.Verb, cast.Active, cast.AccentHide);
}
protected override void Dispose(bool disposing)
diff --git a/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml b/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml
index e23aca1239..18416757b9 100644
--- a/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml
+++ b/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml
@@ -12,5 +12,7 @@
+
+
diff --git a/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs b/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs
index 7ca4dd4b95..a5e7036283 100644
--- a/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs
+++ b/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs
@@ -12,6 +12,8 @@ public sealed partial class VoiceMaskNameChangeWindow : FancyWindow
{
public Action? OnNameChange;
public Action? OnVerbChange;
+ public Action? OnToggle;
+ public Action? OnAccentToggle;
private List<(string, string)> _verbs = new();
@@ -31,6 +33,9 @@ public sealed partial class VoiceMaskNameChangeWindow : FancyWindow
OnVerbChange?.Invoke((string?) args.Button.GetItemMetadata(args.Id));
SpeechVerbSelector.SelectId(args.Id);
};
+
+ ToggleButton.OnPressed += args => OnToggle?.Invoke();
+ ToggleAccentButton.OnPressed += args => OnAccentToggle?.Invoke();
}
public void ReloadVerbs(IPrototypeManager proto)
@@ -64,10 +69,12 @@ public sealed partial class VoiceMaskNameChangeWindow : FancyWindow
SpeechVerbSelector.SelectId(id);
}
- public void UpdateState(string name, string? verb)
+ public void UpdateState(string name, string? verb, bool active, bool accentHide)
{
NameSelector.Text = name;
_verb = verb;
+ ToggleButton.Pressed = active;
+ ToggleAccentButton.Pressed = accentHide;
for (int id = 0; id < SpeechVerbSelector.ItemCount; id++)
{
diff --git a/Content.Client/Wall/WallmountDebugOverlay.cs b/Content.Client/Wall/WallmountDebugOverlay.cs
new file mode 100644
index 0000000000..02fd9ebeec
--- /dev/null
+++ b/Content.Client/Wall/WallmountDebugOverlay.cs
@@ -0,0 +1,58 @@
+using Content.Shared.Interaction;
+using Content.Shared.Wall;
+using Robust.Client.Graphics;
+using Robust.Shared.Enums;
+using System.Numerics;
+
+namespace Content.Client.Wall;
+
+///
+/// Shows the area in which entities with can be interacted from.
+///
+public sealed class WallmountDebugOverlay : Overlay
+{
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ private readonly SharedTransformSystem _transform;
+ private readonly EntityLookupSystem _lookup;
+ private readonly HashSet> _intersecting = [];
+
+ public override OverlaySpace Space => OverlaySpace.WorldSpace;
+
+ public WallmountDebugOverlay()
+ {
+ IoCManager.InjectDependencies(this);
+
+ _transform = _entManager.System();
+ _lookup = _entManager.System();
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ _intersecting.Clear();
+ _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds, _intersecting);
+ foreach (var ent in _intersecting)
+ {
+ var (worldPos, worldRot) = _transform.GetWorldPositionRotation(ent.Owner);
+ DrawArc(args.WorldHandle, worldPos, SharedInteractionSystem.InteractionRange, worldRot + ent.Comp.Direction, ent.Comp.Arc);
+ }
+ }
+
+ private static void DrawArc(DrawingHandleWorld handle, Vector2 position, float radius, Angle rot, Angle arc)
+ {
+ // 32 segments for a full circle, but 2 at least
+ var segments = Math.Max((int)(arc.Theta / Math.Tau * 32), 2);
+ var step = arc.Theta / (segments - 1);
+ var verts = new Vector2[segments + 1];
+
+ verts[0] = position;
+ for (var i = 0; i < segments; i++)
+ {
+ var angle = (float)(-arc.Theta / 2 + i * step - rot.Theta + Math.PI);
+ var pos = new Vector2(MathF.Sin(angle), MathF.Cos(angle));
+
+ verts[i + 1] = position + pos * radius;
+ }
+
+ handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts, Color.Green.WithAlpha(0.5f));
+ }
+}
diff --git a/Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs b/Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs
index 15d137d8a9..0bccd5b1b3 100644
--- a/Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs
+++ b/Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs
@@ -43,6 +43,9 @@ public sealed partial class MeleeWeaponSystem
return;
}
+ var length = 1f;
+ var offset = 1f;
+
var spriteRotation = Angle.Zero;
if (arcComponent.Animation != WeaponArcAnimation.None
&& TryComp(weapon, out MeleeWeaponComponent? meleeWeaponComponent))
@@ -55,10 +58,11 @@ public sealed partial class MeleeWeaponSystem
if (meleeWeaponComponent.SwingLeft)
angle *= -1;
- if (meleeWeaponComponent.ChangeSwingDirection) meleeWeaponComponent.SwingLeft = !meleeWeaponComponent.SwingLeft; // DeltaV - Nice swing animation for desword
+
+ length = (1 / meleeWeaponComponent.AttackRate) * 0.6f;
+ offset = meleeWeaponComponent.AnimationOffset;
}
_sprite.SetRotation((animationUid, sprite), localPos.ToWorldAngle());
- var distance = Math.Clamp(localPos.Length() / 2f, 0.2f, 1f);
var xform = _xformQuery.GetComponent(animationUid);
TrackUserComponent track;
@@ -68,16 +72,16 @@ public sealed partial class MeleeWeaponSystem
case WeaponArcAnimation.Slash:
track = EnsureComp(animationUid);
track.User = user;
- _animation.Play(animationUid, GetSlashAnimation(sprite, angle, spriteRotation), SlashAnimationKey);
+ _animation.Play(animationUid, GetSlashAnimation((animationUid, sprite), angle, spriteRotation, length, offset), SlashAnimationKey);
if (arcComponent.Fadeout)
- _animation.Play(animationUid, GetFadeAnimation(sprite, 0.065f, 0.065f + 0.05f), FadeAnimationKey);
+ _animation.Play(animationUid, GetFadeAnimation(sprite, length * 0.5f, length + 0.15f), FadeAnimationKey);
break;
case WeaponArcAnimation.Thrust:
track = EnsureComp(animationUid);
track.User = user;
- _animation.Play(animationUid, GetThrustAnimation((animationUid, sprite), distance, spriteRotation), ThrustAnimationKey);
+ _animation.Play(animationUid, GetThrustAnimation((animationUid, sprite), offset, spriteRotation, length), ThrustAnimationKey);
if (arcComponent.Fadeout)
- _animation.Play(animationUid, GetFadeAnimation(sprite, 0.05f, 0.15f), FadeAnimationKey);
+ _animation.Play(animationUid, GetFadeAnimation(sprite, length * 0.5f, length + 0.15f), FadeAnimationKey);
break;
case WeaponArcAnimation.None:
var (mapPos, mapRot) = TransformSystem.GetWorldPositionRotation(userXform);
@@ -90,21 +94,22 @@ public sealed partial class MeleeWeaponSystem
}
}
- private Animation GetSlashAnimation(SpriteComponent sprite, Angle arc, Angle spriteRotation)
+ private Animation GetSlashAnimation(Entity sprite, Angle arc, Angle spriteRotation, float length, float offset)
{
- const float slashStart = 0.03f;
- const float slashEnd = 0.065f;
- const float length = slashEnd + 0.05f;
- var startRotation = sprite.Rotation + arc / 2;
- var endRotation = sprite.Rotation - arc / 2;
- var startRotationOffset = startRotation.RotateVec(new Vector2(0f, -1f));
- var endRotationOffset = endRotation.RotateVec(new Vector2(0f, -1f));
+ var startRotation = sprite.Comp.Rotation + (arc * 0.5f);
+ var endRotation = sprite.Comp.Rotation - (arc * 0.5f);
+
+ var startRotationOffset = startRotation.RotateVec(new Vector2(0f, -offset * 0.9f));
+ var minRotationOffset = sprite.Comp.Rotation.RotateVec(new Vector2(0f, -offset * 1.1f));
+ var endRotationOffset = endRotation.RotateVec(new Vector2(0f, -offset * 0.9f));
+
startRotation += spriteRotation;
endRotation += spriteRotation;
+ sprite.Comp.NoRotation = true;
return new Animation()
{
- Length = TimeSpan.FromSeconds(length),
+ Length = TimeSpan.FromSeconds(length + 0.05f),
AnimationTracks =
{
new AnimationTrackComponentProperty()
@@ -113,10 +118,12 @@ public sealed partial class MeleeWeaponSystem
Property = nameof(SpriteComponent.Rotation),
KeyFrames =
{
- new AnimationTrackProperty.KeyFrame(startRotation, 0f),
- new AnimationTrackProperty.KeyFrame(startRotation, slashStart),
- new AnimationTrackProperty.KeyFrame(endRotation, slashEnd)
- }
+ new AnimationTrackProperty.KeyFrame(Angle.Lerp(startRotation,endRotation,0.0f), length * 0.0f),
+ new AnimationTrackProperty.KeyFrame(Angle.Lerp(startRotation,endRotation,0.5f), length * 0.10f),
+ new AnimationTrackProperty.KeyFrame(Angle.Lerp(startRotation,endRotation,1.0f), length * 0.15f),
+ new AnimationTrackProperty.KeyFrame(Angle.Lerp(startRotation,endRotation,0.9f), length * 0.20f),
+ new AnimationTrackProperty.KeyFrame(Angle.Lerp(startRotation,endRotation,0.80f), length * 0.6f, Easings.OutQuart)
+ },
},
new AnimationTrackComponentProperty()
{
@@ -124,21 +131,21 @@ public sealed partial class MeleeWeaponSystem
Property = nameof(SpriteComponent.Offset),
KeyFrames =
{
- new AnimationTrackProperty.KeyFrame(startRotationOffset, 0f),
- new AnimationTrackProperty.KeyFrame(startRotationOffset, slashStart),
- new AnimationTrackProperty.KeyFrame(endRotationOffset, slashEnd)
+ new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startRotationOffset,endRotationOffset,0.0f), length * 0.0f),
+ new AnimationTrackProperty.KeyFrame(minRotationOffset, length * 0.10f),
+ new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startRotationOffset,endRotationOffset,1.0f), length * 0.15f),
+ new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startRotationOffset,endRotationOffset,0.80f), length * 0.6f, Easings.OutQuart)
}
},
}
};
}
- private Animation GetThrustAnimation(Entity sprite, float distance, Angle spriteRotation)
+ private Animation GetThrustAnimation(Entity sprite, float offset, Angle spriteRotation, float length)
{
- const float thrustEnd = 0.05f;
- const float length = 0.15f;
- var startOffset = sprite.Comp.Rotation.RotateVec(new Vector2(0f, -distance / 5f));
- var endOffset = sprite.Comp.Rotation.RotateVec(new Vector2(0f, -distance));
+ var startOffset = sprite.Comp.Rotation.RotateVec(new Vector2(0f, 0f));
+ var endOffset = sprite.Comp.Rotation.RotateVec(new Vector2(0f, -offset * 1.2f));
+
_sprite.SetRotation(sprite.AsNullable(), sprite.Comp.Rotation + spriteRotation);
return new Animation()
@@ -152,9 +159,11 @@ public sealed partial class MeleeWeaponSystem
Property = nameof(SpriteComponent.Offset),
KeyFrames =
{
- new AnimationTrackProperty.KeyFrame(startOffset, 0f),
- new AnimationTrackProperty.KeyFrame(endOffset, thrustEnd),
- new AnimationTrackProperty.KeyFrame(endOffset, length),
+ new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startOffset, endOffset, 0f), length * 0f),
+ new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startOffset, endOffset, 0.65f), length * 0.10f),
+ new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startOffset, endOffset, 1f), length * 0.20f),
+ new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startOffset, endOffset, 0.9f), length * 0.30f),
+ new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startOffset, endOffset, 0.7f), length * 0.60f, Easings.OutQuart)
}
},
}
@@ -201,11 +210,12 @@ public sealed partial class MeleeWeaponSystem
InterpolationMode = AnimationInterpolationMode.Linear,
KeyFrames =
{
- new AnimationTrackProperty.KeyFrame(direction.Normalized() * 0.15f, 0f),
- new AnimationTrackProperty.KeyFrame(Vector2.Zero, length)
- }
- }
- }
+ new AnimationTrackProperty.KeyFrame(Vector2.Zero, 0f),
+ new AnimationTrackProperty.KeyFrame(direction.Normalized() * 0.15f, length*0.4f),
+ new AnimationTrackProperty.KeyFrame(Vector2.Zero, length*0.6f),
+ },
+ },
+ },
};
}
diff --git a/Content.Client/Weapons/Ranged/Components/MagazineVisualsComponent.cs b/Content.Client/Weapons/Ranged/Components/MagazineVisualsComponent.cs
index 373be94a38..025f09b1a4 100644
--- a/Content.Client/Weapons/Ranged/Components/MagazineVisualsComponent.cs
+++ b/Content.Client/Weapons/Ranged/Components/MagazineVisualsComponent.cs
@@ -11,17 +11,20 @@ public sealed partial class MagazineVisualsComponent : Component
///
/// What RsiState we use.
///
- [DataField("magState")] public string? MagState;
+ [DataField]
+ public string? MagState;
///
/// How many steps there are
///
- [DataField("steps")] public int MagSteps;
+ [DataField("steps")]
+ public int MagSteps;
///
/// Should we hide when the count is 0
///
- [DataField("zeroVisible")] public bool ZeroVisible;
+ [DataField]
+ public bool ZeroVisible;
}
public enum GunVisualLayers : byte
diff --git a/Content.Client/Weapons/Ranged/Components/SpentAmmoVisualsComponent.cs b/Content.Client/Weapons/Ranged/Components/SpentAmmoVisualsComponent.cs
index 622a168fbf..03f7532659 100644
--- a/Content.Client/Weapons/Ranged/Components/SpentAmmoVisualsComponent.cs
+++ b/Content.Client/Weapons/Ranged/Components/SpentAmmoVisualsComponent.cs
@@ -8,9 +8,10 @@ public sealed partial class SpentAmmoVisualsComponent : Component
///
/// Should we do "{_state}-spent" or just "spent"
///
- [DataField("suffix")] public bool Suffix = true;
+ [DataField]
+ public bool Suffix = true;
- [DataField("state")]
+ [DataField]
public string State = "base";
}
diff --git a/Content.Client/Weapons/Ranged/GunSpreadOverlay.cs b/Content.Client/Weapons/Ranged/GunSpreadOverlay.cs
index 63d21c8463..c714ca2eb5 100644
--- a/Content.Client/Weapons/Ranged/GunSpreadOverlay.cs
+++ b/Content.Client/Weapons/Ranged/GunSpreadOverlay.cs
@@ -48,7 +48,7 @@ public sealed class GunSpreadOverlay : Overlay
if (mapPos.MapId == MapId.Nullspace)
return;
- if (!_guns.TryGetGun(player.Value, out var gunUid, out var gun))
+ if (!_guns.TryGetGun(player.Value, out var gun))
return;
var mouseScreenPos = _input.MouseScreenPosition;
@@ -58,12 +58,12 @@ public sealed class GunSpreadOverlay : Overlay
return;
// (☞゚ヮ゚)☞
- var maxSpread = gun.MaxAngleModified;
- var minSpread = gun.MinAngleModified;
- var timeSinceLastFire = (_timing.CurTime - gun.NextFire).TotalSeconds;
- var currentAngle = new Angle(MathHelper.Clamp(gun.CurrentAngle.Theta - gun.AngleDecayModified.Theta * timeSinceLastFire,
- gun.MinAngleModified.Theta, gun.MaxAngleModified.Theta));
- var direction = (mousePos.Position - mapPos.Position);
+ var maxSpread = gun.Comp.MaxAngleModified;
+ var minSpread = gun.Comp.MinAngleModified;
+ var timeSinceLastFire = (_timing.CurTime - gun.Comp.NextFire).TotalSeconds;
+ var currentAngle = new Angle(MathHelper.Clamp(gun.Comp.CurrentAngle.Theta - gun.Comp.AngleDecayModified.Theta * timeSinceLastFire,
+ gun.Comp.MinAngleModified.Theta, gun.Comp.MaxAngleModified.Theta));
+ var direction = mousePos.Position - mapPos.Position;
worldHandle.DrawLine(mapPos.Position, mousePos.Position + direction, Color.Orange);
diff --git a/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs b/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs
index e11d5e7158..3c57cf73d0 100644
--- a/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs
+++ b/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs
@@ -41,7 +41,7 @@ public abstract class BaseBulletRenderer : Control
{
var countPerRow = Math.Min(Capacity, CountPerRow(availableSize.X));
- var rows = Math.Min((int) MathF.Ceiling(Capacity / (float) countPerRow), Rows);
+ var rows = Math.Min((int)MathF.Ceiling(Capacity / (float)countPerRow), Rows);
var height = _params.ItemHeight * rows + (_params.VerticalSeparation * rows - 1);
var width = RowWidth(countPerRow);
@@ -110,7 +110,7 @@ public abstract class BaseBulletRenderer : Control
private int CountPerRow(float width)
{
- return (int) ((width - _params.ItemWidth + _params.ItemSeparation) / _params.ItemSeparation);
+ return (int)((width - _params.ItemWidth + _params.ItemSeparation) / _params.ItemSeparation);
}
private int RowWidth(int count)
diff --git a/Content.Client/Weapons/Ranged/Systems/FlyBySoundSystem.cs b/Content.Client/Weapons/Ranged/Systems/FlyBySoundSystem.cs
index 401b7cdfaf..3a0aa25501 100644
--- a/Content.Client/Weapons/Ranged/Systems/FlyBySoundSystem.cs
+++ b/Content.Client/Weapons/Ranged/Systems/FlyBySoundSystem.cs
@@ -2,10 +2,8 @@ using Content.Shared.Projectiles;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Client.Player;
-using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Physics.Events;
-using Robust.Shared.Player;
using Robust.Shared.Random;
namespace Content.Client.Weapons.Ranged.Systems;
@@ -22,26 +20,26 @@ public sealed class FlyBySoundSystem : SharedFlyBySoundSystem
SubscribeLocalEvent(OnCollide);
}
- private void OnCollide(EntityUid uid, FlyBySoundComponent component, ref StartCollideEvent args)
+ private void OnCollide(Entity ent, ref StartCollideEvent args)
{
var attachedEnt = _player.LocalEntity;
// If it's not our ent or we shot it.
if (attachedEnt == null ||
args.OtherEntity != attachedEnt ||
- TryComp(uid, out var projectile) &&
+ TryComp(ent, out var projectile) &&
projectile.Shooter == attachedEnt)
{
return;
}
if (args.OurFixtureId != FlyByFixture ||
- !_random.Prob(component.Prob))
+ !_random.Prob(ent.Comp.Prob))
{
return;
}
// Play attached to our entity because the projectile may immediately delete or the likes.
- _audio.PlayPredicted(component.Sound, attachedEnt.Value, attachedEnt.Value);
+ _audio.PlayPredicted(ent.Comp.Sound, attachedEnt.Value, attachedEnt.Value);
}
}
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs
index dc27a5db87..49b067e395 100644
--- a/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs
@@ -14,12 +14,12 @@ namespace Content.Client.Weapons.Ranged.Systems;
public sealed partial class GunSystem
{
- private void OnAmmoCounterCollect(EntityUid uid, AmmoCounterComponent component, ItemStatusCollectMessage args)
+ private void OnAmmoCounterCollect(Entity ent, ref ItemStatusCollectMessage args)
{
- RefreshControl(uid, component);
+ RefreshControl(ent);
- if (component.Control != null)
- args.Controls.Add(component.Control);
+ if (ent.Comp.Control != null)
+ args.Controls.Add(ent.Comp.Control);
}
///
@@ -27,35 +27,32 @@ public sealed partial class GunSystem
///
///
///
- private void RefreshControl(EntityUid uid, AmmoCounterComponent? component = null)
+ private void RefreshControl(Entity ent)
{
- if (!Resolve(uid, ref component, false))
- return;
-
- component.Control?.Dispose();
- component.Control = null;
+ ent.Comp.Control?.Dispose();
+ ent.Comp.Control = null;
var ev = new AmmoCounterControlEvent();
- RaiseLocalEvent(uid, ev, false);
+ RaiseLocalEvent(ent, ev, false);
// Fallback to default if none specified
ev.Control ??= new DefaultStatusControl();
- component.Control = ev.Control;
- UpdateAmmoCount(uid, component);
+ ent.Comp.Control = ev.Control;
+ UpdateAmmoCount(ent);
}
- private void UpdateAmmoCount(EntityUid uid, AmmoCounterComponent component)
+ private void UpdateAmmoCount(Entity ent)
{
- if (component.Control == null)
+ if (ent.Comp.Control == null)
return;
var ev = new UpdateAmmoCounterEvent()
{
- Control = component.Control
+ Control = ent.Comp.Control
};
- RaiseLocalEvent(uid, ev, false);
+ RaiseLocalEvent(ent, ev, false);
}
protected override void UpdateAmmoCount(EntityUid uid, bool prediction = true)
@@ -68,7 +65,7 @@ public sealed partial class GunSystem
return;
}
- UpdateAmmoCount(uid, clientComp);
+ UpdateAmmoCount((uid, clientComp));
}
///
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.Ballistic.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.Ballistic.cs
index d09661b770..44593215aa 100644
--- a/Content.Client/Weapons/Ranged/Systems/GunSystem.Ballistic.cs
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.Ballistic.cs
@@ -12,41 +12,41 @@ public sealed partial class GunSystem
SubscribeLocalEvent(OnBallisticAmmoCount);
}
- private void OnBallisticAmmoCount(EntityUid uid, BallisticAmmoProviderComponent component, UpdateAmmoCounterEvent args)
+ private void OnBallisticAmmoCount(Entity ent, ref UpdateAmmoCounterEvent args)
{
if (args.Control is DefaultStatusControl control)
{
- control.Update(GetBallisticShots(component), component.Capacity);
+ control.Update(GetBallisticShots(ent.Comp), ent.Comp.Capacity);
}
}
- protected override void Cycle(EntityUid uid, BallisticAmmoProviderComponent component, MapCoordinates coordinates)
+ protected override void Cycle(Entity ent, MapCoordinates coordinates)
{
if (!Timing.IsFirstTimePredicted)
return;
- EntityUid? ent = null;
+ EntityUid? ammoEnt = null;
// TODO: Combine with TakeAmmo
- if (component.Entities.Count > 0)
+ if (ent.Comp.Entities.Count > 0)
{
- var existing = component.Entities[^1];
- component.Entities.RemoveAt(component.Entities.Count - 1);
+ var existing = ent.Comp.Entities[^1];
+ ent.Comp.Entities.RemoveAt(ent.Comp.Entities.Count - 1);
- Containers.Remove(existing, component.Container);
+ Containers.Remove(existing, ent.Comp.Container);
EnsureShootable(existing);
}
- else if (component.UnspawnedCount > 0)
+ else if (ent.Comp.UnspawnedCount > 0)
{
- component.UnspawnedCount--;
- ent = Spawn(component.Proto, coordinates);
- EnsureShootable(ent.Value);
+ ent.Comp.UnspawnedCount--;
+ ammoEnt = Spawn(ent.Comp.Proto, coordinates);
+ EnsureShootable(ammoEnt.Value);
}
- if (ent != null && IsClientSide(ent.Value))
- Del(ent.Value);
+ if (ammoEnt != null && IsClientSide(ammoEnt.Value))
+ Del(ammoEnt.Value);
var cycledEvent = new GunCycledEvent();
- RaiseLocalEvent(uid, ref cycledEvent);
+ RaiseLocalEvent(ent, ref cycledEvent);
}
}
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.BasicEntity.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.BasicEntity.cs
index 4fa50999bd..669bc3ec40 100644
--- a/Content.Client/Weapons/Ranged/Systems/GunSystem.BasicEntity.cs
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.BasicEntity.cs
@@ -10,11 +10,11 @@ public partial class GunSystem
SubscribeLocalEvent(OnBasicEntityAmmoCount);
}
- private void OnBasicEntityAmmoCount(EntityUid uid, BasicEntityAmmoProviderComponent component, UpdateAmmoCounterEvent args)
+ private void OnBasicEntityAmmoCount(Entity ent, ref UpdateAmmoCounterEvent args)
{
- if (args.Control is DefaultStatusControl control && component.Count != null && component.Capacity != null)
+ if (args.Control is DefaultStatusControl control && ent.Comp.Count != null && ent.Comp.Capacity != null)
{
- control.Update(component.Count.Value, component.Capacity.Value);
+ control.Update(ent.Comp.Count.Value, ent.Comp.Capacity.Value);
}
}
}
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.ChamberMagazine.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.ChamberMagazine.cs
index d40708de99..e47b0c952d 100644
--- a/Content.Client/Weapons/Ranged/Systems/GunSystem.ChamberMagazine.cs
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.ChamberMagazine.cs
@@ -18,11 +18,11 @@ public sealed partial class GunSystem
SubscribeLocalEvent(OnChamberMagazineAppearance);
}
- private void OnChamberMagazineAppearance(EntityUid uid, ChamberMagazineAmmoProviderComponent component, ref AppearanceChangeEvent args)
+ private void OnChamberMagazineAppearance(Entity ent, ref AppearanceChangeEvent args)
{
if (args.Sprite == null ||
- !_sprite.LayerMapTryGet((uid, args.Sprite), GunVisualLayers.Base, out var boltLayer, false) ||
- !Appearance.TryGetData(uid, AmmoVisuals.BoltClosed, out bool boltClosed))
+ !_sprite.LayerMapTryGet((ent, args.Sprite), GunVisualLayers.Base, out var boltLayer, false) ||
+ !Appearance.TryGetData(ent, AmmoVisuals.BoltClosed, out bool boltClosed))
{
return;
}
@@ -30,11 +30,11 @@ public sealed partial class GunSystem
// Maybe re-using base layer for this will bite me someday but screw you future sloth.
if (boltClosed)
{
- _sprite.LayerSetRsiState((uid, args.Sprite), boltLayer, "base");
+ _sprite.LayerSetRsiState((ent, args.Sprite), boltLayer, "base");
}
else
{
- _sprite.LayerSetRsiState((uid, args.Sprite), boltLayer, "bolt-open");
+ _sprite.LayerSetRsiState((ent, args.Sprite), boltLayer, "bolt-open");
}
}
@@ -55,17 +55,17 @@ public sealed partial class GunSystem
// to avoid 6-7 additional entity spawns.
}
- private void OnChamberMagazineCounter(EntityUid uid, ChamberMagazineAmmoProviderComponent component, AmmoCounterControlEvent args)
+ private void OnChamberMagazineCounter(Entity ent, ref AmmoCounterControlEvent args)
{
args.Control = new ChamberMagazineStatusControl();
}
- private void OnChamberMagazineAmmoUpdate(EntityUid uid, ChamberMagazineAmmoProviderComponent component, UpdateAmmoCounterEvent args)
+ private void OnChamberMagazineAmmoUpdate(Entity ent, ref UpdateAmmoCounterEvent args)
{
if (args.Control is not ChamberMagazineStatusControl control) return;
- var chambered = GetChamberEntity(uid);
- var magEntity = GetMagazineEntity(uid);
+ var chambered = GetChamberEntity(ent);
+ var magEntity = GetMagazineEntity(ent);
var ammoCountEv = new GetAmmoCountEvent();
if (magEntity != null)
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.Magazine.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.Magazine.cs
index 0df95e4c02..e21eef570e 100644
--- a/Content.Client/Weapons/Ranged/Systems/GunSystem.Magazine.cs
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.Magazine.cs
@@ -11,11 +11,11 @@ public sealed partial class GunSystem
SubscribeLocalEvent(OnMagazineControl);
}
- private void OnMagazineAmmoUpdate(EntityUid uid, MagazineAmmoProviderComponent component, UpdateAmmoCounterEvent args)
+ private void OnMagazineAmmoUpdate(Entity ent, ref UpdateAmmoCounterEvent args)
{
- var ent = GetMagazineEntity(uid);
+ var magEnt = GetMagazineEntity(ent);
- if (ent == null)
+ if (magEnt == null)
{
if (args.Control is DefaultStatusControl control)
{
@@ -25,14 +25,14 @@ public sealed partial class GunSystem
return;
}
- RaiseLocalEvent(ent.Value, args, false);
+ RaiseLocalEvent(magEnt.Value, args, false);
}
- private void OnMagazineControl(EntityUid uid, MagazineAmmoProviderComponent component, AmmoCounterControlEvent args)
+ private void OnMagazineControl(Entity ent, ref AmmoCounterControlEvent args)
{
- var ent = GetMagazineEntity(uid);
- if (ent == null)
+ var magEnt = GetMagazineEntity(ent);
+ if (magEnt == null)
return;
- RaiseLocalEvent(ent.Value, args, false);
+ RaiseLocalEvent(magEnt.Value, args, false);
}
}
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.MagazineVisuals.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.MagazineVisuals.cs
index c21b12ceac..8bf37eaca8 100644
--- a/Content.Client/Weapons/Ranged/Systems/GunSystem.MagazineVisuals.cs
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.MagazineVisuals.cs
@@ -13,24 +13,24 @@ public sealed partial class GunSystem
SubscribeLocalEvent(OnMagazineVisualsChange);
}
- private void OnMagazineVisualsInit(EntityUid uid, MagazineVisualsComponent component, ComponentInit args)
+ private void OnMagazineVisualsInit(Entity ent, ref ComponentInit args)
{
- if (!TryComp(uid, out var sprite)) return;
+ if (!TryComp(ent, out var sprite)) return;
- if (_sprite.LayerMapTryGet((uid, sprite), GunVisualLayers.Mag, out _, false))
+ if (_sprite.LayerMapTryGet((ent, sprite), GunVisualLayers.Mag, out _, false))
{
- _sprite.LayerSetRsiState((uid, sprite), GunVisualLayers.Mag, $"{component.MagState}-{component.MagSteps - 1}");
- _sprite.LayerSetVisible((uid, sprite), GunVisualLayers.Mag, false);
+ _sprite.LayerSetRsiState((ent, sprite), GunVisualLayers.Mag, $"{ent.Comp.MagState}-{ent.Comp.MagSteps - 1}");
+ _sprite.LayerSetVisible((ent, sprite), GunVisualLayers.Mag, false);
}
- if (_sprite.LayerMapTryGet((uid, sprite), GunVisualLayers.MagUnshaded, out _, false))
+ if (_sprite.LayerMapTryGet((ent, sprite), GunVisualLayers.MagUnshaded, out _, false))
{
- _sprite.LayerSetRsiState((uid, sprite), GunVisualLayers.MagUnshaded, $"{component.MagState}-unshaded-{component.MagSteps - 1}");
- _sprite.LayerSetVisible((uid, sprite), GunVisualLayers.MagUnshaded, false);
+ _sprite.LayerSetRsiState((ent, sprite), GunVisualLayers.MagUnshaded, $"{ent.Comp.MagState}-unshaded-{ent.Comp.MagSteps - 1}");
+ _sprite.LayerSetVisible((ent, sprite), GunVisualLayers.MagUnshaded, false);
}
}
- private void OnMagazineVisualsChange(EntityUid uid, MagazineVisualsComponent component, ref AppearanceChangeEvent args)
+ private void OnMagazineVisualsChange(Entity ent, ref AppearanceChangeEvent args)
{
// tl;dr
// 1.If no mag then hide it OR
@@ -45,53 +45,53 @@ public sealed partial class GunSystem
{
if (!args.AppearanceData.TryGetValue(AmmoVisuals.AmmoMax, out var capacity))
{
- capacity = component.MagSteps;
+ capacity = ent.Comp.MagSteps;
}
if (!args.AppearanceData.TryGetValue(AmmoVisuals.AmmoCount, out var current))
{
- current = component.MagSteps;
+ current = ent.Comp.MagSteps;
}
- var step = ContentHelpers.RoundToLevels((int)current, (int)capacity, component.MagSteps);
+ var step = ContentHelpers.RoundToLevels((int)current, (int)capacity, ent.Comp.MagSteps);
- if (step == 0 && !component.ZeroVisible)
+ if (step == 0 && !ent.Comp.ZeroVisible)
{
- if (_sprite.LayerMapTryGet((uid, sprite), GunVisualLayers.Mag, out _, false))
+ if (_sprite.LayerMapTryGet((ent, sprite), GunVisualLayers.Mag, out _, false))
{
- _sprite.LayerSetVisible((uid, sprite), GunVisualLayers.Mag, false);
+ _sprite.LayerSetVisible((ent, sprite), GunVisualLayers.Mag, false);
}
- if (_sprite.LayerMapTryGet((uid, sprite), GunVisualLayers.MagUnshaded, out _, false))
+ if (_sprite.LayerMapTryGet((ent, sprite), GunVisualLayers.MagUnshaded, out _, false))
{
- _sprite.LayerSetVisible((uid, sprite), GunVisualLayers.MagUnshaded, false);
+ _sprite.LayerSetVisible((ent, sprite), GunVisualLayers.MagUnshaded, false);
}
return;
}
- if (_sprite.LayerMapTryGet((uid, sprite), GunVisualLayers.Mag, out _, false))
+ if (_sprite.LayerMapTryGet((ent, sprite), GunVisualLayers.Mag, out _, false))
{
- _sprite.LayerSetVisible((uid, sprite), GunVisualLayers.Mag, true);
- _sprite.LayerSetRsiState((uid, sprite), GunVisualLayers.Mag, $"{component.MagState}-{step}");
+ _sprite.LayerSetVisible((ent, sprite), GunVisualLayers.Mag, true);
+ _sprite.LayerSetRsiState((ent, sprite), GunVisualLayers.Mag, $"{ent.Comp.MagState}-{step}");
}
- if (_sprite.LayerMapTryGet((uid, sprite), GunVisualLayers.MagUnshaded, out _, false))
+ if (_sprite.LayerMapTryGet((ent, sprite), GunVisualLayers.MagUnshaded, out _, false))
{
- _sprite.LayerSetVisible((uid, sprite), GunVisualLayers.MagUnshaded, true);
- _sprite.LayerSetRsiState((uid, sprite), GunVisualLayers.MagUnshaded, $"{component.MagState}-unshaded-{step}");
+ _sprite.LayerSetVisible((ent, sprite), GunVisualLayers.MagUnshaded, true);
+ _sprite.LayerSetRsiState((ent, sprite), GunVisualLayers.MagUnshaded, $"{ent.Comp.MagState}-unshaded-{step}");
}
}
else
{
- if (_sprite.LayerMapTryGet((uid, sprite), GunVisualLayers.Mag, out _, false))
+ if (_sprite.LayerMapTryGet((ent, sprite), GunVisualLayers.Mag, out _, false))
{
- _sprite.LayerSetVisible((uid, sprite), GunVisualLayers.Mag, false);
+ _sprite.LayerSetVisible((ent, sprite), GunVisualLayers.Mag, false);
}
- if (_sprite.LayerMapTryGet((uid, sprite), GunVisualLayers.MagUnshaded, out _, false))
+ if (_sprite.LayerMapTryGet((ent, sprite), GunVisualLayers.MagUnshaded, out _, false))
{
- _sprite.LayerSetVisible((uid, sprite), GunVisualLayers.MagUnshaded, false);
+ _sprite.LayerSetVisible((ent, sprite), GunVisualLayers.MagUnshaded, false);
}
}
}
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.Revolver.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.Revolver.cs
index 33a4042daf..0a6101b20a 100644
--- a/Content.Client/Weapons/Ranged/Systems/GunSystem.Revolver.cs
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.Revolver.cs
@@ -14,25 +14,25 @@ public sealed partial class GunSystem
SubscribeLocalEvent(OnRevolverEntRemove);
}
- private void OnRevolverEntRemove(EntityUid uid, RevolverAmmoProviderComponent component, EntRemovedFromContainerMessage args)
+ private void OnRevolverEntRemove(Entity ent, ref EntRemovedFromContainerMessage args)
{
if (args.Container.ID != RevolverContainer)
return;
- // See ChamberMagazineAmmoProvider
+ //
if (!IsClientSide(args.Entity))
return;
QueueDel(args.Entity);
}
- private void OnRevolverAmmoUpdate(EntityUid uid, RevolverAmmoProviderComponent component, UpdateAmmoCounterEvent args)
+ private void OnRevolverAmmoUpdate(Entity ent, ref UpdateAmmoCounterEvent args)
{
if (args.Control is not RevolverStatusControl control) return;
- control.Update(component.CurrentIndex, component.Chambers);
+ control.Update(ent.Comp.CurrentIndex, ent.Comp.Chambers);
}
- private void OnRevolverCounter(EntityUid uid, RevolverAmmoProviderComponent component, AmmoCounterControlEvent args)
+ private void OnRevolverCounter(Entity ent, ref AmmoCounterControlEvent args)
{
args.Control = new RevolverStatusControl();
}
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.SpentAmmo.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.SpentAmmo.cs
index dc5aa4d08c..a4fb34fcb7 100644
--- a/Content.Client/Weapons/Ranged/Systems/GunSystem.SpentAmmo.cs
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.SpentAmmo.cs
@@ -11,7 +11,7 @@ public sealed partial class GunSystem
SubscribeLocalEvent(OnSpentAmmoAppearance);
}
- private void OnSpentAmmoAppearance(EntityUid uid, SpentAmmoVisualsComponent component, ref AppearanceChangeEvent args)
+ private void OnSpentAmmoAppearance(Entity ent, ref AppearanceChangeEvent args)
{
var sprite = args.Sprite;
if (sprite == null) return;
@@ -21,15 +21,15 @@ public sealed partial class GunSystem
return;
}
- var spent = (bool) varSpent;
+ var spent = (bool)varSpent;
string state;
if (spent)
- state = component.Suffix ? $"{component.State}-spent" : "spent";
+ state = ent.Comp.Suffix ? $"{ent.Comp.State}-spent" : "spent";
else
- state = component.State;
+ state = ent.Comp.State;
- _sprite.LayerSetRsiState((uid, sprite), AmmoVisualLayers.Base, state);
- _sprite.RemoveLayer((uid, sprite), AmmoVisualLayers.Tip, false);
+ _sprite.LayerSetRsiState((ent, sprite), AmmoVisualLayers.Base, state);
+ _sprite.RemoveLayer((ent, sprite), AmmoVisualLayers.Tip, false);
}
}
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs
index d3dfd50cbf..2814cef650 100644
--- a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs
@@ -31,13 +31,13 @@ namespace Content.Client.Weapons.Ranged.Systems;
public sealed partial class GunSystem : SharedGunSystem
{
+ [Dependency] private readonly AnimationPlayerSystem _animPlayer = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
+ [Dependency] private readonly InputSystem _inputSystem = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IStateManager _state = default!;
- [Dependency] private readonly AnimationPlayerSystem _animPlayer = default!;
- [Dependency] private readonly InputSystem _inputSystem = default!;
[Dependency] private readonly SharedCameraRecoilSystem _recoil = default!;
[Dependency] private readonly SharedMapSystem _maps = default!;
[Dependency] private readonly SharedTransformSystem _xform = default!;
@@ -167,29 +167,29 @@ public sealed partial class GunSystem : SharedGunSystem
var entity = entityNull.Value;
- if (!TryGetGun(entity, out var gunUid, out var gun))
+ if (!TryGetGun(entity, out var gun))
{
return;
}
- var useKey = gun.UseKey ? EngineKeyFunctions.Use : EngineKeyFunctions.UseSecondary;
+ var useKey = gun.Comp.UseKey ? EngineKeyFunctions.Use : EngineKeyFunctions.UseSecondary;
- if (_inputSystem.CmdStates.GetState(useKey) != BoundKeyState.Down && !gun.BurstActivated)
+ if (_inputSystem.CmdStates.GetState(useKey) != BoundKeyState.Down && !gun.Comp.BurstActivated)
{
- if (gun.ShotCounter != 0)
- RaisePredictiveEvent(new RequestStopShootEvent { Gun = GetNetEntity(gunUid) });
+ if (gun.Comp.ShotCounter != 0)
+ RaisePredictiveEvent(new RequestStopShootEvent { Gun = GetNetEntity(gun) });
return;
}
- if (gun.NextFire > Timing.CurTime)
+ if (gun.Comp.NextFire > Timing.CurTime)
return;
var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition);
if (mousePos.MapId == MapId.Nullspace)
{
- if (gun.ShotCounter != 0)
- RaisePredictiveEvent(new RequestStopShootEvent { Gun = GetNetEntity(gunUid) });
+ if (gun.Comp.ShotCounter != 0)
+ RaisePredictiveEvent(new RequestStopShootEvent { Gun = GetNetEntity(gun) });
return;
}
@@ -207,11 +207,11 @@ public sealed partial class GunSystem : SharedGunSystem
{
Target = target,
Coordinates = GetNetCoordinates(coordinates),
- Gun = GetNetEntity(gunUid),
+ Gun = GetNetEntity(gun),
});
}
- public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid? Entity, IShootable Shootable)> ammo,
+ public override void Shoot(Entity gun, List<(EntityUid? Entity, IShootable Shootable)> ammo,
EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, out bool userImpulse, EntityUid? user = null, bool throwItems = false)
{
userImpulse = true;
@@ -226,7 +226,7 @@ public sealed partial class GunSystem : SharedGunSystem
{
if (throwItems)
{
- Recoil(user, direction, gun.CameraRecoilScalarModified);
+ Recoil(user, direction, gun.Comp.CameraRecoilScalarModified);
if (IsClientSide(ent!.Value))
Del(ent.Value);
else
@@ -241,9 +241,9 @@ public sealed partial class GunSystem : SharedGunSystem
if (!cartridge.Spent)
{
SetCartridgeSpent(ent!.Value, cartridge, true);
- MuzzleFlash(gunUid, cartridge, worldAngle, user);
- Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
- Recoil(user, direction, gun.CameraRecoilScalarModified);
+ MuzzleFlash(gun, cartridge, worldAngle, user);
+ Audio.PlayPredicted(gun.Comp.SoundGunshotModified, gun, user);
+ Recoil(user, direction, gun.Comp.CameraRecoilScalarModified);
// TODO: Can't predict entity deletions.
//if (cartridge.DeleteOnSpawn)
// Del(cartridge.Owner);
@@ -251,7 +251,7 @@ public sealed partial class GunSystem : SharedGunSystem
else
{
userImpulse = false;
- Audio.PlayPredicted(gun.SoundEmpty, gunUid, user);
+ Audio.PlayPredicted(gun.Comp.SoundEmpty, gun, user);
}
if (IsClientSide(ent!.Value))
@@ -259,17 +259,17 @@ public sealed partial class GunSystem : SharedGunSystem
break;
case AmmoComponent newAmmo:
- MuzzleFlash(gunUid, newAmmo, worldAngle, user);
- Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
- Recoil(user, direction, gun.CameraRecoilScalarModified);
+ MuzzleFlash(gun, newAmmo, worldAngle, user);
+ Audio.PlayPredicted(gun.Comp.SoundGunshotModified, gun, user);
+ Recoil(user, direction, gun.Comp.CameraRecoilScalarModified);
if (IsClientSide(ent!.Value))
Del(ent.Value);
else
RemoveShootable(ent.Value);
break;
case HitscanAmmoComponent:
- Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
- Recoil(user, direction, gun.CameraRecoilScalarModified);
+ Audio.PlayPredicted(gun.Comp.SoundGunshotModified, gun, user);
+ Recoil(user, direction, gun.Comp.CameraRecoilScalarModified);
break;
}
}
@@ -407,5 +407,5 @@ public sealed partial class GunSystem : SharedGunSystem
}
// TODO: Move RangedDamageSoundComponent to shared so this can be predicted.
- public override void PlayImpactSound(EntityUid otherEntity, DamageSpecifier? modifiedDamage, SoundSpecifier? weaponSound, bool forceWeaponSound) {}
+ public override void PlayImpactSound(EntityUid otherEntity, DamageSpecifier? modifiedDamage, SoundSpecifier? weaponSound, bool forceWeaponSound) { }
}
diff --git a/Content.Client/_DV/SmartFridge/SmartFridgeSystem.cs b/Content.Client/_DV/SmartFridge/SmartFridgeSystem.cs
new file mode 100644
index 0000000000..e68f0eb598
--- /dev/null
+++ b/Content.Client/_DV/SmartFridge/SmartFridgeSystem.cs
@@ -0,0 +1,18 @@
+using Content.Shared._DV.SmartFridge;
+
+namespace Content.Client._DV.SmartFridge;
+
+public sealed class SmartFridgeSystem : SharedSmartFridgeSystem
+{
+ [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
+
+ protected override void UpdateUI(Entity ent)
+ {
+ base.UpdateUI(ent);
+
+ if (!_uiSystem.TryGetOpenUi(ent.Owner, SmartFridgeUiKey.Key, out var bui))
+ return;
+
+ bui.Refresh();
+ }
+}
diff --git a/Content.Client/_DV/SmartFridge/SmartFridgeUISystem.cs b/Content.Client/_DV/SmartFridge/SmartFridgeUISystem.cs
deleted file mode 100644
index 32589dd5f7..0000000000
--- a/Content.Client/_DV/SmartFridge/SmartFridgeUISystem.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using Content.Shared._DV.SmartFridge;
-using Robust.Shared.Analyzers;
-
-namespace Content.Client._DV.SmartFridge;
-
-public sealed class SmartFridgeUISystem : EntitySystem
-{
- [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent(OnSmartFridgeAfterState);
- }
-
- private void OnSmartFridgeAfterState(Entity ent, ref AfterAutoHandleStateEvent args)
- {
- if (!_uiSystem.TryGetOpenUi(ent.Owner, SmartFridgeUiKey.Key, out var bui))
- return;
-
- bui.Refresh();
- }
-}
diff --git a/Content.IntegrationTests/Content.IntegrationTests.csproj b/Content.IntegrationTests/Content.IntegrationTests.csproj
index 2e922d2509..8c62806f74 100644
--- a/Content.IntegrationTests/Content.IntegrationTests.csproj
+++ b/Content.IntegrationTests/Content.IntegrationTests.csproj
@@ -1,12 +1,10 @@
-
- $(TargetFramework)
..\bin\Content.IntegrationTests\
- false
false
- 12
+ disable
+
@@ -17,12 +15,12 @@
-
-
-
-
-
+
+
+
+
+
diff --git a/Content.IntegrationTests/Tests/Atmos/AirtightTest.cs b/Content.IntegrationTests/Tests/Atmos/AirtightTest.cs
new file mode 100644
index 0000000000..63e4d9f2d2
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Atmos/AirtightTest.cs
@@ -0,0 +1,585 @@
+using System.Numerics;
+using Content.Server.Atmos.Components;
+using Content.Server.Atmos.EntitySystems;
+using Content.Shared.Atmos;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.IntegrationTests.Tests.Atmos;
+
+///
+/// Mega-testclass for testing and .
+///
+[TestOf(typeof(AirtightSystem))]
+[TestOf(typeof(AtmosphereSystem))]
+public sealed class AirtightTest : AtmosTest
+{
+ // Load the same DeltaPressure test because it's quite a useful testmap for testing airtightness.
+ protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml");
+
+ private readonly EntProtoId _wallProto = new("WallSolid");
+
+ private EntityUid _targetWall = EntityUid.Invalid;
+ private EntityUid _targetRotationEnt = EntityUid.Invalid;
+
+ #region Prototypes
+
+ [TestPrototypes]
+ private const string Prototypes = @"
+- type: entity
+ id: AirtightDirectionalRotationTest
+ parent: WindowDirectional
+ components:
+ - type: Airtight
+ airBlockedDirection: North
+ fixAirBlockedDirectionInitialize: true
+ noAirWhenFullyAirBlocked: false
+";
+
+ #endregion
+
+ #region Component and Helper Assertions
+
+ /*
+ Tests for asserting that proper ComponentInit and other events properly work.
+ */
+
+ [Test]
+ public async Task Component_InitDataCorrect()
+ {
+ // Ensure grid/atmos is initialized.
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ await Server.WaitPost(delegate
+ {
+ var coords = new EntityCoordinates(RelevantAtmos.Owner, Vector2.Zero);
+ _targetWall = SEntMan.SpawnAtPosition(_wallProto, coords);
+ });
+
+ SEntMan.TryGetComponent(_targetWall, out var airtightComp);
+ Assert.That(airtightComp, Is.Not.Null, "Expected spawned wall entity to have AirtightComponent.");
+
+ // The data on the component itself should reflect full blockage.
+ // It should also hold the proper last position.
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(airtightComp.AirBlockedDirection, Is.EqualTo(AtmosDirection.All));
+ Assert.That(airtightComp.LastPosition, Is.EqualTo((RelevantAtmos.Owner, Vector2i.Zero)));
+ }
+ }
+
+ [Test]
+ [TestCase(AtmosDirection.North)]
+ [TestCase(AtmosDirection.South)]
+ [TestCase(AtmosDirection.East)]
+ [TestCase(AtmosDirection.West)]
+ public async Task MultiTile_Component_InitDataCorrect(AtmosDirection direction)
+ {
+ // Ensure grid/atmos is initialized.
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ var offsetVec = Vector2i.Zero.Offset(direction);
+ await Server.WaitPost(delegate
+ {
+ var coords = new EntityCoordinates(RelevantAtmos.Owner, offsetVec);
+ _targetWall = SEntMan.SpawnAtPosition(_wallProto, coords);
+ });
+
+ SEntMan.TryGetComponent(_targetWall, out var airtightComp);
+ Assert.That(airtightComp, Is.Not.Null, "Expected spawned wall entity to have AirtightComponent.");
+
+ // The data on the component itself should reflect full blockage.
+ // It should also hold the proper last position.
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(airtightComp.AirBlockedDirection, Is.EqualTo(AtmosDirection.All));
+ Assert.That(airtightComp.LastPosition, Is.EqualTo((RelevantAtmos.Owner, offsetVec)));
+ }
+ }
+
+ #endregion
+
+ #region Single Tile Assertion
+
+ /*
+ Tests for asserting single tile airtightness state on both reconstructed and cached data.
+ These tests just spawn a wall in the center and make sure that both reconstructed and cached
+ airtight data reflect the expected states both immediately after the action and after an atmos tick.
+ */
+
+ ///
+ /// Tests that the reconstructed airtight map reflects properly when an airtight entity is spawned.
+ ///
+ [Test]
+ public async Task Spawn_ReconstructedUpdatesImmediately()
+ {
+ // Ensure grid/atmos is initialized.
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ // Before an entity is spawned, the tile in question should be completely unblocked.
+ // This should be reflected in a reconstruction.
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(
+ SAtmos.IsTileAirBlocked(ProcessEnt.Owner, Vector2i.Zero, mapGridComp: ProcessEnt.Comp3),
+ Is.False,
+ "Expected no airtightness for reconstructed AirtightData before spawning an airtight entity.");
+ }
+
+ // We cannot use the Spawn InteractionTest helper because it runs ticks,
+ // which invalidate testing for cached data (ticks would update the cache).
+ await Server.WaitPost(delegate
+ {
+ var coords = new EntityCoordinates(RelevantAtmos.Owner, Vector2.Zero);
+ _targetWall = SEntMan.SpawnAtPosition(_wallProto, coords);
+ });
+
+ // Now, immediately after spawn, the reconstructed data should reflect airtightness.
+ Assert.That(
+ SAtmos.IsTileAirBlocked(ProcessEnt.Owner, Vector2i.Zero, mapGridComp: ProcessEnt.Comp3),
+ Is.True,
+ "Expected airtightness for reconstructed AirtightData immediately after spawn.");
+ }
+
+ ///
+ /// Tests that the AirtightData cache updates properly when an airtight entity is spawned.
+ ///
+ [Test]
+ public async Task Spawn_CacheUpdatesOnAtmosTick()
+ {
+ // Ensure grid/atmos is initialized.
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ // Space should be blank before spawn.
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(
+ SAtmos.IsTileAirBlockedCached(RelevantAtmos, Vector2i.Zero),
+ Is.False,
+ "Expected cached AirtightData to be unblocked before spawning an airtight entity.");
+
+ var tile = RelevantAtmos.Comp.Tiles[Vector2i.Zero];
+ Assert.That(tile.AdjacentBits,
+ Is.EqualTo(AtmosDirection.All),
+ "Expected tile to be completely unblocked before spawning an airtight entity.");
+
+ Assert.That(tile.AirtightData.BlockedDirections,
+ Is.EqualTo(AtmosDirection.Invalid),
+ "Expected AirtightData to reflect non-airtight state before spawning an airtight entity.");
+
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ var direction = (AtmosDirection)(1 << i);
+ var curTile = tile.AdjacentTiles[i];
+ Assert.That(curTile, Is.Not.Null, $"Center tile does not hold expected reference to adjacent tile in direction {direction}.");
+ }
+ }
+
+ await Server.WaitPost(delegate
+ {
+ var coords = new EntityCoordinates(RelevantAtmos.Owner, Vector2.Zero);
+ _targetWall = SEntMan.SpawnAtPosition(_wallProto, coords);
+ });
+
+ // Now, immediately after spawn, the reconstructed data should reflect airtightness,
+ // but the cached data should still be stale.
+ // This goes the same for the references, which haven't been updated, as well as the AirtightData.
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(
+ SAtmos.IsTileAirBlockedCached(RelevantAtmos, Vector2i.Zero),
+ Is.False,
+ "Expected cached AirtightData to remain stale immediately after spawn before atmos tick.");
+
+ var tile = RelevantAtmos.Comp.Tiles[Vector2i.Zero];
+ Assert.That(tile.AdjacentBits,
+ Is.EqualTo(AtmosDirection.All),
+ "Expected tile to still show non-airtight state before an atmos tick.");
+
+ Assert.That(tile.AirtightData.BlockedDirections,
+ Is.EqualTo(AtmosDirection.Invalid),
+ "Expected AirtightData to reflect non-airtight state after spawn before an atmos tick.");
+
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ var direction = (AtmosDirection)(1 << i);
+ var curTile = tile.AdjacentTiles[i];
+ Assert.That(curTile, Is.Not.Null, $"Center tile does not hold expected reference to adjacent tile in direction {direction}.");
+ }
+ }
+
+ // Tick to update cache.
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(
+ SAtmos.IsTileAirBlocked(ProcessEnt.Owner, Vector2i.Zero, mapGridComp: ProcessEnt.Comp3),
+ Is.True,
+ "Expected airtightness for reconstructed AirtightData after atmos tick.");
+
+ Assert.That(
+ SAtmos.IsTileAirBlockedCached(RelevantAtmos, Vector2i.Zero),
+ Is.True,
+ "Expected cached AirtightData to reflect airtightness after atmos tick.");
+
+ var tile = RelevantAtmos.Comp.Tiles[Vector2i.Zero];
+ Assert.That(tile.AdjacentBits,
+ Is.EqualTo(AtmosDirection.Invalid),
+ "Expected tile to reflect airtight state after atmos tick.");
+
+ Assert.That(tile.AirtightData.BlockedDirections,
+ Is.EqualTo(AtmosDirection.All),
+ "Expected AirtightData to reflect airtight state after spawn before an atmos tick.");
+
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ var direction = (AtmosDirection)(1 << i);
+ var curTile = tile.AdjacentTiles[i];
+ Assert.That(curTile, Is.Null, $"Center tile holds unexpected reference to adjacent tile in direction {direction}.");
+ }
+ }
+ }
+
+ ///
+ /// Tests that an airtight reconstruction reflects properly after an entity is deleted.
+ ///
+ [Test]
+ public async Task Delete_ReconstructedUpdatesImmediately()
+ {
+ // Ensure grid/atmos is initialized.
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ await Server.WaitPost(delegate
+ {
+ var coords = new EntityCoordinates(RelevantAtmos.Owner, Vector2.Zero);
+ _targetWall = SEntMan.SpawnAtPosition(_wallProto, coords);
+ });
+
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ Assert.That(
+ SAtmos.IsTileAirBlocked(ProcessEnt.Owner, Vector2i.Zero, mapGridComp: ProcessEnt.Comp3),
+ Is.True,
+ "Expected airtightness for reconstructed AirtightData before deletion.");
+
+ await Server.WaitPost(delegate
+ {
+ SEntMan.DeleteEntity(_targetWall);
+ });
+
+ Assert.That(
+ SAtmos.IsTileAirBlocked(ProcessEnt.Owner, Vector2i.Zero, mapGridComp: ProcessEnt.Comp3),
+ Is.False,
+ "Expected no airtightness for reconstructed AirtightData immediately after deletion.");
+
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ Assert.That(
+ SAtmos.IsTileAirBlocked(ProcessEnt.Owner, Vector2i.Zero, mapGridComp: ProcessEnt.Comp3),
+ Is.False,
+ "Expected no airtightness for reconstructed AirtightData after atmos tick.");
+ }
+
+ ///
+ /// Tests that the cached airtight map reflects properly when an entity is deleted
+ ///
+ [Test]
+ public async Task Delete_CacheUpdatesOnAtmosTick()
+ {
+ // Ensure grid/atmos is initialized.
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ await Server.WaitPost(delegate
+ {
+ var coords = new EntityCoordinates(RelevantAtmos.Owner, Vector2.Zero);
+ _targetWall = SEntMan.SpawnAtPosition(_wallProto, coords);
+ });
+
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ await Server.WaitPost(delegate
+ {
+ SEntMan.DeleteEntity(_targetWall);
+ });
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(
+ SAtmos.IsTileAirBlockedCached(RelevantAtmos, Vector2i.Zero),
+ Is.True,
+ "Expected cached AirtightData to remain stale immediately after deletion before atmos tick.");
+
+ var tile = RelevantAtmos.Comp.Tiles[Vector2i.Zero];
+ Assert.That(tile.AdjacentBits,
+ Is.EqualTo(AtmosDirection.Invalid),
+ "Expected tile to still show airtight state before atmos tick after deletion.");
+
+ Assert.That(tile.AirtightData.BlockedDirections,
+ Is.EqualTo(AtmosDirection.All),
+ "Expected AirtightData to reflect non-airtight state before after deletion before an atmos tick.");
+
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ var direction = (AtmosDirection)(1 << i);
+ var curTile = tile.AdjacentTiles[i];
+ Assert.That(curTile, Is.Null, $"Center tile holds unexpected reference to adjacent tile in direction {direction}.");
+ }
+ }
+
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(
+ SAtmos.IsTileAirBlockedCached(RelevantAtmos, Vector2i.Zero),
+ Is.False,
+ "Expected cached AirtightData to reflect deletion after atmos tick.");
+
+ var tile = RelevantAtmos.Comp.Tiles[Vector2i.Zero];
+ Assert.That(tile.AdjacentBits,
+ Is.EqualTo(AtmosDirection.All),
+ "Expected tile to reflect non-airtight state after atmos tick.");
+
+ Assert.That(tile.AirtightData.BlockedDirections,
+ Is.EqualTo(AtmosDirection.Invalid),
+ "Expected AirtightData to reflect non-airtight state after atmos tick.");
+
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ var direction = (AtmosDirection)(1 << i);
+ var curTile = tile.AdjacentTiles[i];
+ Assert.That(curTile, Is.Not.Null, $"Center tile does not hold expected reference to adjacent tile in direction {direction}.");
+ }
+ }
+ }
+
+ #endregion
+
+ #region Multi-Tile Assertion
+
+ /*
+ Tests for asserting multi-tile airtightness state on cached data.
+ These tests spawn multiple entities and check that the center unblocked entity
+ properly reflects partial airtightness states.
+
+ Note that reconstruction won't save you in the case where you're surrounded by airtight entities,
+ as those don't show up in the reconstruction. Thus, only cached data tests are done here.
+ */
+
+ ///
+ /// Tests that the cached airtight map reflects properly when airtight entities are spawned
+ /// along the cardinal directions.
+ ///
+ /// The direction to spawn the airtight entity in.
+ [Test]
+ [TestCase(AtmosDirection.North)]
+ [TestCase(AtmosDirection.South)]
+ [TestCase(AtmosDirection.East)]
+ [TestCase(AtmosDirection.West)]
+ public async Task MultiTile_Spawn_CacheUpdatesOnAtmosTick(AtmosDirection atmosDirection)
+ {
+ // Ensure grid/atmos is initialized.
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ // Tile should be completely unblocked.
+ using (Assert.EnterMultipleScope())
+ {
+ var tile = RelevantAtmos.Comp.Tiles[Vector2i.Zero];
+ Assert.That(tile.AdjacentBits,
+ Is.EqualTo(AtmosDirection.All),
+ "Expected tile to be completely unblocked before spawning an airtight entity.");
+
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ var direction = (AtmosDirection)(1 << i);
+ var curTile = tile.AdjacentTiles[i];
+ Assert.That(curTile, Is.Not.Null, $"Center tile does not hold expected reference to adjacent tile in direction {direction}.");
+ }
+ }
+
+ await Server.WaitPost(delegate
+ {
+ var offsetVec = Vector2i.Zero.Offset(atmosDirection);
+ var coords = new EntityCoordinates(RelevantAtmos.Owner, offsetVec);
+ _targetWall = SEntMan.SpawnAtPosition(_wallProto, coords);
+ });
+
+ using (Assert.EnterMultipleScope())
+ {
+ var tile = RelevantAtmos.Comp.Tiles[Vector2i.Zero];
+ Assert.That(tile.AdjacentBits,
+ Is.EqualTo(AtmosDirection.All),
+ "Expected tile to still show non-airtight state before an atmos tick.");
+
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ var direction = (AtmosDirection)(1 << i);
+ var curTile = tile.AdjacentTiles[i];
+ Assert.That(curTile, Is.Not.Null, $"Center tile does not hold expected reference to adjacent tile in direction {direction}.");
+ }
+ }
+
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ using (Assert.EnterMultipleScope())
+ {
+ var tile = RelevantAtmos.Comp.Tiles[Vector2i.Zero];
+ Assert.That(tile.AdjacentBits,
+ Is.EqualTo(AtmosDirection.All & ~atmosDirection),
+ "Expected tile to reflect airtight state after atmos tick.");
+
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ var direction = (AtmosDirection)(1 << i);
+ var curTile = tile.AdjacentTiles[i];
+ if (direction == atmosDirection)
+ {
+ Assert.That(curTile, Is.Null, $"Center tile holds unexpected reference to adjacent tile in direction {direction}.");
+ }
+ else
+ {
+ Assert.That(curTile, Is.Not.Null, $"Center tile does not hold expected reference to adjacent tile in direction {direction}.");
+ }
+ }
+ }
+ }
+
+ ///
+ /// Tests that the cached airtight map reflects properly when an airtight entity is deleted
+ /// along a cardinal direction.
+ ///
+ /// The direction the airtight entity is spawned and then deleted in.
+ [Test]
+ [TestCase(AtmosDirection.North)]
+ [TestCase(AtmosDirection.South)]
+ [TestCase(AtmosDirection.East)]
+ [TestCase(AtmosDirection.West)]
+ public async Task MultiTile_Delete_CacheUpdatesOnAtmosTick(AtmosDirection atmosDirection)
+ {
+ // Ensure grid/atmos is initialized.
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ await Server.WaitPost(delegate
+ {
+ var offsetVec = Vector2i.Zero.Offset(atmosDirection);
+ var coords = new EntityCoordinates(RelevantAtmos.Owner, offsetVec);
+ _targetWall = SEntMan.SpawnAtPosition(_wallProto, coords);
+ });
+
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ await Server.WaitPost(delegate
+ {
+ SEntMan.DeleteEntity(_targetWall);
+ });
+
+ using (Assert.EnterMultipleScope())
+ {
+ var tile = RelevantAtmos.Comp.Tiles[Vector2i.Zero];
+ Assert.That(tile.AdjacentBits,
+ Is.EqualTo(AtmosDirection.All & ~atmosDirection),
+ "Expected tile to remain stale immediately after deletion before an atmos tick.");
+
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ var direction = (AtmosDirection)(1 << i);
+ var curTile = tile.AdjacentTiles[i];
+ if (direction == atmosDirection)
+ {
+ Assert.That(curTile, Is.Null, $"Center tile holds unexpected reference to adjacent tile in direction {direction}.");
+ }
+ else
+ {
+ Assert.That(curTile, Is.Not.Null, $"Center tile does not hold expected reference to adjacent tile in direction {direction}.");
+ }
+ }
+ }
+
+ // Tick to update cache after deletion.
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ using (Assert.EnterMultipleScope())
+ {
+ var tile = RelevantAtmos.Comp.Tiles[Vector2i.Zero];
+ Assert.That(tile.AdjacentBits,
+ Is.EqualTo(AtmosDirection.All),
+ "Expected tile to reflect non-airtight state after deletion after atmos tick.");
+
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ var direction = (AtmosDirection)(1 << i);
+ var curTile = tile.AdjacentTiles[i];
+ Assert.That(curTile, Is.Not.Null, $"Center tile does not hold expected reference to adjacent tile in direction {direction}.");
+ }
+ }
+ }
+
+ #endregion
+
+ #region Rotation Assertion
+
+ ///
+ /// Asserts that an airtight entity with a directional air blocked direction
+ /// properly reflects rotation on spawn.
+ ///
+ /// The degrees to rotate the entity on spawn.
+ /// The expected blocked direction after rotation.
+ /// Yeah, so here I learned that RT handles rotation directions
+ /// as positive == counterclockwise.
+ [Test]
+ [TestCase(0f, AtmosDirection.North)]
+ [TestCase(90f, AtmosDirection.West)]
+ [TestCase(180f, AtmosDirection.South)]
+ [TestCase(270f, AtmosDirection.East)]
+ [TestCase(-90f, AtmosDirection.East)]
+ [TestCase(-180f, AtmosDirection.South)]
+ [TestCase(-270f, AtmosDirection.West)]
+ public async Task Rotation_AirBlockedDirectionsOnSpawn(float degrees, AtmosDirection expected)
+ {
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ var rotation = Angle.FromDegrees(degrees);
+
+ await Server.WaitPost(delegate
+ {
+ var coords = new EntityCoordinates(RelevantAtmos.Owner, Vector2.Zero);
+ _targetRotationEnt = SEntMan.SpawnAtPosition("AirtightDirectionalRotationTest", coords);
+
+ Transform.SetLocalRotation(_targetRotationEnt, rotation);
+ });
+
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ await Server.WaitAssertion(delegate
+ {
+ using (Assert.EnterMultipleScope())
+ {
+ SEntMan.TryGetComponent(_targetRotationEnt, out var airtight);
+ Assert.That(airtight, Is.Not.Null);
+
+ var initial = (AtmosDirection)airtight.InitialAirBlockedDirection;
+ Assert.That(initial,
+ Is.EqualTo(AtmosDirection.North),
+ "Directional airtight entity should block North on spawn.");
+
+ Assert.That(airtight.AirBlockedDirection,
+ Is.EqualTo(expected),
+ $"Expected AirBlockedDirection to be {expected} after rotating by {degrees} degrees on spawn.");
+
+ // i dont trust you airtightsystem
+ if (degrees is 90f or 270f)
+ {
+ Assert.That(expected,
+ Is.Not.EqualTo(initial),
+ "Rotated directions should differ for 90/270 degrees.");
+ }
+ }
+ });
+ }
+
+ #endregion
+}
diff --git a/Content.IntegrationTests/Tests/Atmos/AtmosMonitoringTest.cs b/Content.IntegrationTests/Tests/Atmos/AtmosMonitoringTest.cs
new file mode 100644
index 0000000000..83d7b9be53
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Atmos/AtmosMonitoringTest.cs
@@ -0,0 +1,130 @@
+using System.Numerics;
+using Content.Server.Atmos.Monitor.Components;
+using Content.Shared.Atmos;
+using Robust.Shared.Console;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.IntegrationTests.Tests.Atmos;
+
+///
+/// Test for determining that an AtmosMonitoringComponent/System correctly references
+/// the GasMixture of the tile it is on if the tile's GasMixture ever changes.
+///
+[TestOf(typeof(Atmospherics))]
+public sealed class AtmosMonitoringTest : AtmosTest
+{
+ // We can just reuse the dP test, I just want a grid.
+ protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml");
+
+ private readonly EntProtoId _airSensorProto = new("AirSensor");
+ private readonly EntProtoId _wallProto = new("WallSolid");
+
+ ///
+ /// Tests if the monitor properly nulls out its reference to the tile mixture
+ /// when a wall is placed on top of it, and restores the reference when the wall is removed.
+ ///
+ [Test]
+ public async Task NullOutTileAtmosphereGasMixture()
+ {
+ // run an atmos update to initialize everything For Real surely
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ var gridNetEnt = SEntMan.GetNetEntity(RelevantAtmos.Owner);
+ TargetCoords = new NetCoordinates(gridNetEnt, Vector2.Zero);
+ var netEnt = await Spawn(_airSensorProto);
+ var airSensorUid = SEntMan.GetEntity(netEnt);
+ Transform.TryGetGridTilePosition(airSensorUid, out var vec);
+
+ // run another one to ensure that the ref to the GasMixture was picked up
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ // should be in the middle
+ Assert.That(vec,
+ Is.EqualTo(Vector2i.Zero),
+ "Air sensor not in expected position on grid (0, 0)");
+
+ var atmosMonitor = SEntMan.GetComponent(airSensorUid);
+ var tileMixture = SAtmos.GetTileMixture(airSensorUid);
+
+ Assert.That(tileMixture,
+ Is.SameAs(atmosMonitor.TileGas),
+ "Atmos monitor's TileGas does not match actual tile mixture after spawn.");
+
+ // ok now spawn a wall or something on top of it
+ var wall = await Spawn(_wallProto);
+ var wallUid = SEntMan.GetEntity(wall);
+
+ // ensure that atmospherics registers the change - the gas mixture should no longer exist
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ // the monitor's ref to the gas should be null now
+ Assert.That(atmosMonitor.TileGas,
+ Is.Null,
+ "Atmos monitor's TileGas is not null after wall placed on top. Possible dead reference.");
+ // the actual mixture on the tile should be null now too
+ var nullTileMixture = SAtmos.GetTileMixture(airSensorUid);
+ Assert.That(nullTileMixture, Is.Null, "Tile mixture is not null after wall placed on top.");
+
+ // ok now delete the wall
+ await Delete(wallUid);
+
+ // ensure that atmospherics registers the change - the gas mixture should be back
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ // gas mixture should now exist again
+ var newTileMixture = SAtmos.GetTileMixture(airSensorUid);
+ Assert.That(newTileMixture, Is.Not.Null, "Tile mixture is null after wall removed.");
+ // monitor's ref to the gas should be back too
+ Assert.That(atmosMonitor.TileGas,
+ Is.SameAs(newTileMixture),
+ "Atmos monitor's TileGas does not match actual tile mixture after wall removed.");
+ }
+
+ ///
+ /// Tests if the monitor properly updates its reference to the tile mixture
+ /// when the FixGridAtmos command is called.
+ ///
+ [Test]
+ public async Task FixGridAtmosReplaceMixtureOnTileChange()
+ {
+ // run an atmos update to initialize everything For Real surely
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ var gridNetEnt = SEntMan.GetNetEntity(RelevantAtmos.Owner);
+ TargetCoords = new NetCoordinates(gridNetEnt, Vector2.Zero);
+ var netEnt = await Spawn(_airSensorProto);
+ var airSensorUid = SEntMan.GetEntity(netEnt);
+ Transform.TryGetGridTilePosition(airSensorUid, out var vec);
+
+ // run another one to ensure that the ref to the GasMixture was picked up
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+
+ // should be in the middle
+ Assert.That(vec,
+ Is.EqualTo(Vector2i.Zero),
+ "Air sensor not in expected position on grid (0, 0)");
+
+ var atmosMonitor = SEntMan.GetComponent(airSensorUid);
+ var tileMixture = SAtmos.GetTileMixture(airSensorUid);
+
+ Assert.That(tileMixture,
+ Is.SameAs(atmosMonitor.TileGas),
+ "Atmos monitor's TileGas does not match actual tile mixture after spawn.");
+
+ SAtmos.RebuildGridAtmosphere((ProcessEnt.Owner, ProcessEnt.Comp1, ProcessEnt.Comp3));
+
+ // EXTREMELY IMPORTANT: The reference to the tile mixture on the tile should be completely different.
+ var newTileMixture = SAtmos.GetTileMixture(airSensorUid);
+ Assert.That(newTileMixture,
+ Is.Not.SameAs(tileMixture),
+ "Tile mixture is the same instance after fixgridatmos was ran. It should be a new instance.");
+
+ // The monitor's ref to the tile mixture should have updated too.
+ Assert.That(atmosMonitor.TileGas,
+ Is.SameAs(newTileMixture),
+ "Atmos monitor's TileGas does not match actual tile mixture after fixgridatmos was ran.");
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Atmos/AtmosTest.cs b/Content.IntegrationTests/Tests/Atmos/AtmosTest.cs
new file mode 100644
index 0000000000..a956d0cbab
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Atmos/AtmosTest.cs
@@ -0,0 +1,125 @@
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Server.Atmos.Components;
+using Content.Server.Atmos.EntitySystems;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Tests;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Maths;
+
+namespace Content.IntegrationTests.Tests.Atmos;
+
+///
+/// Helper class for atmospherics tests.
+/// See on how to add new tests with custom maps.
+///
+[TestFixture]
+public abstract class AtmosTest : InteractionTest
+{
+ protected AtmosphereSystem SAtmos = default!;
+ protected Content.Client.Atmos.EntitySystems.AtmosphereSystem CAtmos = default!;
+ protected EntityLookupSystem LookupSystem = default!;
+
+ protected Entity RelevantAtmos;
+
+ ///
+ /// Used in . Resolved during test setup.
+ ///
+ protected Entity ProcessEnt = default;
+
+ protected virtual float Moles => 1000.0f;
+
+ // 5% is a lot, but it can get this bad ATM...
+ protected virtual float Tolerance => 0.05f;
+
+ [SetUp]
+ public override async Task Setup()
+ {
+ await base.Setup();
+
+ SAtmos = SEntMan.System();
+ CAtmos = CEntMan.System();
+ LookupSystem = SEntMan.System();
+
+ SEntMan.TryGetComponent(MapData.Grid, out var gridAtmosComp);
+ SEntMan.TryGetComponent(MapData.Grid, out var overlayComp);
+ SEntMan.TryGetComponent(MapData.Grid, out var mapGridComp);
+ var xform = SEntMan.GetComponent(MapData.Grid);
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(gridAtmosComp,
+ Is.Not.Null,
+ "Loaded map doesn't have a GridAtmosphereComponent on its grid. " +
+ "Did you forget to override TestMapPath with a proper atmospherics testing map?");
+ Assert.That(overlayComp,
+ Is.Not.Null,
+ "Loaded map doesn't have a GasTileOverlayComponent on its grid. " +
+ "Did you forget to override TestMapPath with a proper atmospherics testing map?");
+ Assert.That(mapGridComp,
+ Is.Not.Null,
+ "Loaded map doesn't have a MapGridComponent on its grid. " +
+ "Did you forget to override TestMapPath with a proper atmospherics testing map?");
+ }
+
+ RelevantAtmos = (MapData.Grid, gridAtmosComp);
+
+ ProcessEnt = new Entity(
+ MapData.Grid.Owner,
+ gridAtmosComp,
+ overlayComp,
+ mapGridComp,
+ xform);
+ }
+
+ ///
+ /// Tries to get a mapped marker with a given name.
+ ///
+ /// Marker entities to look through
+ /// Marker name to look up (set during mapping)
+ /// Found marker EntityUid or Invalid
+ /// True if found
+ protected static bool GetMarker(Entity[] markers, string id, out EntityUid marker)
+ {
+ foreach (var ent in markers)
+ {
+ if (ent.Comp.Id == id)
+ {
+ marker = ent;
+ return true;
+ }
+ }
+ marker = EntityUid.Invalid;
+ return false;
+ }
+
+ protected static float GetGridMoles(Entity grid)
+ {
+ var moles = 0.0f;
+ foreach (var tile in grid.Comp.Tiles.Values)
+ {
+ moles += tile.Air?.TotalMoles ?? 0.0f;
+ }
+
+ return moles;
+ }
+
+ ///
+ /// Asserts that test grid has this many moles, within tolerance percentage.
+ ///
+ protected void AssertGridMoles(float moles, float tolerance)
+ {
+ var gridMoles = GetGridMoles(RelevantAtmos);
+ Assert.That(MathHelper.CloseToPercent(moles, gridMoles, tolerance), $"Grid has {gridMoles} moles, but {moles} was expected");
+ }
+
+ ///
+ /// Asserts that provided GasMixtures have same total moles, within tolerance percentage.
+ ///
+ protected void AssertMixMoles(GasMixture mix1, GasMixture mix2, float tolerance)
+ {
+ Assert.That(MathHelper.CloseToPercent(mix1.TotalMoles, mix2.TotalMoles, tolerance),
+ $"GasMixtures do not match. Got {mix1.TotalMoles} and {mix2.TotalMoles} moles");
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs b/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs
index c3b3877c98..d4283568e8 100644
--- a/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs
+++ b/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs
@@ -1,15 +1,11 @@
-using System.Linq;
using System.Numerics;
using Content.Server.Atmos;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
-using Robust.Shared.EntitySerialization;
-using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
namespace Content.IntegrationTests.Tests.Atmos;
@@ -20,7 +16,7 @@ namespace Content.IntegrationTests.Tests.Atmos;
///
[TestFixture]
[TestOf(typeof(DeltaPressureSystem))]
-public sealed class DeltaPressureTest
+public sealed class DeltaPressureTest : AtmosTest
{
#region Prototypes
@@ -92,7 +88,7 @@ public sealed class DeltaPressureTest
#endregion
- private readonly ResPath _testMap = new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml");
+ protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml");
// TODO ATMOS TESTS
// - Check for directional windows (partial airtight ents) properly computing pressure differences
@@ -111,40 +107,15 @@ public sealed class DeltaPressureTest
[Test]
public async Task ProcessingListAutoJoinTest()
{
- await using var pair = await PoolManager.GetServerClient();
- var server = pair.Server;
-
- var entMan = server.EntMan;
- var mapLoader = entMan.System();
- var atmosphereSystem = entMan.System();
- var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
-
- Entity grid = default;
- Entity dpEnt;
-
- // Load our test map in and assert that it exists.
- await server.WaitPost(() =>
+ await Server.WaitAssertion(() =>
{
-#pragma warning disable NUnit2045
- Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
- $"Failed to load map {_testMap}.");
- Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
-#pragma warning restore NUnit2045
+ var uid = SEntMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(ProcessEnt, Vector2.Zero));
+ var dpEnt = new Entity(uid, SEntMan.GetComponent(uid));
- grid = gridSet.First();
+ Assert.That(SAtmos.IsDeltaPressureEntityInList(RelevantAtmos, dpEnt), "Entity was not in processing list when it should have automatically joined!");
+ SEntMan.DeleteEntity(uid);
+ Assert.That(!SAtmos.IsDeltaPressureEntityInList(RelevantAtmos, dpEnt), "Entity was still in processing list after deletion!");
});
-
- await server.WaitAssertion(() =>
- {
- var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
- dpEnt = new Entity(uid, entMan.GetComponent(uid));
-
- Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have automatically joined!");
- entMan.DeleteEntity(uid);
- Assert.That(!atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was still in processing list after deletion!");
- });
-
- await pair.CleanReturnAsync();
}
///
@@ -154,45 +125,27 @@ public sealed class DeltaPressureTest
[Test]
public async Task ProcessingDeltaStandbyTest()
{
- await using var pair = await PoolManager.GetServerClient();
- var server = pair.Server;
-
- var entMan = server.EntMan;
- var mapLoader = entMan.System();
- var atmosphereSystem = entMan.System();
- var transformSystem = entMan.System();
- var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
-
- Entity grid = default;
Entity dpEnt = default;
TileAtmosphere tile = null!;
AtmosDirection direction = default;
// Load our test map in and assert that it exists.
- await server.WaitPost(() =>
+ await Server.WaitPost(() =>
{
-#pragma warning disable NUnit2045
- Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
- $"Failed to load map {_testMap}.");
- Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
-#pragma warning restore NUnit2045
-
- grid = gridSet.First();
- var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
- dpEnt = new Entity(uid, entMan.GetComponent(uid));
- Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
+ var uid = SEntMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(ProcessEnt, Vector2.Zero));
+ dpEnt = new Entity(uid, SEntMan.GetComponent(uid));
+ Assert.That(SAtmos.IsDeltaPressureEntityInList(ProcessEnt, dpEnt), "Entity was not in processing list when it should have been added!");
});
for (var i = 0; i < Atmospherics.Directions; i++)
{
- await server.WaitPost(() =>
+ await Server.WaitPost(() =>
{
- var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
- var gridAtmosComp = entMan.GetComponent(grid);
+ var indices = Transform.GetGridOrMapTilePosition(dpEnt);
direction = (AtmosDirection)(1 << i);
var offsetIndices = indices.Offset(direction);
- tile = gridAtmosComp.Tiles[offsetIndices];
+ tile = RelevantAtmos.Comp.Tiles[offsetIndices];
Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
@@ -202,19 +155,17 @@ public sealed class DeltaPressureTest
tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
});
- await server.WaitRunTicks(30);
+ await Server.WaitRunTicks(30);
// Entity should exist, if it took one tick of damage then it should be instantly destroyed.
- await server.WaitAssertion(() =>
+ await Server.WaitAssertion(() =>
{
- Assert.That(!entMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold pressure from {direction} side!");
+ Assert.That(!SEntMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold pressure from {direction} side!");
tile.Air!.Clear();
});
- await server.WaitRunTicks(30);
+ await Server.WaitRunTicks(30);
}
-
- await pair.CleanReturnAsync();
}
///
@@ -224,47 +175,43 @@ public sealed class DeltaPressureTest
[Test]
public async Task ProcessingDeltaDamageTest()
{
- await using var pair = await PoolManager.GetServerClient();
- var server = pair.Server;
-
- var entMan = server.EntMan;
- var mapLoader = entMan.System();
- var atmosphereSystem = entMan.System();
- var transformSystem = entMan.System();
- var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
-
- Entity grid = default;
Entity dpEnt = default;
- TileAtmosphere tile = null!;
AtmosDirection direction = default;
// Load our test map in and assert that it exists.
- await server.WaitPost(() =>
+ await Server.WaitPost(() =>
{
-#pragma warning disable NUnit2045
- Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
- $"Failed to load map {_testMap}.");
- Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
-#pragma warning restore NUnit2045
-
- grid = gridSet.First();
+ SAtmos.SetAtmosphereSimulation(ProcessEnt, false);
});
for (var i = 0; i < Atmospherics.Directions; i++)
{
- await server.WaitPost(() =>
+ /*
+ RUNNING REGULAR TICKS USING WaitRunTicks AND GUESSING AS TO HOW MANY ATMOS SIMULATION TICKS ARE HAPPENING
+ WILL CAUSE A RACE CONDITION THAT IS A PAIN IN THE ASS TO DEBUG
+
+ AN ENTITY MAY BE REMOVED AND ADDED BETWEEN A SUBTICK. IF LINDA PROCESSING IS ENABLED IT MIGHT CAUSE
+ AN EQUALIZATION TO PUT AIR IN OTHER TILES IN THE SMALL WINDOW WHERE THE TILE IS NOT AIRTIGHT
+ WHICH WILL THROW OFF DELTAS
+ */
+
+ await Server.WaitPost(() =>
+ {
+ SAtmos.RunProcessingFull(ProcessEnt,ProcessEnt.Owner, SAtmos.AtmosTickRate);
+ });
+
+ await Server.WaitPost(() =>
{
// Need to spawn an entity each run to ensure it works for all directions.
- var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
- dpEnt = new Entity(uid, entMan.GetComponent(uid));
- Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
+ var uid = SEntMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(ProcessEnt.Owner, Vector2.Zero));
+ dpEnt = new Entity(uid, SEntMan.GetComponent(uid));
+ Assert.That(SAtmos.IsDeltaPressureEntityInList(ProcessEnt.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
- var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
- var gridAtmosComp = entMan.GetComponent(grid);
+ var indices = Transform.GetGridOrMapTilePosition(dpEnt);
direction = (AtmosDirection)(1 << i);
var offsetIndices = indices.Offset(direction);
- tile = gridAtmosComp.Tiles[offsetIndices];
+ var tile = ProcessEnt.Comp1.Tiles[offsetIndices];
Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
@@ -274,19 +221,29 @@ public sealed class DeltaPressureTest
tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
});
- await server.WaitRunTicks(30);
-
- // Entity should exist, if it took one tick of damage then it should be instantly destroyed.
- await server.WaitAssertion(() =>
+ // get jiggy with it! hit that dance white boy!
+ await Server.WaitPost(() =>
{
- Assert.That(entMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold pressure from {direction} side!");
- tile.Air!.Clear();
+ SAtmos.RunProcessingFull(ProcessEnt,ProcessEnt.Owner, SAtmos.AtmosTickRate);
});
- await server.WaitRunTicks(30);
- }
+ // need to run some ticks as deleted entities are queued for removal
+ // and not removed instantly
+ await Server.WaitRunTicks(30);
- await pair.CleanReturnAsync();
+ // Entity shouldn't exist, if it took one tick of damage then it should be instantly destroyed.
+ await Server.WaitAssertion(() =>
+ {
+ Assert.That(SEntMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold pressure from {direction} side!");
+
+ // Double whammy: in case any unintended gas leak occured due to a race condition,
+ // clear out all the tiles.
+ foreach (var mix in SAtmos.GetAllMixtures(ProcessEnt))
+ {
+ mix.Clear();
+ }
+ });
+ }
}
///
@@ -296,39 +253,23 @@ public sealed class DeltaPressureTest
[Test]
public async Task ProcessingAbsoluteStandbyTest()
{
- await using var pair = await PoolManager.GetServerClient();
- var server = pair.Server;
-
- var entMan = server.EntMan;
- var mapLoader = entMan.System();
- var atmosphereSystem = entMan.System();
- var transformSystem = entMan.System();
- var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
-
- Entity grid = default;
Entity dpEnt = default;
TileAtmosphere tile = null!;
AtmosDirection direction = default;
- await server.WaitPost(() =>
+ await Server.WaitPost(() =>
{
-#pragma warning disable NUnit2045
- Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
- $"Failed to load map {_testMap}.");
- Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
-#pragma warning restore NUnit2045
- grid = gridSet.First();
- var uid = entMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(grid.Owner, Vector2.Zero));
- dpEnt = new Entity(uid, entMan.GetComponent(uid));
- Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
+ var uid = SEntMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(ProcessEnt.Owner, Vector2.Zero));
+ dpEnt = new Entity(uid, SEntMan.GetComponent(uid));
+ Assert.That(SAtmos.IsDeltaPressureEntityInList(ProcessEnt.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
});
for (var i = 0; i < Atmospherics.Directions; i++)
{
- await server.WaitPost(() =>
+ await Server.WaitPost(() =>
{
- var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
- var gridAtmosComp = entMan.GetComponent(grid);
+ var indices = Transform.GetGridOrMapTilePosition(dpEnt);
+ var gridAtmosComp = SEntMan.GetComponent(ProcessEnt);
direction = (AtmosDirection)(1 << i);
var offsetIndices = indices.Offset(direction);
@@ -340,18 +281,16 @@ public sealed class DeltaPressureTest
tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
});
- await server.WaitRunTicks(30);
+ await Server.WaitRunTicks(30);
- await server.WaitAssertion(() =>
+ await Server.WaitAssertion(() =>
{
- Assert.That(!entMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold absolute pressure from {direction} side!");
+ Assert.That(!SEntMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold absolute pressure from {direction} side!");
tile.Air!.Clear();
});
- await server.WaitRunTicks(30);
+ await Server.WaitRunTicks(30);
}
-
- await pair.CleanReturnAsync();
}
///
@@ -361,41 +300,21 @@ public sealed class DeltaPressureTest
[Test]
public async Task ProcessingAbsoluteDamageTest()
{
- await using var pair = await PoolManager.GetServerClient();
- var server = pair.Server;
-
- var entMan = server.EntMan;
- var mapLoader = entMan.System();
- var atmosphereSystem = entMan.System();
- var transformSystem = entMan.System();
- var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
-
- Entity grid = default;
Entity dpEnt = default;
TileAtmosphere tile = null!;
AtmosDirection direction = default;
- await server.WaitPost(() =>
- {
-#pragma warning disable NUnit2045
- Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
- $"Failed to load map {_testMap}.");
- Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
-#pragma warning restore NUnit2045
- grid = gridSet.First();
- });
-
for (var i = 0; i < Atmospherics.Directions; i++)
{
- await server.WaitPost(() =>
+ await Server.WaitPost(() =>
{
// Spawn fresh entity each iteration to verify all directions work
- var uid = entMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(grid.Owner, Vector2.Zero));
- dpEnt = new Entity(uid, entMan.GetComponent(uid));
- Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
+ var uid = SEntMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(ProcessEnt.Owner, Vector2.Zero));
+ dpEnt = new Entity(uid, SEntMan.GetComponent(uid));
+ Assert.That(SAtmos.IsDeltaPressureEntityInList(ProcessEnt.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
- var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
- var gridAtmosComp = entMan.GetComponent(grid);
+ var indices = Transform.GetGridOrMapTilePosition(dpEnt);
+ var gridAtmosComp = SEntMan.GetComponent(ProcessEnt);
direction = (AtmosDirection)(1 << i);
var offsetIndices = indices.Offset(direction);
@@ -408,17 +327,15 @@ public sealed class DeltaPressureTest
tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
});
- await server.WaitRunTicks(30);
+ await Server.WaitRunTicks(30);
- await server.WaitAssertion(() =>
+ await Server.WaitAssertion(() =>
{
- Assert.That(entMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold absolute pressure from {direction} side!");
+ Assert.That(SEntMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold absolute pressure from {direction} side!");
tile.Air!.Clear();
});
- await server.WaitRunTicks(30);
+ await Server.WaitRunTicks(30);
}
-
- await pair.CleanReturnAsync();
}
}
diff --git a/Content.IntegrationTests/Tests/Atmos/RoomSpacingTest.cs b/Content.IntegrationTests/Tests/Atmos/RoomSpacingTest.cs
new file mode 100644
index 0000000000..4cdc0894ca
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Atmos/RoomSpacingTest.cs
@@ -0,0 +1,127 @@
+using Content.Shared.Atmos;
+using Content.Shared.Coordinates;
+using Content.Shared.Tests;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Utility;
+
+namespace Content.IntegrationTests.Tests.Atmos;
+
+public sealed class RoomSpacingTest : AtmosTest
+{
+ protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/tile_atmosphere_test_room.yml");
+
+ ///
+ /// Checks that deleting an outer wall spaces the room.
+ ///
+ [Test]
+ public async Task DeleteWall()
+ {
+ var markers = SEntMan.AllEntities();
+
+ EntityUid source, floor, wallPos;
+ source = floor = wallPos = EntityUid.Invalid;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(GetMarker(markers, "source", out source));
+ Assert.That(GetMarker(markers, "floor", out floor));
+ Assert.That(GetMarker(markers, "wall", out wallPos));
+ });
+
+ var lookup = LookupSystem.GetEntitiesIntersecting(wallPos);
+ var wall = lookup.FirstOrNull();
+ Assert.That(wall, Is.Not.Null);
+
+ Assert.That(GetGridMoles(RelevantAtmos), Is.EqualTo(0));
+
+ var sourceMix = SAtmos.GetTileMixture(source, true);
+ Assert.That(sourceMix, Is.Not.EqualTo(null));
+ sourceMix.AdjustMoles(Gas.Frezon, Moles);
+
+ await Server.WaitRunTicks(500);
+
+ var mix1 = SAtmos.GetTileMixture(floor);
+ Assert.That(mix1, Is.Not.EqualTo(null));
+
+ AssertMixMoles(sourceMix, mix1, Tolerance);
+ AssertGridMoles(Moles, Tolerance);
+
+ // Space the room
+ await Server.WaitAssertion(() =>
+ {
+ SEntMan.DeleteEntity(wall);
+ });
+
+ await Server.WaitRunTicks(10);
+
+ await Server.WaitPost(() =>
+ {
+ for (var i = 0; i < 50; i++)
+ {
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+ }
+ });
+
+ AssertMixMoles(sourceMix, mix1, Tolerance);
+ AssertGridMoles(0, Tolerance);
+ }
+
+ ///
+ /// Checks that exposing tile lattice spaces the room.
+ ///
+ [Test]
+ public async Task PryLattice()
+ {
+ var markers = SEntMan.AllEntities();
+
+ EntityUid source, floor, wallPos;
+ source = floor = wallPos = EntityUid.Invalid;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(GetMarker(markers, "source", out source));
+ Assert.That(GetMarker(markers, "floor", out floor));
+ Assert.That(GetMarker(markers, "wall", out wallPos));
+ });
+
+ var lookup = LookupSystem.GetEntitiesIntersecting(wallPos);
+ var wall = lookup.FirstOrNull();
+ Assert.That(wall, Is.Not.Null);
+
+ Assert.That(GetGridMoles(RelevantAtmos), Is.EqualTo(0));
+
+ var sourceMix = SAtmos.GetTileMixture(source, true);
+ Assert.That(sourceMix, Is.Not.EqualTo(null));
+ sourceMix.AdjustMoles(Gas.Frezon, Moles);
+
+ await Server.WaitPost(() =>
+ {
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+ });
+
+ var mix1 = SAtmos.GetTileMixture(floor);
+ Assert.That(mix1, Is.Not.EqualTo(null));
+
+ AssertMixMoles(sourceMix, mix1, Tolerance);
+ AssertGridMoles(Moles, Tolerance);
+
+ // Space the room
+ await SetTile(Lattice, SEntMan.GetNetCoordinates(floor.ToCoordinates()), MapData.Grid);
+
+ await Server.WaitRunTicks(10);
+
+ await Server.WaitPost(() =>
+ {
+ for (var i = 0; i < 50; i++)
+ {
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+ }
+ });
+
+ mix1 = SAtmos.GetTileMixture(floor);
+ Assert.That(mix1, Is.Not.EqualTo(null));
+
+ AssertMixMoles(sourceMix, mix1, Tolerance);
+ AssertGridMoles(0, Tolerance);
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Atmos/SharedGasSpecificHeatsTest.cs b/Content.IntegrationTests/Tests/Atmos/SharedGasSpecificHeatsTest.cs
new file mode 100644
index 0000000000..ac80f4a105
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Atmos/SharedGasSpecificHeatsTest.cs
@@ -0,0 +1,281 @@
+using Content.Client.Atmos.EntitySystems;
+using Content.IntegrationTests.Pair;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.EntitySystems;
+using Content.Shared.CCVar;
+using Robust.Shared.Configuration;
+using Robust.Shared.GameObjects;
+using Robust.UnitTesting;
+
+namespace Content.IntegrationTests.Tests.Atmos;
+
+///
+/// Tests for asserting that various gas specific heat operations agree with each other and do not deviate
+/// across client and server.
+///
+[TestOf(nameof(SharedAtmosphereSystem)), FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
+public sealed class SharedGasSpecificHeatsTest
+{
+ private IConfigurationManager _sConfig;
+ private IConfigurationManager _cConfig;
+
+ private TestPair _pair = default!;
+
+ private RobustIntegrationTest.ServerIntegrationInstance Server => _pair.Server;
+ private RobustIntegrationTest.ClientIntegrationInstance Client => _pair.Client;
+
+ private IEntityManager _sEntMan = default!;
+ private Content.Server.Atmos.EntitySystems.AtmosphereSystem _sAtmos = default!;
+
+ private IEntityManager _cEntMan = default!;
+ private AtmosphereSystem _cAtmos = default!;
+
+ [SetUp]
+ public async Task SetUp()
+ {
+ var poolSettings = new PoolSettings
+ {
+ Connected = true,
+ };
+ _pair = await PoolManager.GetServerClient(poolSettings);
+
+ _sEntMan = Server.ResolveDependency();
+ _cEntMan = Client.ResolveDependency();
+
+ _sAtmos = _sEntMan.System();
+ _cAtmos = _cEntMan.System();
+ }
+
+ [TearDown]
+ public async Task TearDown()
+ {
+ await _pair.CleanReturnAsync();
+ }
+
+ ///
+ /// Asserts that the cached gas specific heat arrays agree with each other.
+ ///
+ [Test]
+ public async Task GasSpecificHeats_Agree()
+ {
+ var serverSpecificHeats = Array.Empty();
+ var clientSpecificHeats = Array.Empty();
+ await Server.WaitPost(delegate
+ {
+ serverSpecificHeats = _sAtmos.GasSpecificHeats;
+ });
+
+ await Client.WaitPost(delegate
+ {
+ clientSpecificHeats = _cAtmos.GasSpecificHeats;
+ });
+
+ Assert.That(serverSpecificHeats,
+ Is.EqualTo(clientSpecificHeats),
+ "Server and client gas specific heat arrays do not agree.");
+ }
+
+ ///
+ /// Asserts that heat capacity calculations agree for the same gas mixture.
+ ///
+ [Test]
+ public async Task HeatCapacity_Agree()
+ {
+ const float volume = 2500f;
+ const float temperature = 293.15f;
+
+ const float o2 = 12.3f;
+ const float n2 = 45.6f;
+ const float co2 = 0.42f;
+ const float plasma = 0.05f;
+
+ var serverScaled = 0f;
+ var serverUnscaled = 0f;
+ var clientScaled = 0f;
+ var clientUnscaled = 0f;
+
+ await Server.WaitPost(delegate
+ {
+ var mix = new GasMixture(volume) { Temperature = temperature };
+ mix.AdjustMoles(Gas.Oxygen, o2);
+ mix.AdjustMoles(Gas.Nitrogen, n2);
+ mix.AdjustMoles(Gas.CarbonDioxide, co2);
+ mix.AdjustMoles(Gas.Plasma, plasma);
+
+ serverScaled = _sAtmos.GetHeatCapacity(mix, applyScaling: true);
+ serverUnscaled = _sAtmos.GetHeatCapacity(mix, applyScaling: false);
+ });
+
+ await Client.WaitPost(delegate
+ {
+ var mix = new GasMixture(volume) { Temperature = temperature };
+ mix.AdjustMoles(Gas.Oxygen, o2);
+ mix.AdjustMoles(Gas.Nitrogen, n2);
+ mix.AdjustMoles(Gas.CarbonDioxide, co2);
+ mix.AdjustMoles(Gas.Plasma, plasma);
+
+ clientScaled = _cAtmos.GetHeatCapacity(mix, applyScaling: true);
+ clientUnscaled = _cAtmos.GetHeatCapacity(mix, applyScaling: false);
+ });
+
+ // none of these should be exploding or nonzero.
+ // they could potentially agree at insane values and pass the test
+ // so check for if they're sane.
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(serverScaled,
+ Is.GreaterThan(0f),
+ "Heat capacity calculated on server with scaling is not greater than zero.");
+ Assert.That(serverUnscaled,
+ Is.GreaterThan(0f),
+ "Heat capacity calculated on server without scaling is not greater than zero.");
+ Assert.That(clientScaled,
+ Is.GreaterThan(0f),
+ "Heat capacity calculated on client with scaling is not greater than zero.");
+ Assert.That(clientUnscaled,
+ Is.GreaterThan(0f),
+ "Heat capacity calculated on client without scaling is not greater than zero.");
+
+ Assert.That(float.IsFinite(serverScaled),
+ Is.True,
+ "Heat capacity calculated on server with scaling is not finite.");
+ Assert.That(float.IsFinite(serverUnscaled),
+ Is.True,
+ "Heat capacity calculated on server without scaling is not finite.");
+ Assert.That(float.IsFinite(clientScaled),
+ Is.True,
+ "Heat capacity calculated on client with scaling is not finite.");
+ Assert.That(float.IsFinite(clientUnscaled),
+ Is.True,
+ "Heat capacity calculated on client without scaling is not finite.");
+ }
+
+ const float epsilon = 1e-4f;
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(serverScaled,
+ Is.EqualTo(clientScaled).Within(epsilon),
+ "Heat capacity calculated with scaling does not agree between client and server.");
+ Assert.That(serverUnscaled,
+ Is.EqualTo(clientUnscaled).Within(epsilon),
+ "Heat capacity calculated without scaling does not agree between client and server.");
+
+ Assert.That(serverUnscaled,
+ Is.EqualTo(serverScaled * _sAtmos.HeatScale).Within(epsilon),
+ "Heat capacity calculated on server without scaling does not equal scaled value multiplied by HeatScale.");
+ Assert.That(clientUnscaled,
+ Is.EqualTo(clientScaled * _cAtmos.HeatScale).Within(epsilon),
+ "Heat capacity calculated on client without scaling does not equal scaled value multiplied by HeatScale.");
+ }
+ }
+
+ ///
+ /// HeatScale CVAR is required for specific heat calculations.
+ /// Assert that they agree across client and server, and that changing the CVAR
+ /// replicates properly and updates the cached value.
+ /// Also assert that calculations using the updated HeatScale agree properly.
+ ///
+ [Test]
+ public async Task HeatScaleCVar_Replicates_Agree()
+ {
+ // ensure that replicated value changes by testing a new value
+ const float newHeatScale = 13f;
+
+ _sConfig = Server.ResolveDependency();
+ _cConfig = Client.ResolveDependency();
+
+ await Server.WaitPost(delegate
+ {
+ _sConfig.SetCVar(CCVars.AtmosHeatScale, newHeatScale);
+ });
+
+ await Server.WaitRunTicks(5);
+ await Client.WaitRunTicks(5);
+
+ // assert agreement between client and server
+ float serverCVar = 0;
+ float clientCVar = 0;
+ float serverHeatScale = 0;
+ float clientHeatScale = 0;
+
+ await Server.WaitPost(delegate
+ {
+ serverCVar = _sConfig.GetCVar(CCVars.AtmosHeatScale);
+ serverHeatScale = _sAtmos.HeatScale;
+ });
+
+ await Client.WaitPost(delegate
+ {
+ clientCVar = _cConfig.GetCVar(CCVars.AtmosHeatScale);
+ clientHeatScale = _cAtmos.HeatScale;
+ });
+
+ const float epsilon = 1e-4f;
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(serverCVar,
+ Is.EqualTo(newHeatScale).Within(epsilon),
+ "Server CVAR value for AtmosHeatScale does not equal the set value.");
+ Assert.That(clientCVar,
+ Is.EqualTo(newHeatScale).Within(epsilon),
+ "Client CVAR value for AtmosHeatScale does not equal the set value.");
+
+ Assert.That(serverHeatScale,
+ Is.EqualTo(newHeatScale).Within(epsilon),
+ "Server cached HeatScale does not equal the set CVAR value.");
+ Assert.That(clientHeatScale,
+ Is.EqualTo(newHeatScale).Within(epsilon),
+ "Client cached HeatScale does not equal the set CVAR value.");
+
+ Assert.That(serverHeatScale,
+ Is.EqualTo(clientHeatScale).Within(epsilon),
+ "Client and server cached HeatScale values do not agree.");
+ }
+
+ // verify that anything calculated using the shared HeatScale agrees properly
+ const float volume = 2500f;
+ const float temperature = 293.15f;
+
+ var sScaled = 0f;
+ var sUnscaled = 0f;
+ var cScaled = 0f;
+ var cUnscaled = 0f;
+
+ await Server.WaitPost(delegate
+ {
+ var mix = new GasMixture(volume) { Temperature = temperature };
+ mix.AdjustMoles(Gas.Oxygen, 10f);
+ mix.AdjustMoles(Gas.Nitrogen, 20f);
+
+ sScaled = _sAtmos.GetHeatCapacity(mix, applyScaling: true);
+ sUnscaled = _sAtmos.GetHeatCapacity(mix, applyScaling: false);
+ });
+
+ await Client.WaitPost(delegate
+ {
+ var mix = new GasMixture(volume) { Temperature = temperature };
+ mix.AdjustMoles(Gas.Oxygen, 10f);
+ mix.AdjustMoles(Gas.Nitrogen, 20f);
+
+ cScaled = _cAtmos.GetHeatCapacity(mix, applyScaling: true);
+ cUnscaled = _cAtmos.GetHeatCapacity(mix, applyScaling: false);
+ });
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(sScaled,
+ Is.GreaterThan(0f),
+ "Heat capacity calculated on server with scaling is not greater than zero after CVAR change.");
+ Assert.That(cScaled,
+ Is.GreaterThan(0f),
+ "Heat capacity calculated on client with scaling is not greater than zero after CVAR change.");
+
+ Assert.That(sUnscaled,
+ Is.EqualTo(sScaled * serverHeatScale).Within(epsilon),
+ "Heat capacity calculated on server without scaling does not equal scaled value multiplied by updated HeatScale.");
+ Assert.That(cUnscaled,
+ Is.EqualTo(cScaled * clientHeatScale).Within(epsilon),
+ "Heat capacity calculated on client without scaling does not equal scaled value multiplied by updated HeatScale.");
+ }
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Atmos/TileAtmosphereTest.cs b/Content.IntegrationTests/Tests/Atmos/TileAtmosphereTest.cs
new file mode 100644
index 0000000000..368bb63c1d
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Atmos/TileAtmosphereTest.cs
@@ -0,0 +1,159 @@
+using Content.Shared.Atmos;
+using Content.Shared.CCVar;
+using Content.Shared.Coordinates;
+using Content.Shared.Tests;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Maths;
+using Robust.Shared.Utility;
+
+namespace Content.IntegrationTests.Tests.Atmos;
+
+[TestOf(typeof(Atmospherics))]
+public abstract class TileAtmosphereTest : AtmosTest
+{
+ ///
+ /// Spawns gas in an enclosed space and checks that pressure equalizes within reasonable time.
+ /// Checks that mole count stays the same.
+ ///
+ [Test]
+ public async Task GasSpreading()
+ {
+ var markers = SEntMan.AllEntities();
+
+ EntityUid source, point1, point2;
+ source = point1 = point2 = EntityUid.Invalid;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(GetMarker(markers, "source", out source));
+ Assert.That(GetMarker(markers, "point1", out point1));
+ Assert.That(GetMarker(markers, "point2", out point2));
+ });
+
+ Assert.That(GetGridMoles(RelevantAtmos), Is.EqualTo(0.0f));
+
+ var sourceMix = SAtmos.GetTileMixture(source, true);
+ Assert.That(sourceMix, Is.Not.EqualTo(null));
+ sourceMix.AdjustMoles(Gas.Frezon, Moles);
+
+ await Pair.Server.WaitPost(() =>
+ {
+ SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
+ });
+
+ var mix1 = SAtmos.GetTileMixture(point1);
+ var mix2 = SAtmos.GetTileMixture(point2);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(mix1, Is.Not.EqualTo(null));
+ Assert.That(mix2, Is.Not.EqualTo(null));
+ });
+
+ AssertMixMoles(mix1, mix2, Tolerance);
+ AssertGridMoles(Moles, Tolerance);
+ }
+
+ ///
+ /// Spawns a combustible mixture and sets it ablaze.
+ /// Checks that fire propages through the entire grid.
+ ///
+ [Test]
+ public async Task FireSpreading()
+ {
+ var markers = SEntMan.AllEntities();
+
+ EntityUid source, point1, point2;
+ source = point1 = point2 = EntityUid.Invalid;
+
+ Vector2i sourceXY, point1XY, point2XY;
+ sourceXY = point1XY = point2XY = Vector2i.Zero;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(GetMarker(markers, "source", out source));
+ Assert.That(GetMarker(markers, "point1", out point1));
+ Assert.That(GetMarker(markers, "point2", out point2));
+
+ Assert.That(Transform.TryGetGridTilePosition(source, out sourceXY, MapData.Grid));
+ Assert.That(Transform.TryGetGridTilePosition(source, out point1XY, MapData.Grid));
+ Assert.That(Transform.TryGetGridTilePosition(source, out point2XY, MapData.Grid));
+ });
+
+ Assert.That(GetGridMoles(RelevantAtmos), Is.EqualTo(0));
+
+ var sourceMix = SAtmos.GetTileMixture(source, true);
+ Assert.That(sourceMix, Is.Not.EqualTo(null));
+
+ sourceMix.AdjustMoles(Gas.Plasma, Moles / 10);
+ sourceMix.AdjustMoles(Gas.Oxygen, Moles - Moles / 10);
+ sourceMix.Temperature = Atmospherics.FireMinimumTemperatureToExist - 10;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(SAtmos.IsHotspotActive(MapData.Grid, sourceXY), Is.False);
+ Assert.That(SAtmos.IsHotspotActive(MapData.Grid, point1XY), Is.False);
+ Assert.That(SAtmos.IsHotspotActive(MapData.Grid, point2XY), Is.False);
+ });
+
+ await Server.WaitAssertion(() =>
+ {
+ var welder = SEntMan.SpawnEntity("Welder", source.ToCoordinates());
+ Assert.That(ItemToggleSys.TryActivate(welder));
+ });
+
+ await Server.WaitRunTicks(500);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(SAtmos.IsHotspotActive(MapData.Grid, sourceXY), Is.True);
+ Assert.That(SAtmos.IsHotspotActive(MapData.Grid, point1XY), Is.True);
+ Assert.That(SAtmos.IsHotspotActive(MapData.Grid, point2XY), Is.True);
+ });
+
+ var mix1 = SAtmos.GetTileMixture(point1);
+ var mix2 = SAtmos.GetTileMixture(point2);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(mix1, Is.Not.EqualTo(null));
+ Assert.That(mix2, Is.Not.EqualTo(null));
+ });
+
+ AssertMixMoles(mix1, mix2, Tolerance);
+ AssertGridMoles(Moles, Tolerance);
+ }
+}
+
+// Declare separate fixtures to override the TestMap and configure CVars
+public sealed class TileAtmosphereTest_X : TileAtmosphereTest
+{
+ protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/tile_atmosphere_test_x.yml");
+}
+
+public sealed class TileAtmosphereTest_Snake : TileAtmosphereTest
+{
+ protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/tile_atmosphere_test_snake.yml");
+}
+
+public sealed class TileAtmosphereTest_LINDA_X : TileAtmosphereTest
+{
+ protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/tile_atmosphere_test_x.yml");
+ public override async Task Setup()
+ {
+ await base.Setup();
+ Assert.That(Server.CfgMan.GetCVar(CCVars.MonstermosEqualization));
+ Server.CfgMan.SetCVar(CCVars.MonstermosEqualization, false);
+ }
+}
+
+public sealed class TileAtmosphereTest_LINDA_Snake : TileAtmosphereTest
+{
+ protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/tile_atmosphere_test_snake.yml");
+ public override async Task Setup()
+ {
+ await base.Setup();
+ Assert.That(Server.CfgMan.GetCVar(CCVars.MonstermosEqualization));
+ Server.CfgMan.SetCVar(CCVars.MonstermosEqualization, false);
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Body/GibTest.cs b/Content.IntegrationTests/Tests/Body/GibTest.cs
deleted file mode 100644
index e3d5dac33c..0000000000
--- a/Content.IntegrationTests/Tests/Body/GibTest.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-#nullable enable
-using Content.Server.Body.Systems;
-using Robust.Shared.GameObjects;
-
-namespace Content.IntegrationTests.Tests.Body;
-
-[TestFixture]
-public sealed class GibTest
-{
- [Test]
- public async Task TestGib()
- {
- await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true });
- var (server, client) = (pair.Server, pair.Client);
- var map = await pair.CreateTestMap();
-
- EntityUid target1 = default;
- EntityUid target2 = default;
-
- await server.WaitAssertion(() => target1 = server.EntMan.Spawn("MobHuman", map.MapCoords));
- await server.WaitAssertion(() => target2 = server.EntMan.Spawn("MobHuman", map.MapCoords));
- await pair.WaitCommand($"setoutfit {server.EntMan.GetNetEntity(target1)} CaptainGear");
- await pair.WaitCommand($"setoutfit {server.EntMan.GetNetEntity(target2)} CaptainGear");
-
- await pair.RunTicksSync(5);
- var nuid1 = pair.ToClientUid(target1);
- var nuid2 = pair.ToClientUid(target2);
- Assert.That(client.EntMan.EntityExists(nuid1));
- Assert.That(client.EntMan.EntityExists(nuid2));
-
- await server.WaitAssertion(() => server.System().GibBody(target1, acidify: false));
- await server.WaitAssertion(() => server.System().GibBody(target2, acidify: true));
-
- await pair.RunTicksSync(5);
- await pair.WaitCommand("dirty");
- await pair.RunTicksSync(5);
-
- Assert.That(!client.EntMan.EntityExists(nuid1));
- Assert.That(!client.EntMan.EntityExists(nuid2));
-
- await pair.CleanReturnAsync();
- }
-}
diff --git a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs
index a14aa2e495..14ec07b48e 100644
--- a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs
+++ b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs
@@ -31,6 +31,8 @@ namespace Content.IntegrationTests.Tests.Buckle
- type: Hands
- type: ComplexInteraction
- type: InputMover
+ - type: Physics
+ bodyType: KinematicController
- type: Body
prototype: Human
- type: StandingState
@@ -317,10 +319,10 @@ namespace Content.IntegrationTests.Tests.Buckle
// he's not supposed to be buckled with the new falling down system
// do i just did this :trollface:
- // Now with no item in any hand
+ // Still with items in hand
foreach (var hand in hands.Hands.Keys)
{
- Assert.That(handsSys.GetHeldItem((human, hands), hand), Is.Null);
+ Assert.That(handsSys.GetHeldItem((human, hands), hand), Is.Not.Null);
}
buckleSystem.Unbuckle(human, human);
diff --git a/Content.IntegrationTests/Tests/Chemistry/DrainTest.cs b/Content.IntegrationTests/Tests/Chemistry/DrainTest.cs
new file mode 100644
index 0000000000..55f405f039
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Chemistry/DrainTest.cs
@@ -0,0 +1,124 @@
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.FixedPoint;
+using Content.Shared.Fluids.Components;
+using Content.Shared.Nutrition.Components;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.Chemistry;
+
+public sealed class DrainTest : InteractionTest
+{
+ private static readonly EntProtoId PizzaPrototype = "FoodPizzaMargherita";
+ private static readonly EntProtoId DrainPrototype = "FloorDrain";
+ private static readonly EntProtoId BucketPrototype = "Bucket";
+ private static readonly ProtoId BloodReagent = "Blood";
+ private static readonly ProtoId WaterReagent = "Water";
+ private static readonly FixedPoint2 WaterVolume = 50; // 50u
+ private static readonly FixedPoint2 PuddleVolume = 30; // 30u
+
+ [TestPrototypes]
+ private static readonly string Prototypes = @$"
+- type: entity
+ parent: Puddle
+ id: PuddleBloodTest
+ suffix: Blood (30u)
+ components:
+ - type: SolutionContainerManager
+ solutions:
+ puddle:
+ maxVol: 1000
+ reagents:
+ - ReagentId: {BloodReagent}
+ Quantity: {PuddleVolume}
+";
+
+
+ ///
+ /// Tests that drag drop interactions with drains are working as intended.
+ ///
+ [Test]
+ public async Task DragDropOntoDrainTest()
+ {
+ var solutionContainerSys = SEntMan.System();
+
+ // Spawn a drain one tile away.
+ var drain = await Spawn(DrainPrototype);
+
+ // Spawn a bucket at the player's coordinates.
+ var bucket = await Spawn(BucketPrototype, PlayerCoords);
+
+ // Add water to the bucket.
+ Assert.That(solutionContainerSys.TryGetDrainableSolution(ToServer(bucket), out var solutionEnt, out var solution), "Bucket had no drainable solution.");
+ await Server.WaitAssertion(() =>
+ {
+ Assert.That(solutionContainerSys.TryAddReagent(solutionEnt.Value, WaterReagent, WaterVolume), "Could not add water to the bucket.");
+ });
+
+ // Check that the bucket was filled.
+ Assert.That(solutionContainerSys.TryGetDrainableSolution(ToServer(bucket), out solutionEnt, out solution), "Bucket had no drainable solution after filling it.");
+ Assert.That(solution.Volume, Is.EqualTo(WaterVolume));
+
+ // Drag drop the bucket onto the drain.
+ await DragDrop(bucket, drain);
+
+ // Check that the bucket is empty.
+ Assert.That(solutionContainerSys.TryGetDrainableSolution(ToServer(bucket), out solutionEnt, out solution), "Bucket had no drainable solution after draining it.");
+ Assert.That(solution.Volume, Is.EqualTo(FixedPoint2.Zero), "Bucket was not empty after draining it.");
+
+ await Delete(bucket);
+
+ // Spawn a pizza at the player's coordinates.
+ var pizza = await Spawn(PizzaPrototype, PlayerCoords);
+
+ // Check that the pizza is not empty.
+ var edibleSolutionId = Comp(pizza).Solution;
+ Assert.That(solutionContainerSys.TryGetSolution(ToServer(pizza), edibleSolutionId, out solutionEnt, out solution), "Pizza had no edible solution.");
+ var pizzaVolume = solution.Volume;
+ Assert.That(pizzaVolume, Is.GreaterThan(FixedPoint2.Zero), "Pizza had no reagents inside its edible solution.");
+
+ // Drag drop the pizza onto the drain.
+ // Yes, this was a bug that existed before.
+ await DragDrop(pizza, drain);
+
+ // Check that the pizza did not get deleted or had its reagents drained.
+ AssertExists(pizza);
+ Assert.That(solutionContainerSys.TryGetSolution(ToServer(pizza), edibleSolutionId, out solutionEnt, out solution), "Pizza had no edible solution.");
+ Assert.That(solution.Volume, Is.EqualTo(pizzaVolume), "Pizza lost reagents when drag dropped onto a drain.");
+ }
+
+ ///
+ /// Tests that drains make puddles next to them disappear.
+ ///
+ [Test]
+ public async Task DrainPuddleTest()
+ {
+ var solutionContainerSys = SEntMan.System();
+
+ // Spawn a puddle at the player coordinates;
+ var puddle = await Spawn("PuddleBloodTest", PlayerCoords);
+
+ // Make sure the reagent chosen for this test does not evaporate on its own.
+ // If you are a fork that made more reagents evaporate, just change BloodReagent ProtoId above to something else.
+ Assert.That(HasComp(puddle), Is.False, "The chosen reagent is evaporating on its own and we cannot use it for the drain test.");
+
+ var puddleSolutionId = Comp(puddle).SolutionName;
+ Assert.That(solutionContainerSys.TryGetSolution(ToServer(puddle), puddleSolutionId, out _, out var solution), "Puddle had no solution.");
+ Assert.That(solution.Volume, Is.EqualTo(PuddleVolume), "Puddle had the wrong amount of reagents after spawning.");
+
+ // Wait a few seconds and check that the puddle did not disappear on its own.
+ await RunSeconds(10);
+ Assert.That(solutionContainerSys.TryGetSolution(ToServer(puddle), puddleSolutionId, out _, out solution), "Puddle had no solution.");
+ Assert.That(solution.Volume, Is.EqualTo(PuddleVolume), "Puddle had the wrong amount of reagents after spawning.");
+
+ // Spawn a drain one tile away.
+ await Spawn(DrainPrototype);
+
+ // Wait a few seconds.
+ await RunSeconds(10);
+
+ // Make sure the puddle was deleted by the drain.
+ AssertDeleted(puddle);
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Chemistry/ReagentDataTest.cs b/Content.IntegrationTests/Tests/Chemistry/ReagentDataTest.cs
index f488734655..b038458662 100644
--- a/Content.IntegrationTests/Tests/Chemistry/ReagentDataTest.cs
+++ b/Content.IntegrationTests/Tests/Chemistry/ReagentDataTest.cs
@@ -11,10 +11,9 @@ namespace Content.IntegrationTests.Tests.Chemistry;
public sealed class ReagentDataTest : InteractionTest
{
[Test]
- public async Task ReagentDataIsSerializable()
+ public void ReagentDataIsSerializable()
{
- await using var pair = await PoolManager.GetServerClient();
- var reflection = pair.Server.ResolveDependency();
+ var reflection = Pair.Server.ResolveDependency();
Assert.Multiple(() =>
{
@@ -24,7 +23,5 @@ public sealed class ReagentDataTest : InteractionTest
Assert.That(instance.HasCustomAttribute(), $"{instance} must have the serializable attribute.");
}
});
-
- await pair.CleanReturnAsync();
}
}
diff --git a/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs b/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs
index 6d860f0ac3..13d8bdc6d2 100644
--- a/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs
+++ b/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs
@@ -50,11 +50,12 @@ namespace Content.IntegrationTests.Tests.Chemistry
beaker = entityManager.SpawnEntity("TestSolutionContainer", coordinates);
Assert.That(solutionContainerSystem
.TryGetSolution(beaker, "beaker", out solutionEnt, out solution));
+ solutionEnt.Value.Comp.Solution.CanReact = false;
foreach (var (id, reactant) in reactionPrototype.Reactants)
{
#pragma warning disable NUnit2045
Assert.That(solutionContainerSystem
- .TryAddReagent(solutionEnt.Value, id, reactant.Amount, out var quantity));
+ .TryAddReagent(solutionEnt.Value, id, reactant.Amount, out var quantity, reactionPrototype.MinimumTemperature));
Assert.That(reactant.Amount, Is.EqualTo(quantity));
#pragma warning restore NUnit2045
}
@@ -67,7 +68,7 @@ namespace Content.IntegrationTests.Tests.Chemistry
//Check if the reaction is the first to occur when heated
foreach (var possibleReaction in possibleReactions.OrderBy(r => r.MinimumTemperature))
{
- if (possibleReaction.MinimumTemperature < reactionPrototype.MinimumTemperature && possibleReaction.MixingCategories == reactionPrototype.MixingCategories)
+ if (possibleReaction.Priority >= reactionPrototype.Priority && possibleReaction.MinimumTemperature < reactionPrototype.MinimumTemperature && possibleReaction.MixingCategories == reactionPrototype.MixingCategories)
{
Assert.Fail($"The {possibleReaction.ID} reaction may occur before {reactionPrototype.ID} when heated.");
}
@@ -76,14 +77,16 @@ namespace Content.IntegrationTests.Tests.Chemistry
//Check if the reaction is the first to occur when freezing
foreach (var possibleReaction in possibleReactions.OrderBy(r => r.MaximumTemperature))
{
- if (possibleReaction.MaximumTemperature > reactionPrototype.MaximumTemperature && possibleReaction.MixingCategories == reactionPrototype.MixingCategories)
+ if (possibleReaction.Priority >= reactionPrototype.Priority && possibleReaction.MaximumTemperature > reactionPrototype.MaximumTemperature && possibleReaction.MixingCategories == reactionPrototype.MixingCategories)
{
Assert.Fail($"The {possibleReaction.ID} reaction may occur before {reactionPrototype.ID} when freezing.");
}
}
//Now safe set the temperature and mix the reagents
+ solutionEnt.Value.Comp.Solution.CanReact = true;
solutionContainerSystem.SetTemperature(solutionEnt.Value, reactionPrototype.MinimumTemperature);
+ solutionContainerSystem.UpdateChemicals(solutionEnt.Value);
if (reactionPrototype.MixingCategories != null)
{
diff --git a/Content.IntegrationTests/Tests/Commands/ObjectiveCommandsTest.cs b/Content.IntegrationTests/Tests/Commands/ObjectiveCommandsTest.cs
index a77761a7d1..d430325e31 100644
--- a/Content.IntegrationTests/Tests/Commands/ObjectiveCommandsTest.cs
+++ b/Content.IntegrationTests/Tests/Commands/ObjectiveCommandsTest.cs
@@ -54,7 +54,7 @@ public sealed class ObjectiveCommandsTest
});
Assert.That(mindEnt, Is.Not.Null);
- var mindComp = mindEnt.Value.Comp;
+ var mindComp = mindEnt!.Value.Comp;
Assert.That(mindComp.Objectives, Is.Empty, "Dummy player started with objectives.");
await pair.WaitCommand($"addobjective {playerSession.Name} {ObjectiveProtoId}");
diff --git a/Content.IntegrationTests/Tests/Construction/RCDTest.cs b/Content.IntegrationTests/Tests/Construction/RCDTest.cs
index 814f7e89aa..770f004517 100644
--- a/Content.IntegrationTests/Tests/Construction/RCDTest.cs
+++ b/Content.IntegrationTests/Tests/Construction/RCDTest.cs
@@ -38,9 +38,9 @@ public sealed class RCDTest : InteractionTest
pEast = Transform.WithEntityId(pEast, MapData.Grid);
pWest = Transform.WithEntityId(pWest, MapData.Grid);
- await SetTile(Plating, SEntMan.GetNetCoordinates(pNorth), MapData.Grid);
- await SetTile(Plating, SEntMan.GetNetCoordinates(pSouth), MapData.Grid);
- await SetTile(Plating, SEntMan.GetNetCoordinates(pEast), MapData.Grid);
+ await SetTile(PlatingRCD, SEntMan.GetNetCoordinates(pNorth), MapData.Grid);
+ await SetTile(PlatingRCD, SEntMan.GetNetCoordinates(pSouth), MapData.Grid);
+ await SetTile(PlatingRCD, SEntMan.GetNetCoordinates(pEast), MapData.Grid);
await SetTile(Lattice, SEntMan.GetNetCoordinates(pWest), MapData.Grid);
Assert.That(ProtoMan.TryIndex(RCDSettingWall, out var settingWall), $"RCDPrototype not found: {RCDSettingWall}.");
@@ -194,7 +194,7 @@ public sealed class RCDTest : InteractionTest
// Deconstruct the steel tile.
await Interact(null, pEast);
await RunSeconds(settingDeconstructTile.Delay + 1); // wait for the deconstruction to finish
- await AssertTile(Lattice, FromServer(pEast));
+ await AssertTile(PlatingRCD, FromServer(pEast));
// Check that the cost of the deconstruction was subtracted from the current charges.
newCharges = sCharges.GetCurrentCharges(ToServer(rcd));
diff --git a/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs b/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs
index 72e8901631..20b0877548 100644
--- a/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs
+++ b/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs
@@ -164,7 +164,7 @@ namespace Content.IntegrationTests.Tests.Damageable
var damageToDeal = FixedPoint2.New(types.Count * 5);
DamageSpecifier damage = new(group3, damageToDeal);
- sDamageableSystem.TryChangeDamage(uid, damage, true);
+ sDamageableSystem.ChangeDamage(uid, damage, true);
Assert.Multiple(() =>
{
@@ -178,7 +178,7 @@ namespace Content.IntegrationTests.Tests.Damageable
});
// Heal
- sDamageableSystem.TryChangeDamage(uid, -damage);
+ sDamageableSystem.ChangeDamage(uid, -damage);
Assert.Multiple(() =>
{
@@ -197,7 +197,7 @@ namespace Content.IntegrationTests.Tests.Damageable
Assert.That(types, Has.Count.EqualTo(3));
damage = new DamageSpecifier(group3, 14);
- sDamageableSystem.TryChangeDamage(uid, damage, true);
+ sDamageableSystem.ChangeDamage(uid, damage, true);
Assert.Multiple(() =>
{
@@ -209,7 +209,7 @@ namespace Content.IntegrationTests.Tests.Damageable
});
// Heal
- sDamageableSystem.TryChangeDamage(uid, -damage);
+ sDamageableSystem.ChangeDamage(uid, -damage);
Assert.Multiple(() =>
{
@@ -225,7 +225,7 @@ namespace Content.IntegrationTests.Tests.Damageable
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
});
damage = new DamageSpecifier(group1, FixedPoint2.New(10)) + new DamageSpecifier(type2b, FixedPoint2.New(10));
- sDamageableSystem.TryChangeDamage(uid, damage, true);
+ sDamageableSystem.ChangeDamage(uid, damage, true);
Assert.Multiple(() =>
{
@@ -245,9 +245,9 @@ namespace Content.IntegrationTests.Tests.Damageable
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
// Test 'wasted' healing
- sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(type3a, 5));
- sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(type3b, 7));
- sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(group3, -11));
+ sDamageableSystem.ChangeDamage(uid, new DamageSpecifier(type3a, 5));
+ sDamageableSystem.ChangeDamage(uid, new DamageSpecifier(type3b, 7));
+ sDamageableSystem.ChangeDamage(uid, new DamageSpecifier(group3, -11));
Assert.Multiple(() =>
{
@@ -257,11 +257,11 @@ namespace Content.IntegrationTests.Tests.Damageable
});
// Test Over-Healing
- sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(group3, FixedPoint2.New(-100)));
+ sDamageableSystem.ChangeDamage(uid, new DamageSpecifier(group3, FixedPoint2.New(-100)));
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
// Test that if no health change occurred, returns false
- sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(group3, -100));
+ sDamageableSystem.ChangeDamage(uid, new DamageSpecifier(group3, -100));
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
});
await pair.CleanReturnAsync();
diff --git a/Content.IntegrationTests/Tests/FillLevelSpriteTest.cs b/Content.IntegrationTests/Tests/FillLevelSpriteTest.cs
index 37e777fa8c..99354e16c1 100644
--- a/Content.IntegrationTests/Tests/FillLevelSpriteTest.cs
+++ b/Content.IntegrationTests/Tests/FillLevelSpriteTest.cs
@@ -1,5 +1,7 @@
using System.Linq;
+using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
+using Content.Shared.Prototypes;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
@@ -13,14 +15,17 @@ namespace Content.IntegrationTests.Tests;
public sealed class FillLevelSpriteTest
{
private static readonly string[] HandStateNames = ["left", "right"];
+ private static readonly string[] EquipStateNames = ["back", "suitstorage"];
[Test]
public async Task FillLevelSpritesExist()
{
- await using var pair = await PoolManager.GetServerClient();
+ await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true });
var client = pair.Client;
var protoMan = client.ResolveDependency();
var componentFactory = client.ResolveDependency();
+ var entMan = client.ResolveDependency();
+ var spriteSystem = client.System();
await client.WaitAssertion(() =>
{
@@ -31,39 +36,70 @@ public sealed class FillLevelSpriteTest
.OrderBy(p => p.ID)
.ToList();
- foreach (var proto in protos)
+ Assert.Multiple(() =>
{
- Assert.That(proto.TryGetComponent(out var visuals, componentFactory));
- Assert.That(proto.TryGetComponent(out var sprite, componentFactory));
-
- var rsi = sprite.BaseRSI;
-
- // Test base sprite fills
- if (!string.IsNullOrEmpty(visuals.FillBaseName))
+ foreach (var proto in protos)
{
- for (var i = 1; i <= visuals.MaxFillLevels; i++)
+ Assert.That(proto.TryGetComponent(out var visuals, componentFactory));
+ Assert.That(proto.TryGetComponent(out var sprite, componentFactory));
+ if (!proto.HasComponent(componentFactory))
{
- var state = $"{visuals.FillBaseName}{i}";
- Assert.That(rsi.TryGetState(state, out _), @$"{proto.ID} has SolutionContainerVisualsComponent with
- MaxFillLevels = {visuals.MaxFillLevels}, but {rsi.Path} doesn't have state {state}!");
+ Assert.Fail(@$"{proto.ID} has SolutionContainerVisualsComponent but no AppearanceComponent.");
}
- }
- // Test inhand sprite fills
- if (!string.IsNullOrEmpty(visuals.InHandsFillBaseName))
- {
- for (var i = 1; i <= visuals.InHandsMaxFillLevels; i++)
+ // Test base sprite fills
+ if (!string.IsNullOrEmpty(visuals.FillBaseName) && visuals.MaxFillLevels > 0)
{
- foreach (var handname in HandStateNames)
+ var entity = entMan.Spawn(proto.ID);
+ if (!spriteSystem.LayerMapTryGet(entity, SolutionContainerLayers.Fill, out var fillLayerId, false))
{
- var state = $"inhand-{handname}{visuals.InHandsFillBaseName}{i}";
- Assert.That(rsi.TryGetState(state, out _), @$"{proto.ID} has SolutionContainerVisualsComponent with
- InHandsMaxFillLevels = {visuals.InHandsMaxFillLevels}, but {rsi.Path} doesn't have state {state}!");
+ Assert.Fail(@$"{proto.ID} has SolutionContainerVisualsComponent but no fill layer map.");
}
+ if (!spriteSystem.TryGetLayer(entity, fillLayerId, out var fillLayer, false))
+ {
+ Assert.Fail(@$"{proto.ID} somehow lost a layer.");
+ }
+ var rsi = fillLayer.ActualRsi;
+ for (var i = 1; i <= visuals.MaxFillLevels; i++)
+ {
+ var state = $"{visuals.FillBaseName}{i}";
+ Assert.That(rsi.TryGetState(state, out _), @$"{proto.ID} has SolutionContainerVisualsComponent with
+ MaxFillLevels = {visuals.MaxFillLevels}, but {rsi.Path} doesn't have state {state}!");
+ }
+ }
+
+ // Test inhand sprite fills
+ if (!string.IsNullOrEmpty(visuals.InHandsFillBaseName) && visuals.InHandsMaxFillLevels > 0)
+ {
+ var rsi = sprite.BaseRSI;
+ for (var i = 1; i <= visuals.InHandsMaxFillLevels; i++)
+ {
+ foreach (var handname in HandStateNames)
+ {
+ var state = $"inhand-{handname}{visuals.InHandsFillBaseName}{i}";
+ Assert.That(rsi.TryGetState(state, out _), @$"{proto.ID} has SolutionContainerVisualsComponent with
+ InHandsMaxFillLevels = {visuals.InHandsMaxFillLevels}, but {rsi.Path} doesn't have state {state}!");
+ }
+ }
+ }
+
+ // Test equipped sprite fills
+ if (!string.IsNullOrEmpty(visuals.EquippedFillBaseName) && visuals.EquippedMaxFillLevels > 0)
+ {
+ var rsi = sprite.BaseRSI;
+ for (var i = 1; i <= visuals.EquippedMaxFillLevels; i++)
+ {
+ foreach (var equipName in EquipStateNames)
+ {
+ var state = $"equipped-{equipName}{visuals.EquippedFillBaseName}{i}";
+ Assert.That(rsi.TryGetState(state, out _), @$"{proto.ID} has SolutionContainerVisualsComponent with
+ EquippedMaxFillLevels = {visuals.EquippedMaxFillLevels}, but {rsi.Path} doesn't have state {state}!");
+ }
+ }
}
}
- }
+ });
});
await pair.CleanReturnAsync();
diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/EntityPrototypeComponentsTest.cs b/Content.IntegrationTests/Tests/GameObjects/Components/EntityPrototypeComponentsTest.cs
index 5c73245501..ef94cf0f00 100644
--- a/Content.IntegrationTests/Tests/GameObjects/Components/EntityPrototypeComponentsTest.cs
+++ b/Content.IntegrationTests/Tests/GameObjects/Components/EntityPrototypeComponentsTest.cs
@@ -26,7 +26,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components
.ToList()
.AsParallel()
.Where(filePath => filePath.Extension == "yml" &&
- !filePath.Filename.StartsWith(".", StringComparison.Ordinal))
+ !filePath.Filename.StartsWith('.'))
.ToArray();
var cComponentFactory = client.ResolveDependency();
@@ -34,6 +34,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components
var unknownComponentsClient = new List<(string entityId, string component)>();
var unknownComponentsServer = new List<(string entityId, string component)>();
+ var doubleIgnoredComponents = new List<(string entityId, string component)>();
var entitiesValidated = 0;
var componentsValidated = 0;
@@ -72,26 +73,32 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components
var componentType = component.GetNode("type").AsString();
var clientAvailability = cComponentFactory.GetComponentAvailability(componentType);
-
- if (clientAvailability == ComponentAvailability.Unknown)
- {
- var entityId = node.GetNode("id").AsString();
- unknownComponentsClient.Add((entityId, componentType));
- }
-
var serverAvailability = sComponentFactory.GetComponentAvailability(componentType);
- if (serverAvailability == ComponentAvailability.Unknown)
+ var entityId = node.GetNode("id").AsString();
+
+ if ((clientAvailability, serverAvailability) is
+ (ComponentAvailability.Ignore, ComponentAvailability.Ignore))
{
- var entityId = node.GetNode("id").AsString();
- unknownComponentsServer.Add((entityId, componentType));
+ doubleIgnoredComponents.Add((entityId, componentType));
+ continue;
}
+
+ // NOTE: currently, the client's component factory is configured to ignore /all/
+ // non-registered components, meaning this case will never succeed. This is here
+ // mainly for future proofing plus any downstreams that were brave enough to not
+ // ignore all unknown components on clientside.
+ if (clientAvailability == ComponentAvailability.Unknown)
+ unknownComponentsClient.Add((entityId, componentType));
+
+ if (serverAvailability == ComponentAvailability.Unknown)
+ unknownComponentsServer.Add((entityId, componentType));
}
}
}
}
- if (unknownComponentsClient.Count + unknownComponentsServer.Count == 0)
+ if (unknownComponentsClient.Count + unknownComponentsServer.Count + doubleIgnoredComponents.Count == 0)
{
await pair.CleanReturnAsync();
Assert.Pass($"Validated {entitiesValidated} entities with {componentsValidated} components in {paths.Length} files.");
@@ -112,6 +119,12 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components
$"SERVER: Unknown component {component} in prototype {entityId}\n");
}
+ foreach (var (entityId, component) in doubleIgnoredComponents)
+ {
+ message.Append(
+ $"Component {component} in prototype {entityId} is ignored by both client and serverV\n");
+ }
+
Assert.Fail(message.ToString());
}
diff --git a/Content.IntegrationTests/Tests/Gibbing/GibTest.cs b/Content.IntegrationTests/Tests/Gibbing/GibTest.cs
new file mode 100644
index 0000000000..ee0f7a742d
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Gibbing/GibTest.cs
@@ -0,0 +1,36 @@
+#nullable enable
+using Content.Shared.Gibbing;
+using Robust.Shared.GameObjects;
+
+namespace Content.IntegrationTests.Tests.Body;
+
+[TestFixture]
+public sealed class GibTest
+{
+ [Test]
+ public async Task TestGib()
+ {
+ await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true });
+ var (server, client) = (pair.Server, pair.Client);
+ var map = await pair.CreateTestMap();
+
+ EntityUid target = default;
+
+ await server.WaitAssertion(() => target = server.EntMan.Spawn("MobHuman", map.MapCoords));
+ await pair.WaitCommand($"setoutfit {server.EntMan.GetNetEntity(target)} CaptainGear");
+
+ await pair.RunTicksSync(5);
+ var nuid = pair.ToClientUid(target);
+ Assert.That(client.EntMan.EntityExists(nuid));
+
+ await server.WaitAssertion(() => server.System().Gib(target));
+
+ await pair.RunTicksSync(5);
+ await pair.WaitCommand("dirty");
+ await pair.RunTicksSync(5);
+
+ Assert.That(!client.EntMan.EntityExists(nuid));
+
+ await pair.CleanReturnAsync();
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs
index 8917ba7ead..1aac18f3a4 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs
@@ -11,7 +11,9 @@ public abstract partial class InteractionTest
protected const string Floor = "FloorSteel";
protected const string FloorItem = "FloorTileItemSteel";
protected const string Plating = "Plating";
+ protected const string PlatingRCD = "PlatingRCD";
protected const string Lattice = "Lattice";
+ protected const string PlatingBrass = "PlatingBrass";
// Structures
protected const string Airlock = "Airlock";
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
index d04ed4cb3c..0f85397901 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
@@ -91,16 +91,18 @@ public abstract partial class InteractionTest
}
///
- /// Spawn an entity at the target coordinates and set it as the target.
+ /// Spawn an entity at the given coordinates and set it as the target.
+ /// If no coordinates are given it will default to
///
[MemberNotNull(nameof(Target), nameof(STarget), nameof(CTarget))]
#pragma warning disable CS8774 // Member must have a non-null value when exiting.
- protected async Task SpawnTarget(string prototype)
+ protected async Task SpawnTarget(string prototype, NetCoordinates? coords = null)
{
+ coords ??= TargetCoords;
Target = NetEntity.Invalid;
await Server.WaitPost(() =>
{
- Target = SEntMan.GetNetEntity(SEntMan.SpawnAtPosition(prototype, SEntMan.GetCoordinates(TargetCoords)));
+ Target = SEntMan.GetNetEntity(SEntMan.SpawnAtPosition(prototype, SEntMan.GetCoordinates(coords.Value)));
});
await RunTicks(5);
@@ -110,14 +112,16 @@ public abstract partial class InteractionTest
#pragma warning restore CS8774 // Member must have a non-null value when exiting.
///
- /// Spawn an entity entity at the target coordinates without setting it as the target.
+ /// Spawn an entity entity at the given coordinates without setting it as the target.
+ /// If no coordinates are given it will default to
///
- protected async Task Spawn(string prototype)
+ protected async Task Spawn(string prototype, NetCoordinates? coords = null)
{
+ coords ??= TargetCoords;
var entity = NetEntity.Invalid;
await Server.WaitPost(() =>
{
- entity = SEntMan.GetNetEntity(SEntMan.SpawnAtPosition(prototype, SEntMan.GetCoordinates(TargetCoords)));
+ entity = SEntMan.GetNetEntity(SEntMan.SpawnAtPosition(prototype, SEntMan.GetCoordinates(coords.Value)));
});
await RunTicks(5);
@@ -407,6 +411,33 @@ public abstract partial class InteractionTest
}
}
+ ///
+ /// Simulates a drag and drop mouse interaction from one entity to another.
+ ///
+ protected async Task DragDrop(NetEntity source, NetEntity target)
+ {
+ // ScreenCoordinates diff needs to be larger than DragDropSystem.Deadzone for the drag drop to initiate
+ var screenX = CDragDropSys.Deadzone + 1f;
+
+ // Start drag
+ await SetKey(EngineKeyFunctions.Use,
+ BoundKeyState.Down,
+ NetPosition(source),
+ source,
+ screenCoordinates: new ScreenCoordinates(screenX, 0f, WindowId.Main));
+
+ await RunTicks(3);
+
+ // End drag
+ await SetKey(EngineKeyFunctions.Use,
+ BoundKeyState.Up,
+ NetPosition(target),
+ target,
+ screenCoordinates: new ScreenCoordinates(0f, 0f, WindowId.Main));
+
+ await RunTicks(3);
+ }
+
///
/// Throw the currently held entity. Defaults to targeting the current
///
@@ -478,11 +509,11 @@ public abstract partial class InteractionTest
var wasInCombatMode = IsInCombatMode();
await SetCombatMode(true);
- Assert.That(SGun.TryGetGun(SPlayer, out var gunUid, out var gunComp), "Player was not holding a gun!");
+ Assert.That(SGun.TryGetGun(SPlayer, out var gun), "Player was not holding a gun!");
await Server.WaitAssertion(() =>
{
- var success = SGun.AttemptShoot(SPlayer, gunUid, gunComp!, actualTarget);
+ var success = SGun.AttemptShoot(SPlayer, gun, actualTarget);
if (assert)
Assert.That(success, "Gun failed to shoot.");
});
@@ -517,11 +548,11 @@ public abstract partial class InteractionTest
var wasInCombatMode = IsInCombatMode();
await SetCombatMode(true);
- Assert.That(SGun.TryGetGun(SPlayer, out var gunUid, out var gunComp), "Player was not holding a gun!");
+ Assert.That(SGun.TryGetGun(SPlayer, out var gun), "Player was not holding a gun!");
await Server.WaitAssertion(() =>
{
- var success = SGun.AttemptShoot(SPlayer, gunUid, gunComp!, Position(actualTarget!.Value), ToServer(actualTarget));
+ var success = SGun.AttemptShoot(SPlayer, gun, Position(actualTarget!.Value), ToServer(actualTarget));
if (assert)
Assert.That(success, "Gun failed to shoot.");
});
@@ -839,7 +870,7 @@ public abstract partial class InteractionTest
/// The entity at which the events were directed
/// How many new events are expected
/// A predicate that can be used to filter the recorded events
- protected void AssertEvent(EntityUid? uid = null, int count = 1, Func? predicate = null)
+ protected void AssertEvent(EntityUid? uid = null, int count = 1, Func? predicate = null)
where TEvent : notnull
{
Assert.That(GetEvents(uid, predicate).Count, Is.EqualTo(count));
@@ -872,7 +903,7 @@ public abstract partial class InteractionTest
where TEvent : notnull
{
if (_listenerCache.TryGetValue(typeof(TEvent), out var listener))
- return (TestListenerSystem) listener;
+ return (TestListenerSystem)listener;
var type = Server.Resolve().GetAllChildren>().Single();
if (!SEntMan.EntitySysManager.TryGetEntitySystem(type, out var systemObj))
@@ -1607,6 +1638,7 @@ public abstract partial class InteractionTest
protected EntityCoordinates Position(NetEntity uid) => Position(ToServer(uid));
protected EntityCoordinates Position(EntityUid uid) => Xform(uid).Coordinates;
+ protected NetCoordinates NetPosition(NetEntity uid) => FromServer(Position(uid));
#endregion
}
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
index 245aeab9ee..62a03b0abe 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
@@ -3,6 +3,7 @@ using System.Numerics;
using Content.Client.Construction;
using Content.Client.Examine;
using Content.Client.Gameplay;
+using Content.Client.Interaction;
using Content.IntegrationTests.Pair;
using Content.Server.Hands.Systems;
using Content.Server.Stack;
@@ -134,6 +135,7 @@ public abstract partial class InteractionTest
protected InteractionTestSystem CTestSystem = default!;
protected ISawmill CLogger = default!;
protected SharedUserInterfaceSystem CUiSys = default!;
+ protected DragDropSystem CDragDropSys = default!;
// player components
protected HandsComponent? Hands;
@@ -208,6 +210,7 @@ public abstract partial class InteractionTest
CConSys = CEntMan.System();
ExamineSys = CEntMan.System();
CUiSys = CEntMan.System();
+ CDragDropSys = CEntMan.System();
// Setup map.
if (TestMapPath == null)
diff --git a/Content.IntegrationTests/Tests/Medical/DefibrillatorTest.cs b/Content.IntegrationTests/Tests/Medical/DefibrillatorTest.cs
new file mode 100644
index 0000000000..9781bf5c80
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Medical/DefibrillatorTest.cs
@@ -0,0 +1,102 @@
+#nullable enable
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
+using Content.Shared.Damage.Prototypes;
+using Content.Shared.Damage.Systems;
+using Content.Shared.FixedPoint;
+using Content.Shared.Medical;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Mobs.Systems;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.Medical;
+
+///
+/// Tests for defibrilators.
+///
+[TestOf(typeof(DefibrillatorComponent))]
+public sealed class DefibrillatorTest : InteractionTest
+{
+ // We need two hands to use a defbrillator.
+ protected override string PlayerPrototype => "MobHuman";
+
+ private static readonly EntProtoId DefibrillatorProtoId = "Defibrillator";
+ private static readonly EntProtoId TargetProtoId = "MobHuman";
+ private static readonly ProtoId BluntDamageTypeId = "Blunt";
+
+ ///
+ /// Kills a target mob, heals them and then revives them with a defibrillator.
+ ///
+ [Test]
+ public async Task KillAndReviveTest()
+ {
+ var damageableSystem = SEntMan.System();
+ var mobThresholdsSystem = SEntMan.System();
+
+ // Don't let the player and target suffocate.
+ await AddAtmosphere();
+
+ await SpawnTarget(TargetProtoId);
+
+ var targetMobState = Comp();
+ var targetDamageable = Comp();
+
+ // Check that the target has no damage and is not crit or dead.
+ Assert.Multiple(() =>
+ {
+ Assert.That(targetMobState.CurrentState, Is.EqualTo(MobState.Alive), "Target mob was not alive when spawned.");
+ Assert.That(targetDamageable.TotalDamage, Is.EqualTo(FixedPoint2.Zero), "Target mob was damaged when spawned.");
+ });
+
+ // Get the damage needed to kill or crit the target.
+ var critThreshold = mobThresholdsSystem.GetThresholdForState(STarget.Value, MobState.Critical);
+ var deathThreshold = mobThresholdsSystem.GetThresholdForState(STarget.Value, MobState.Dead);
+ var critDamage = new DamageSpecifier(ProtoMan.Index(BluntDamageTypeId), (critThreshold + deathThreshold) / 2);
+ var deathDamage = new DamageSpecifier(ProtoMan.Index(BluntDamageTypeId), deathThreshold);
+
+ // Kill the target by applying blunt damage.
+ await Server.WaitPost(() => damageableSystem.SetDamage((STarget.Value, targetDamageable), deathDamage));
+ await RunTicks(3);
+
+ // Check that the target is dead.
+ Assert.Multiple(() =>
+ {
+ Assert.That(targetMobState.CurrentState, Is.EqualTo(MobState.Dead), "Target mob did not die from deadly damage amount.");
+ Assert.That(targetDamageable.TotalDamage, Is.EqualTo(deathThreshold), "Target mob had the wrong total damage amount after being killed.");
+ });
+
+ // Spawn a defib and activate it.
+ var defib = await PlaceInHands(DefibrillatorProtoId, enableToggleable: true);
+ var cooldown = Comp(defib).ZapDelay;
+
+ // Wait for the cooldown.
+ await RunSeconds((float)cooldown.TotalSeconds);
+
+ // ZAP!
+ await Interact();
+
+ // Check that the target is still dead since it is over the crit threshold.
+ // And it should have taken some extra damage.
+ Assert.Multiple(() =>
+ {
+ Assert.That(targetMobState.CurrentState, Is.EqualTo(MobState.Dead), "Target mob was revived despite being over the death damage threshold.");
+ Assert.That(targetDamageable.TotalDamage, Is.GreaterThan(deathThreshold), "Target mob did not take damage from being defibrillated.");
+ });
+
+ // Set the damage halfway between the crit and death thresholds so that the target can be revived.
+ await Server.WaitPost(() => damageableSystem.SetDamage((STarget.Value, targetDamageable), critDamage));
+ await RunTicks(3);
+
+ // Check that the target is still dead.
+ Assert.That(targetMobState.CurrentState, Is.EqualTo(MobState.Dead), "Target mob revived on its own.");
+
+ // ZAP!
+ await RunSeconds((float)cooldown.TotalSeconds);
+ await Interact();
+
+ // The target should be revived, but in crit.
+ Assert.That(targetMobState.CurrentState, Is.EqualTo(MobState.Critical), "Target mob was not revived from being defibrillated.");
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs b/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs
index 6f33188813..513049bcad 100644
--- a/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs
+++ b/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs
@@ -112,7 +112,7 @@ public sealed partial class MindTests
Assert.That(entMan.EntityExists(attachedEntity), Is.True);
Assert.That(attachedEntity, Is.Not.EqualTo(playerEnt));
Assert.That(entMan.HasComponent(attachedEntity));
- var transform = entMan.GetComponent(attachedEntity.Value);
+ var transform = entMan.GetComponent(attachedEntity!.Value);
Assert.That(transform.MapID, Is.Not.EqualTo(MapId.Nullspace));
Assert.That(transform.MapID, Is.Not.EqualTo(testMap.MapId));
#pragma warning restore NUnit2045
@@ -175,7 +175,7 @@ public sealed partial class MindTests
Assert.That(player.AttachedEntity, Is.Not.Null);
Assert.That(entMan.EntityExists(player.AttachedEntity));
#pragma warning restore NUnit2045
- var originalEntity = player.AttachedEntity.Value;
+ var originalEntity = player.AttachedEntity!.Value;
EntityUid ghost = default!;
await server.WaitAssertion(() =>
@@ -248,7 +248,7 @@ public sealed partial class MindTests
var mindId = player.ContentData()?.Mind;
Assert.That(mindId, Is.Not.Null);
- var mind = entMan.GetComponent(mindId.Value);
+ var mind = entMan.GetComponent(mindId!.Value);
Assert.That(mind.VisitingEntity, Is.Null);
await pair.CleanReturnAsync();
diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs
index 95860b1720..6729f23160 100644
--- a/Content.IntegrationTests/Tests/PostMapInitTest.cs
+++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs
@@ -2,29 +2,28 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
+using YamlDotNet.RepresentationModel;
using Content.Server.Administration.Systems;
using Content.Server.GameTicking;
-using Content.Server.Maps;
using Content.Server.Shuttles.Components;
using Content.Server.Shuttles.Systems;
using Content.Server.Spawners.Components;
using Content.Server.Station.Components;
using Content.Shared.CCVar;
+using Content.Shared.Maps;
using Content.Shared.Roles;
+using Content.Shared.Station.Components;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
-using Robust.Shared.Prototypes;
-using Content.Shared.Station.Components;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
+using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
-using Robust.Shared.Utility;
-using YamlDotNet.RepresentationModel;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
using Robust.Shared.Map.Events;
-
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
namespace Content.IntegrationTests.Tests
{
[TestFixture]
diff --git a/Content.IntegrationTests/Tests/Power/PowerStateTest.cs b/Content.IntegrationTests/Tests/Power/PowerStateTest.cs
new file mode 100644
index 0000000000..dec398212d
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Power/PowerStateTest.cs
@@ -0,0 +1,186 @@
+using Content.Shared.Coordinates;
+using Content.Shared.Power.Components;
+using Content.Shared.Power.EntitySystems;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+
+namespace Content.IntegrationTests.Tests.Power;
+
+[TestFixture]
+public sealed class PowerStateTest
+{
+ [TestPrototypes]
+ private const string Prototypes = @"
+- type: entity
+ id: PowerStateApcReceiverDummy
+ components:
+ - type: ApcPowerReceiver
+ - type: ExtensionCableReceiver
+ - type: Transform
+ anchored: true
+ - type: PowerState
+ isWorking: false
+ idlePowerDraw: 10
+ workingPowerDraw: 50
+";
+
+ ///
+ /// Asserts that switching from idle to working updates the power receiver load to the working draw.
+ ///
+ [Test]
+ public async Task SetWorkingState_IdleToWorking_UpdatesLoad()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var mapManager = server.ResolveDependency();
+ var entManager = server.ResolveDependency();
+ var mapSys = entManager.System();
+
+ await server.WaitAssertion(() =>
+ {
+ mapSys.CreateMap(out var mapId);
+ var grid = mapManager.CreateGridEntity(mapId);
+
+ mapSys.SetTile(grid, Vector2i.Zero, new Tile(1));
+
+ var ent = entManager.SpawnEntity("PowerStateApcReceiverDummy", grid.Owner.ToCoordinates());
+
+ var receiver = entManager.GetComponent(ent);
+ var powerState = entManager.GetComponent(ent);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(powerState.IsWorking, Is.False);
+ Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f));
+ });
+
+ var system = entManager.System();
+ system.SetWorkingState((ent, powerState), true);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(powerState.IsWorking, Is.True);
+ Assert.That(receiver.Load, Is.EqualTo(powerState.WorkingPowerDraw).Within(0.01f));
+ });
+ });
+
+ await pair.CleanReturnAsync();
+ }
+
+ ///
+ /// Asserts that switching from working to idle updates the power receiver load to the idle draw.
+ ///
+ [Test]
+ public async Task SetWorkingState_WorkingToIdle_UpdatesLoad()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var mapManager = server.ResolveDependency();
+ var entManager = server.ResolveDependency();
+ var mapSys = entManager.System();
+
+ await server.WaitAssertion(() =>
+ {
+ mapSys.CreateMap(out var mapId);
+ var grid = mapManager.CreateGridEntity(mapId);
+
+ mapSys.SetTile(grid, Vector2i.Zero, new Tile(1));
+
+ var ent = entManager.SpawnEntity("PowerStateApcReceiverDummy", grid.Owner.ToCoordinates());
+
+ var receiver = entManager.GetComponent(ent);
+ var powerState = entManager.GetComponent(ent);
+ var system = entManager.System();
+ Entity newEnt = (ent, powerState);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(powerState.IsWorking, Is.False);
+ Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f));
+ });
+
+ system.SetWorkingState(newEnt, true);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(powerState.IsWorking, Is.True);
+ Assert.That(receiver.Load, Is.EqualTo(powerState.WorkingPowerDraw).Within(0.01f));
+ });
+
+ system.SetWorkingState(newEnt, false);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(powerState.IsWorking, Is.False);
+ Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f));
+ });
+ });
+
+ await pair.CleanReturnAsync();
+ }
+
+ ///
+ /// Asserts that setting the working state to the current state does not change the power receiver load.
+ ///
+ [Test]
+ public async Task SetWorkingState_AlreadyInState_NoChange()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var mapManager = server.ResolveDependency();
+ var entManager = server.ResolveDependency();
+ var mapSys = entManager.System();
+
+ await server.WaitAssertion(() =>
+ {
+ mapSys.CreateMap(out var mapId);
+ var grid = mapManager.CreateGridEntity(mapId);
+
+ mapSys.SetTile(grid, Vector2i.Zero, new Tile(1));
+
+ var ent = entManager.SpawnEntity("PowerStateApcReceiverDummy", grid.Owner.ToCoordinates());
+
+ var receiver = entManager.GetComponent(ent);
+ var powerState = entManager.GetComponent(ent);
+ var system = entManager.System();
+ Entity valueTuple = (ent, powerState);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(powerState.IsWorking, Is.False);
+ Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f));
+ });
+
+ system.SetWorkingState(valueTuple, false);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(powerState.IsWorking, Is.False);
+ Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f));
+ });
+
+ system.SetWorkingState(valueTuple, true);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(powerState.IsWorking, Is.True);
+ Assert.That(receiver.Load, Is.EqualTo(powerState.WorkingPowerDraw).Within(0.01f));
+ });
+
+ system.SetWorkingState(valueTuple, true);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(powerState.IsWorking, Is.True);
+ Assert.That(receiver.Load, Is.EqualTo(powerState.WorkingPowerDraw).Within(0.01f));
+ });
+ });
+
+ await pair.CleanReturnAsync();
+ }
+}
+
diff --git a/Content.IntegrationTests/Tests/Power/StationPowerTests.cs b/Content.IntegrationTests/Tests/Power/StationPowerTests.cs
index e9692c2e79..2700d9b0bb 100644
--- a/Content.IntegrationTests/Tests/Power/StationPowerTests.cs
+++ b/Content.IntegrationTests/Tests/Power/StationPowerTests.cs
@@ -1,11 +1,11 @@
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameTicking;
-using Content.Server.Maps;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Power.NodeGroups;
using Content.Server.Power.Pow3r;
+using Content.Shared.Maps;
using Content.Shared.Power.Components;
using Content.Shared.NodeContainer;
using Robust.Server.GameObjects;
diff --git a/Content.IntegrationTests/Tests/Station/StationJobsTest.cs b/Content.IntegrationTests/Tests/Station/StationJobsTest.cs
index 3fee4a146c..4abd32bda0 100644
--- a/Content.IntegrationTests/Tests/Station/StationJobsTest.cs
+++ b/Content.IntegrationTests/Tests/Station/StationJobsTest.cs
@@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.Linq;
-using Content.Server.Maps;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
+using Content.Shared.Maps;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Shared.GameObjects;
diff --git a/Content.IntegrationTests/Tests/Strip/StrippableTest.cs b/Content.IntegrationTests/Tests/Strip/StrippableTest.cs
index f65bab1f81..d2ae3bd7ec 100644
--- a/Content.IntegrationTests/Tests/Strip/StrippableTest.cs
+++ b/Content.IntegrationTests/Tests/Strip/StrippableTest.cs
@@ -1,8 +1,6 @@
-using Content.Client.Interaction;
-using Content.IntegrationTests.Tests.Interaction;
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Shared.Strip.Components;
using Robust.Shared.GameObjects;
-using Robust.Shared.Input;
-using Robust.Shared.Map;
namespace Content.IntegrationTests.Tests.Strip;
@@ -10,37 +8,22 @@ public sealed class StrippableTest : InteractionTest
{
protected override string PlayerPrototype => "MobHuman";
+ ///
+ /// Tests that the stripping UI is opened when drag dropping from another mob onto the player.
+ ///
[Test]
public async Task DragDropOpensStrip()
{
- // Spawn one tile away
- TargetCoords = SEntMan.GetNetCoordinates(new EntityCoordinates(MapData.MapUid, 1, 0));
await SpawnTarget("MobHuman");
var userInterface = Comp(Target);
- Assert.That(userInterface.Actors.Count == 0);
+ Assert.That(userInterface.Actors, Is.Empty);
- // screenCoordinates diff needs to be larger than DragDropSystem._deadzone
- var screenX = CEntMan.System().Deadzone + 1f;
+ await DragDrop(Target.Value, Player);
- // Start drag
- await SetKey(EngineKeyFunctions.Use,
- BoundKeyState.Down,
- TargetCoords,
- Target,
- screenCoordinates: new ScreenCoordinates(screenX, 0f, WindowId.Main));
+ Assert.That(userInterface.Actors, Is.Not.Empty);
- await RunTicks(5);
-
- // End drag
- await SetKey(EngineKeyFunctions.Use,
- BoundKeyState.Up,
- PlayerCoords,
- Player,
- screenCoordinates: new ScreenCoordinates(0f, 0f, WindowId.Main));
-
- await RunTicks(5);
-
- Assert.That(userInterface.Actors.Count > 0);
+ Assert.That(CUiSys.IsUiOpen(CTarget.Value, StrippingUiKey.Key));
+ Assert.That(SUiSys.IsUiOpen(STarget.Value, StrippingUiKey.Key));
}
}
diff --git a/Content.IntegrationTests/Tests/Tiles/TileConstructionTests.cs b/Content.IntegrationTests/Tests/Tiles/TileConstructionTests.cs
index 0827e11b70..64c4c291fe 100644
--- a/Content.IntegrationTests/Tests/Tiles/TileConstructionTests.cs
+++ b/Content.IntegrationTests/Tests/Tiles/TileConstructionTests.cs
@@ -100,4 +100,25 @@ public sealed class TileConstructionTests : InteractionTest
await AssertEntityLookup((FloorItem, 1));
}
+
+ ///
+ /// Test brassPlating -> floor -> brassPlating using tilestacking
+ ///
+ [Test]
+ public async Task BrassPlatingPlace()
+ {
+ await SetTile(PlatingBrass);
+
+ // Brass Plating -> Tile
+ await InteractUsing(FloorItem);
+ Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
+ await AssertTile(Floor);
+ AssertGridCount(1);
+
+ // Tile -> Brass Plating
+ await InteractUsing(Pry);
+ await AssertTile(PlatingBrass);
+ AssertGridCount(1);
+ await AssertEntityLookup((FloorItem, 1));
+ }
}
diff --git a/Content.IntegrationTests/Tests/Tiles/TileStackRecursionTest.cs b/Content.IntegrationTests/Tests/Tiles/TileStackRecursionTest.cs
new file mode 100644
index 0000000000..52c5b03265
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Tiles/TileStackRecursionTest.cs
@@ -0,0 +1,66 @@
+using System.Collections.Generic;
+using System.Linq;
+using Content.Shared.CCVar;
+using Content.Shared.Maps;
+using Robust.Shared.Configuration;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.Tiles;
+
+public sealed class TileStackRecursionTest
+{
+ [Test]
+ public async Task TestBaseTurfRecursion()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var protoMan = pair.Server.ResolveDependency();
+ var cfg = pair.Server.ResolveDependency();
+ var maxTileHistoryLength = cfg.GetCVar(CCVars.TileStackLimit);
+ Assert.That(protoMan.TryGetInstances(out var tiles));
+ Assert.That(tiles, Is.Not.EqualTo(null));
+ //store the distance from the root node to the given tile node
+ var nodes = new List<(ProtoId, int)>();
+ //each element of list is a connection from BaseTurf tile to tile that goes on it
+ var edges = new List<(ProtoId, ProtoId)>();
+ foreach (var ctdef in tiles!.Values)
+ {
+ //at first, each node is unexplored and has infinite distance to root.
+ //we use space node as root - everything is supposed to start at space, and it's hardcoded into the game anyway.
+ if (ctdef.ID == ContentTileDefinition.SpaceID)
+ {
+ nodes.Insert(0, (ctdef.ID, 0)); //space is the first element
+ continue;
+ }
+ Assert.That(ctdef.BaseTurf != ctdef.ID);
+ nodes.Add((ctdef.ID, int.MaxValue));
+ if (ctdef.BaseTurf != null)
+ edges.Add((ctdef.BaseTurf.Value, ctdef.ID));
+ Assert.That(ctdef.BaseWhitelist, Does.Not.Contain(ctdef.ID));
+ edges.AddRange(ctdef.BaseWhitelist.Select(possibleTurf =>
+ (possibleTurf, new ProtoId(ctdef.ID))));
+ }
+ Bfs(nodes, edges, maxTileHistoryLength);
+ await pair.CleanReturnAsync();
+ }
+
+ private void Bfs(List<(ProtoId, int)> nodes, List<(ProtoId, ProtoId)> edges, int depthLimit)
+ {
+ var root = nodes[0];
+ var queue = new Queue<(ProtoId, int)>();
+ queue.Enqueue(root);
+ while (queue.Count != 0)
+ {
+ var u = queue.Dequeue();
+ //get a list of tiles that can be put on this tile
+ var adj = edges.Where(n => n.Item1 == u.Item1).Select(n => n.Item2);
+ var adjNodes = nodes.Where(n => adj.Contains(n.Item1)).ToList();
+ foreach (var node in adjNodes)
+ {
+ var adjNode = node;
+ adjNode.Item2 = u.Item2 + 1;
+ Assert.That(adjNode.Item2, Is.LessThanOrEqualTo(depthLimit)); //we can doomstack tiles on top of each other. Bad!
+ queue.Enqueue(adjNode);
+ }
+ }
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Toolshed/AdminTest.cs b/Content.IntegrationTests/Tests/Toolshed/AdminTest.cs
index ca70120ee9..cc440d0ccb 100644
--- a/Content.IntegrationTests/Tests/Toolshed/AdminTest.cs
+++ b/Content.IntegrationTests/Tests/Toolshed/AdminTest.cs
@@ -14,7 +14,7 @@ public sealed class AdminTest : ToolshedTest
var toolMan = Server.ResolveDependency();
var admin = Server.ResolveDependency();
var ignored = new HashSet()
- {typeof(LocTest).Assembly, typeof(Robust.UnitTesting.Shared.Toolshed.LocTest).Assembly};
+ {typeof(LocTest).Assembly};
await Server.WaitAssertion(() =>
{
diff --git a/Content.IntegrationTests/Tests/Toolshed/LocTest.cs b/Content.IntegrationTests/Tests/Toolshed/LocTest.cs
index fb210eba50..849f9e55d3 100644
--- a/Content.IntegrationTests/Tests/Toolshed/LocTest.cs
+++ b/Content.IntegrationTests/Tests/Toolshed/LocTest.cs
@@ -19,7 +19,7 @@ public sealed class LocTest : ToolshedTest
var locStrings = new HashSet();
var ignored = new HashSet()
- {typeof(LocTest).Assembly, typeof(Robust.UnitTesting.Shared.Toolshed.LocTest).Assembly};
+ {typeof(LocTest).Assembly};
await Server.WaitAssertion(() =>
{
diff --git a/Content.IntegrationTests/Tests/_DV/TraitSystemTest.cs b/Content.IntegrationTests/Tests/_DV/TraitSystemTest.cs
index 5daf838b25..ef9d5ccf4c 100644
--- a/Content.IntegrationTests/Tests/_DV/TraitSystemTest.cs
+++ b/Content.IntegrationTests/Tests/_DV/TraitSystemTest.cs
@@ -7,6 +7,7 @@ using Content.Shared._DV.Traits.Effects;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Nutrition.Components;
+using Content.Shared.StatusEffectNew;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
@@ -773,6 +774,7 @@ public sealed partial class TraitSystemTest
LogMan = IoCManager.Resolve(),
JobId = jobId,
SpeciesId = speciesId,
+ StatusEffects = entMan.System(),
};
}
@@ -790,6 +792,7 @@ public sealed partial class TraitSystemTest
CompFactory = factory,
LogMan = IoCManager.Resolve(),
Transform = entMan.GetComponent(player),
+ StatusEffects = entMan.System(),
};
}
diff --git a/Content.MapRenderer/Content.MapRenderer.csproj b/Content.MapRenderer/Content.MapRenderer.csproj
index 4320717732..98fb446bd5 100644
--- a/Content.MapRenderer/Content.MapRenderer.csproj
+++ b/Content.MapRenderer/Content.MapRenderer.csproj
@@ -3,19 +3,27 @@
Exe
..\bin\Content.MapRenderer\
false
- enable
true
+ false
+
+
-
+
+
+
+
+
+
+
diff --git a/Content.MapRenderer/Program.cs b/Content.MapRenderer/Program.cs
index 534b12565c..90f97a5786 100644
--- a/Content.MapRenderer/Program.cs
+++ b/Content.MapRenderer/Program.cs
@@ -7,7 +7,7 @@ using System.Text.Json;
using System.Threading.Tasks;
using Content.IntegrationTests;
using Content.MapRenderer.Painters;
-using Content.Server.Maps;
+using Content.Shared.Maps;
using Robust.Shared.Prototypes;
using Robust.UnitTesting.Pool;
using SixLabors.ImageSharp;
diff --git a/Content.MapRenderer/RenderMap.cs b/Content.MapRenderer/RenderMap.cs
index 4ebf4ee5d4..8d6087ec7a 100644
--- a/Content.MapRenderer/RenderMap.cs
+++ b/Content.MapRenderer/RenderMap.cs
@@ -1,5 +1,5 @@
using System.IO;
-using Content.Server.Maps;
+using Content.Shared.Maps;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
diff --git a/Content.Packaging/Content.Packaging.csproj b/Content.Packaging/Content.Packaging.csproj
index 9823e40b8e..d278b48734 100644
--- a/Content.Packaging/Content.Packaging.csproj
+++ b/Content.Packaging/Content.Packaging.csproj
@@ -2,13 +2,13 @@
Exe
enable
- enable
True
-
-
-
+
+
+
+
diff --git a/Content.Packaging/DepsHandler.cs b/Content.Packaging/DepsHandler.cs
new file mode 100644
index 0000000000..9907b97ba9
--- /dev/null
+++ b/Content.Packaging/DepsHandler.cs
@@ -0,0 +1,80 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Content.Packaging;
+
+///
+/// Helper class for working with .deps.json files.
+///
+public sealed class DepsHandler
+{
+ public readonly Dictionary Libraries = new();
+
+ public DepsHandler(DepsData data)
+ {
+ if (data.Targets.Count != 1)
+ throw new Exception("Expected exactly one target");
+
+ var target = data.Targets.Single().Value;
+
+ foreach (var (libNameAndVersion, libInfo) in target)
+ {
+ var split = libNameAndVersion.Split('/', 2);
+
+ Libraries.Add(split[0], libInfo);
+ }
+ }
+
+ public static DepsHandler Load(string depsFile)
+ {
+ using var f = File.OpenRead(depsFile);
+ var depsData = JsonSerializer.Deserialize(f) ?? throw new InvalidOperationException("Deps are null!");
+
+ return new DepsHandler(depsData);
+ }
+
+ public HashSet RecursiveGetLibrariesFrom(string start)
+ {
+ var found = new HashSet();
+
+ RecursiveAddLibraries(start, found);
+
+ return found;
+ }
+
+ private void RecursiveAddLibraries(string start, HashSet set)
+ {
+ if (!set.Add(start))
+ return;
+
+ var lib = Libraries[start];
+ if (lib.Dependencies == null)
+ return;
+
+ foreach (var dep in lib.Dependencies.Keys)
+ {
+ RecursiveAddLibraries(dep, set);
+ }
+ }
+
+ public sealed class DepsData
+ {
+ [JsonInclude, JsonPropertyName("targets")]
+ public required Dictionary> Targets;
+ }
+
+ public sealed class LibraryInfo
+ {
+ [JsonInclude, JsonPropertyName("dependencies")]
+ public Dictionary? Dependencies;
+
+ [JsonInclude, JsonPropertyName("runtime")]
+ public Dictionary? Runtime;
+
+ // Paths are like lib/netstandard2.0/JetBrains.Annotations.dll
+ public IEnumerable GetDllNames()
+ {
+ return Runtime == null ? [] : Runtime.Keys.Select(p => p.Split('/')[^1]);
+ }
+ }
+}
diff --git a/Content.Packaging/ServerPackaging.cs b/Content.Packaging/ServerPackaging.cs
index 2715e7a900..8e559d5a65 100644
--- a/Content.Packaging/ServerPackaging.cs
+++ b/Content.Packaging/ServerPackaging.cs
@@ -37,25 +37,9 @@ public static class ServerPackaging
.Select(o => o.Rid)
.ToList();
- private static readonly List ServerContentAssemblies = new()
- {
- "Content.Server.Database",
- "Content.Server",
- "Content.Shared",
- "Content.Shared.Database",
- };
-
- private static readonly List ServerExtraAssemblies = new()
- {
- // Python script had Npgsql. though we want Npgsql.dll as well soooo
- "Npgsql",
- "Microsoft",
- "NetCord",
- };
-
private static readonly List ServerNotExtraAssemblies = new()
{
- "Microsoft.CodeAnalysis",
+ "JetBrains.Annotations",
};
private static readonly HashSet BinSkipFolders = new()
@@ -181,23 +165,13 @@ public static class ServerPackaging
var inputPassCore = graph.InputCore;
var inputPassResources = graph.InputResources;
- var contentAssemblies = new List(ServerContentAssemblies);
// Additional assemblies that need to be copied such as EFCore.
var sourcePath = Path.Combine(contentDir, "bin", "Content.Server");
- // Should this be an asset pass?
- // For future archaeologists I just want audio rework to work and need the audio pass so
- // just porting this as is from python.
- foreach (var fullPath in Directory.EnumerateFiles(sourcePath, "*.*", SearchOption.AllDirectories))
- {
- var fileName = Path.GetFileNameWithoutExtension(fullPath);
+ var deps = DepsHandler.Load(Path.Combine(sourcePath, "Content.Server.deps.json"));
- if (!ServerNotExtraAssemblies.Any(o => fileName.StartsWith(o)) && ServerExtraAssemblies.Any(o => fileName.StartsWith(o)))
- {
- contentAssemblies.Add(fileName);
- }
- }
+ var contentAssemblies = GetContentAssemblyNamesToCopy(deps);
await RobustSharedPackaging.DoResourceCopy(
Path.Combine("RobustToolbox", "bin", "Server",
@@ -229,5 +203,21 @@ public static class ServerPackaging
inputPassResources.InjectFinished();
}
+ // This returns both content assemblies (e.g. Content.Server.dll) and dependencies (e.g. Npgsql)
+ private static IEnumerable GetContentAssemblyNamesToCopy(DepsHandler deps)
+ {
+ var depsContent = deps.RecursiveGetLibrariesFrom("Content.Server").SelectMany(GetLibraryNames);
+ var depsRobust = deps.RecursiveGetLibrariesFrom("Robust.Server").SelectMany(GetLibraryNames);
+
+ var depsContentExclusive = depsContent.Except(depsRobust).ToHashSet();
+
+ // Remove .dll suffix and apply filtering.
+ var names = depsContentExclusive.Select(p => p[..^4]).Where(p => !ServerNotExtraAssemblies.Any(p.StartsWith));
+
+ return names;
+
+ IEnumerable GetLibraryNames(string library) => deps.Libraries[library].GetDllNames();
+ }
+
private readonly record struct PlatformReg(string Rid, string TargetOs, bool BuildByDefault);
}
diff --git a/Content.PatreonParser/Content.PatreonParser.csproj b/Content.PatreonParser/Content.PatreonParser.csproj
index 1724ec0cea..bba47a062d 100644
--- a/Content.PatreonParser/Content.PatreonParser.csproj
+++ b/Content.PatreonParser/Content.PatreonParser.csproj
@@ -2,7 +2,7 @@
Exe
- net9.0
+ net10.0
enable
enable
true
diff --git a/Content.Replay/Content.Replay.csproj b/Content.Replay/Content.Replay.csproj
index 28cfc7f666..5008214522 100644
--- a/Content.Replay/Content.Replay.csproj
+++ b/Content.Replay/Content.Replay.csproj
@@ -1,26 +1,23 @@
- $(TargetFramework)
- 12
- false
false
..\bin\Content.Replay\
Exe
RA0032;nullable
- enable
+
+
-
-
-
-
+
+
+
diff --git a/Content.Replay/EntryPoint.cs b/Content.Replay/EntryPoint.cs
index ed6460a7e7..6a6658f5ef 100644
--- a/Content.Replay/EntryPoint.cs
+++ b/Content.Replay/EntryPoint.cs
@@ -19,8 +19,8 @@ public sealed class EntryPoint : GameClient
public override void Init()
{
base.Init();
- IoCManager.BuildGraph();
- IoCManager.InjectDependencies(this);
+ Dependencies.BuildGraph();
+ Dependencies.InjectDependencies(this);
}
public override void PostInit()
diff --git a/Content.Server.Database/Content.Server.Database.csproj b/Content.Server.Database/Content.Server.Database.csproj
index d98d0642db..22b718b363 100644
--- a/Content.Server.Database/Content.Server.Database.csproj
+++ b/Content.Server.Database/Content.Server.Database.csproj
@@ -1,16 +1,13 @@
-
- $(TargetFramework)
- 12
- false
false
..\bin\Content.Server.Database\
true
- enable
RA0003
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Content.Server.Database/Migrations/Postgres/20250723055137_AdminLogsCurtime.Designer.cs b/Content.Server.Database/Migrations/Postgres/20250723055137_AdminLogsCurtime.Designer.cs
deleted file mode 100644
index ea06967c89..0000000000
--- a/Content.Server.Database/Migrations/Postgres/20250723055137_AdminLogsCurtime.Designer.cs
+++ /dev/null
@@ -1,2125 +0,0 @@
-//
-using System;
-using System.Collections.Generic;
-using System.Net;
-using System.Text.Json;
-using Content.Server.Database;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
-using NpgsqlTypes;
-
-#nullable disable
-
-namespace Content.Server.Database.Migrations.Postgres
-{
- [DbContext(typeof(PostgresServerDbContext))]
- [Migration("20250723055137_AdminLogsCurtime")]
- partial class AdminLogsCurtime
- {
- ///
- protected override void BuildTargetModel(ModelBuilder modelBuilder)
- {
-#pragma warning disable 612, 618
- modelBuilder
- .HasAnnotation("ProductVersion", "9.0.1")
- .HasAnnotation("Relational:MaxIdentifierLength", 63);
-
- NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
-
- modelBuilder.Entity("Content.Server.Database.Admin", b =>
- {
- b.Property("UserId")
- .ValueGeneratedOnAdd()
- .HasColumnType("uuid")
- .HasColumnName("user_id");
-
- b.Property("AdminRankId")
- .HasColumnType("integer")
- .HasColumnName("admin_rank_id");
-
- b.Property("Deadminned")
- .HasColumnType("boolean")
- .HasColumnName("deadminned");
-
- b.Property("Suspended")
- .HasColumnType("boolean")
- .HasColumnName("suspended");
-
- b.Property("Title")
- .HasColumnType("text")
- .HasColumnName("title");
-
- b.HasKey("UserId")
- .HasName("PK_admin");
-
- b.HasIndex("AdminRankId")
- .HasDatabaseName("IX_admin_admin_rank_id");
-
- b.ToTable("admin", (string)null);
- });
-
- modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
- {
- b.Property("Id")
- .ValueGeneratedOnAdd()
- .HasColumnType("integer")
- .HasColumnName("admin_flag_id");
-
- NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
-
- b.Property("AdminId")
- .HasColumnType("uuid")
- .HasColumnName("admin_id");
-
- b.Property("Flag")
- .IsRequired()
- .HasColumnType("text")
- .HasColumnName("flag");
-
- b.Property("Negative")
- .HasColumnType("boolean")
- .HasColumnName("negative");
-
- b.HasKey("Id")
- .HasName("PK_admin_flag");
-
- b.HasIndex("AdminId")
- .HasDatabaseName("IX_admin_flag_admin_id");
-
- b.HasIndex("Flag", "AdminId")
- .IsUnique();
-
- b.ToTable("admin_flag", (string)null);
- });
-
- modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
- {
- b.Property("RoundId")
- .HasColumnType("integer")
- .HasColumnName("round_id");
-
- b.Property("Id")
- .HasColumnType("integer")
- .HasColumnName("admin_log_id");
-
- b.Property("CurTime")
- .HasColumnType("bigint")
- .HasColumnName("cur_time");
-
- b.Property("Date")
- .HasColumnType("timestamp with time zone")
- .HasColumnName("date");
-
- b.Property("Impact")
- .HasColumnType("smallint")
- .HasColumnName("impact");
-
- b.Property("Json")
- .IsRequired()
- .HasColumnType("jsonb")
- .HasColumnName("json");
-
- b.Property("Message")
- .IsRequired()
- .HasColumnType("text")
- .HasColumnName("message");
-
- b.Property("Type")
- .HasColumnType("integer")
- .HasColumnName("type");
-
- b.HasKey("RoundId", "Id")
- .HasName("PK_admin_log");
-
- b.HasIndex("Date");
-
- b.HasIndex("Message")
- .HasAnnotation("Npgsql:TsVectorConfig", "english");
-
- NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Message"), "GIN");
-
- b.HasIndex("Type")
- .HasDatabaseName("IX_admin_log_type");
-
- b.ToTable("admin_log", (string)null);
- });
-
- modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
- {
- b.Property("RoundId")
- .HasColumnType("integer")
- .HasColumnName("round_id");
-
- b.Property("LogId")
- .HasColumnType("integer")
- .HasColumnName("log_id");
-
- b.Property("PlayerUserId")
- .HasColumnType("uuid")
- .HasColumnName("player_user_id");
-
- b.HasKey("RoundId", "LogId", "PlayerUserId")
- .HasName("PK_admin_log_player");
-
- b.HasIndex("PlayerUserId")
- .HasDatabaseName("IX_admin_log_player_player_user_id");
-
- b.ToTable("admin_log_player", (string)null);
- });
-
- modelBuilder.Entity("Content.Server.Database.AdminMessage", b =>
- {
- b.Property("Id")
- .ValueGeneratedOnAdd()
- .HasColumnType("integer")
- .HasColumnName("admin_messages_id");
-
- NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
-
- b.Property("CreatedAt")
- .HasColumnType("timestamp with time zone")
- .HasColumnName("created_at");
-
- b.Property("CreatedById")
- .HasColumnType("uuid")
- .HasColumnName("created_by_id");
-
- b.Property("Deleted")
- .HasColumnType("boolean")
- .HasColumnName("deleted");
-
- b.Property("DeletedAt")
- .HasColumnType("timestamp with time zone")
- .HasColumnName("deleted_at");
-
- b.Property("DeletedById")
- .HasColumnType("uuid")
- .HasColumnName("deleted_by_id");
-
- b.Property("Dismissed")
- .HasColumnType("boolean")
- .HasColumnName("dismissed");
-
- b.Property("ExpirationTime")
- .HasColumnType("timestamp with time zone")
- .HasColumnName("expiration_time");
-
- b.Property