diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index c4de781a23..f099682b6a 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -24,6 +24,11 @@
/Content.*/Forensics/ @ficcialfaint
+/Content.*/Trigger/ @slarticodefast
+
+/Content.*/Stunnable/ @Princess-Cheeseballs
+/Content.*/Nutrition/ @Princess-Cheeseballs
+
# SKREEEE
/Content.*.Database/ @PJB3005 @DrSmugleaf
/Content.Shared.Database/Log*.cs @PJB3005 @DrSmugleaf @crazybrain23
@@ -37,3 +42,5 @@
/Content.*/NPC @metalgearsloth
/Content.*/Shuttles @metalgearsloth
/Content.*/Weapons @metalgearsloth
+
+/Content.Server/Discord/ @Simyon264
diff --git a/.github/workflows/publish-testing.yml b/.github/workflows/publish-testing.yml
index 6dacef1324..7a792ed2df 100644
--- a/.github/workflows/publish-testing.yml
+++ b/.github/workflows/publish-testing.yml
@@ -34,7 +34,7 @@ jobs:
run: dotnet build Content.Packaging --configuration Release --no-restore /m
- name: Package server
- run: dotnet run --project Content.Packaging server --platform win-x64 --platform linux-x64 --platform osx-x64 --platform linux-arm64
+ run: dotnet run --project Content.Packaging server --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 3ce5901841..3d54fef530 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -41,7 +41,7 @@ jobs:
run: dotnet build Content.Packaging --configuration Release --no-restore /m
- name: Package server
- run: dotnet run --project Content.Packaging server --platform win-x64 --platform linux-x64 --platform osx-x64 --platform linux-arm64
+ run: dotnet run --project Content.Packaging server --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release
diff --git a/.github/workflows/test-packaging.yml b/.github/workflows/test-packaging.yml
index ed137a19d0..325d8d04d9 100644
--- a/.github/workflows/test-packaging.yml
+++ b/.github/workflows/test-packaging.yml
@@ -60,7 +60,7 @@ jobs:
run: dotnet build Content.Packaging --configuration Release --no-restore /m
- name: Package server
- run: dotnet run --project Content.Packaging server --platform win-x64 --platform linux-x64 --platform osx-x64 --platform linux-arm64
+ run: dotnet run --project Content.Packaging server --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release
diff --git a/BuildChecker/git_helper.py b/BuildChecker/git_helper.py
index 96a7bdae2a..66d2463669 100644
--- a/BuildChecker/git_helper.py
+++ b/BuildChecker/git_helper.py
@@ -1,17 +1,19 @@
#!/usr/bin/env python3
-# Installs git hooks, updates them, updates submodules, that kind of thing.
+"""
+Installs git hooks, updates them, updates submodules, that kind of thing.
+"""
-import subprocess
-import sys
import os
import shutil
+import subprocess
+import sys
import time
from pathlib import Path
from typing import List
SOLUTION_PATH = Path("..") / "SpaceStation14.sln"
# If this doesn't match the saved version we overwrite them all.
-CURRENT_HOOKS_VERSION = "2"
+CURRENT_HOOKS_VERSION = "3"
QUIET = len(sys.argv) == 2 and sys.argv[1] == "--quiet"
@@ -25,12 +27,10 @@ def run_command(command: List[str], capture: bool = False) -> subprocess.Complet
sys.stdout.flush()
- completed = None
-
if capture:
- completed = subprocess.run(command, cwd="..", stdout=subprocess.PIPE)
+ completed = subprocess.run(command, stdout=subprocess.PIPE, text=True)
else:
- completed = subprocess.run(command, cwd="..")
+ completed = subprocess.run(command)
if completed.returncode != 0:
print("Error: command exited with code {}!".format(completed.returncode))
@@ -43,7 +43,7 @@ def update_submodules():
Updates all submodules.
"""
- if ('GITHUB_ACTIONS' in os.environ):
+ if 'GITHUB_ACTIONS' in os.environ:
return
if os.path.isfile("DISABLE_SUBMODULE_AUTOUPDATE"):
@@ -76,22 +76,21 @@ def install_hooks():
print("No hooks change detected.")
return
- with open("INSTALLED_HOOKS_VERSION", "w") as f:
- f.write(CURRENT_HOOKS_VERSION)
-
print("Hooks need updating.")
- hooks_target_dir = Path("..")/".git"/"hooks"
+ hooks_target_dir = Path(run_command(["git", "rev-parse", "--git-path", "hooks"], True).stdout.strip())
hooks_source_dir = Path("hooks")
# Clear entire tree since we need to kill deleted files too.
- for filename in os.listdir(str(hooks_target_dir)):
- os.remove(str(hooks_target_dir/filename))
+ for filename in os.listdir(hooks_target_dir):
+ os.remove(hooks_target_dir / filename)
- for filename in os.listdir(str(hooks_source_dir)):
+ for filename in os.listdir(hooks_source_dir):
print("Copying hook {}".format(filename))
- shutil.copy2(str(hooks_source_dir/filename),
- str(hooks_target_dir/filename))
+ shutil.copy2(hooks_source_dir / filename, hooks_target_dir / filename)
+
+ with open("INSTALLED_HOOKS_VERSION", "w") as f:
+ f.write(CURRENT_HOOKS_VERSION)
def reset_solution():
@@ -107,8 +106,7 @@ def reset_solution():
def check_for_zip_download():
# Check if .git exists,
- cur_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
- if not os.path.isdir(os.path.join(cur_dir, ".git")):
+ if run_command(["git", "rev-parse"]).returncode != 0:
print("It appears that you downloaded this repository directly from GitHub. (Using the .zip download option) \n"
"When downloading straight from GitHub, it leaves out important information that git needs to function. "
"Such as information to download the engine or even the ability to even be able to create contributions. \n"
diff --git a/BuildChecker/hooks/post-checkout b/BuildChecker/hooks/post-checkout
index c5662445c2..ee4309de1d 100755
--- a/BuildChecker/hooks/post-checkout
+++ b/BuildChecker/hooks/post-checkout
@@ -1,10 +1,10 @@
#!/bin/bash
-gitroot=`git rev-parse --show-toplevel`
+gitroot=$(git rev-parse --show-toplevel)
-cd "$gitroot/BuildChecker"
+cd "$gitroot/BuildChecker" || exit
-if [[ `uname` == MINGW* || `uname` == CYGWIN* ]]; then
+if [[ $(uname) == MINGW* || $(uname) == CYGWIN* ]]; then
# Windows
py -3 git_helper.py --quiet
else
diff --git a/BuildChecker/hooks/post-merge b/BuildChecker/hooks/post-merge
index 85fe61d966..5cf3d91120 100755
--- a/BuildChecker/hooks/post-merge
+++ b/BuildChecker/hooks/post-merge
@@ -1,5 +1,5 @@
#!/bin/bash
# Just call post-checkout since it does the same thing.
-gitroot=`git rev-parse --show-toplevel`
-bash "$gitroot/.git/hooks/post-checkout"
+gitroot=$(git rev-parse --git-path hooks)
+bash "$gitroot/post-checkout"
diff --git a/Content.Benchmarks/DeltaPressureBenchmark.cs b/Content.Benchmarks/DeltaPressureBenchmark.cs
new file mode 100644
index 0000000000..b31b3ed1a2
--- /dev/null
+++ b/Content.Benchmarks/DeltaPressureBenchmark.cs
@@ -0,0 +1,174 @@
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Diagnosers;
+using Content.IntegrationTests;
+using Content.IntegrationTests.Pair;
+using Content.Server.Atmos.Components;
+using Content.Server.Atmos.EntitySystems;
+using Content.Shared.Atmos.Components;
+using Content.Shared.CCVar;
+using Robust.Shared;
+using Robust.Shared.Analyzers;
+using Robust.Shared.Configuration;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Maths;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Benchmarks;
+
+///
+/// Spawns N number of entities with a and
+/// simulates them for a number of ticks M.
+///
+[Virtual]
+[GcServer(true)]
+//[MemoryDiagnoser]
+//[ThreadingDiagnoser]
+public class DeltaPressureBenchmark
+{
+ ///
+ /// Number of entities (windows, really) to spawn with a .
+ ///
+ [Params(1, 10, 100, 1000, 5000, 10000, 50000, 100000)]
+ public int EntityCount;
+
+ ///
+ /// Number of entities that each parallel processing job will handle.
+ ///
+ // [Params(1, 10, 100, 1000, 5000, 10000)] For testing how multithreading parameters affect performance (THESE TESTS TAKE 16+ HOURS TO RUN)
+ [Params(10)]
+ public int BatchSize;
+
+ ///
+ /// Number of entities to process per iteration in the DeltaPressure
+ /// processing loop.
+ ///
+ // [Params(100, 1000, 5000, 10000, 50000)]
+ [Params(1000)]
+ public int EntitiesPerIteration;
+
+ private readonly EntProtoId _windowProtoId = "Window";
+ private readonly EntProtoId _wallProtoId = "WallPlastitaniumIndestructible";
+
+ private TestPair _pair = default!;
+ private IEntityManager _entMan = default!;
+ private SharedMapSystem _map = default!;
+ private IRobustRandom _random = default!;
+ private IConfigurationManager _cvar = default!;
+ private ITileDefinitionManager _tileDefMan = default!;
+ private AtmosphereSystem _atmospereSystem = default!;
+
+ private Entity
+ _testEnt;
+
+ [GlobalSetup]
+ public async Task SetupAsync()
+ {
+ ProgramShared.PathOffset = "../../../../";
+ PoolManager.Startup();
+ _pair = await PoolManager.GetServerClient();
+ var server = _pair.Server;
+
+ var mapdata = await _pair.CreateTestMap();
+
+ _entMan = server.ResolveDependency();
+ _map = _entMan.System();
+ _random = server.ResolveDependency();
+ _cvar = server.ResolveDependency();
+ _tileDefMan = server.ResolveDependency();
+ _atmospereSystem = _entMan.System();
+
+ _random.SetSeed(69420); // Randomness needs to be deterministic for benchmarking.
+
+ _cvar.SetCVar(CCVars.DeltaPressureParallelToProcessPerIteration, EntitiesPerIteration);
+ _cvar.SetCVar(CCVars.DeltaPressureParallelBatchSize, BatchSize);
+
+ var plating = _tileDefMan["Plating"].TileId;
+
+ /*
+ Basically, we want to have a 5-wide grid of tiles.
+ Edges are walled, and the length of the grid is determined by N + 2.
+ Windows should only touch the top and bottom walls, and each other.
+ */
+
+ var length = EntityCount + 2; // ensures we can spawn exactly N windows between side walls
+ const int height = 5;
+
+ await server.WaitPost(() =>
+ {
+ // Fill required tiles (extend grid) with plating
+ for (var x = 0; x < length; x++)
+ {
+ for (var y = 0; y < height; y++)
+ {
+ _map.SetTile(mapdata.Grid, mapdata.Grid, new Vector2i(x, y), new Tile(plating));
+ }
+ }
+
+ // Spawn perimeter walls and windows row in the middle (y = 2)
+ const int midY = height / 2;
+ for (var x = 0; x < length; x++)
+ {
+ for (var y = 0; y < height; y++)
+ {
+ var coords = new EntityCoordinates(mapdata.Grid, x + 0.5f, y + 0.5f);
+
+ var isPerimeter = x == 0 || x == length - 1 || y == 0 || y == height - 1;
+ if (isPerimeter)
+ {
+ _entMan.SpawnEntity(_wallProtoId, coords);
+ continue;
+ }
+
+ // Spawn windows only on the middle row, spanning interior (excluding side walls)
+ if (y == midY)
+ {
+ _entMan.SpawnEntity(_windowProtoId, coords);
+ }
+ }
+ }
+ });
+
+ // Next we run the fixgridatmos command to ensure that we have some air on our grid.
+ // Wait a little bit as well.
+ // TODO: Unhardcode command magic string when fixgridatmos is an actual command we can ref and not just
+ // a stamp-on in AtmosphereSystem.
+ await _pair.WaitCommand("fixgridatmos " + mapdata.Grid.Owner, 1);
+
+ var uid = mapdata.Grid.Owner;
+ _testEnt = new Entity(
+ uid,
+ _entMan.GetComponent(uid),
+ _entMan.GetComponent(uid),
+ _entMan.GetComponent(uid),
+ _entMan.GetComponent(uid));
+ }
+
+ [Benchmark]
+ public async Task PerformFullProcess()
+ {
+ await _pair.Server.WaitPost(() =>
+ {
+ while (!_atmospereSystem.RunProcessingStage(_testEnt, AtmosphereProcessingState.DeltaPressure)) { }
+ });
+ }
+
+ [Benchmark]
+ public async Task PerformSingleRunProcess()
+ {
+ await _pair.Server.WaitPost(() =>
+ {
+ _atmospereSystem.RunProcessingStage(_testEnt, AtmosphereProcessingState.DeltaPressure);
+ });
+ }
+
+ [GlobalCleanup]
+ public async Task CleanupAsync()
+ {
+ await _pair.DisposeAsync();
+ PoolManager.Shutdown();
+ }
+}
diff --git a/Content.Benchmarks/MapLoadBenchmark.cs b/Content.Benchmarks/MapLoadBenchmark.cs
index de788234e5..3d527953b8 100644
--- a/Content.Benchmarks/MapLoadBenchmark.cs
+++ b/Content.Benchmarks/MapLoadBenchmark.cs
@@ -47,7 +47,7 @@ public class MapLoadBenchmark
PoolManager.Shutdown();
}
- public static readonly string[] MapsSource = { "Empty", "Saltern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Convex"};
+ public static string[] MapsSource { get; } = { "Empty", "Saltern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Convex"};
[ParamsSource(nameof(MapsSource))]
public string Map;
diff --git a/Content.Client/Actions/UI/ActionAlertTooltip.cs b/Content.Client/Actions/UI/ActionAlertTooltip.cs
index 2425cdefb9..664a67b406 100644
--- a/Content.Client/Actions/UI/ActionAlertTooltip.cs
+++ b/Content.Client/Actions/UI/ActionAlertTooltip.cs
@@ -21,7 +21,7 @@ namespace Content.Client.Actions.UI
///
public (TimeSpan Start, TimeSpan End)? Cooldown { get; set; }
- public ActionAlertTooltip(FormattedMessage name, FormattedMessage? desc, string? requires = null, FormattedMessage? charges = null)
+ public ActionAlertTooltip(FormattedMessage name, FormattedMessage? desc, string? requires = null)
{
_gameTiming = IoCManager.Resolve();
@@ -52,17 +52,6 @@ namespace Content.Client.Actions.UI
vbox.AddChild(description);
}
- if (charges != null && !string.IsNullOrWhiteSpace(charges.ToString()))
- {
- var chargesLabel = new RichTextLabel
- {
- MaxWidth = TooltipTextMaxWidth,
- StyleClasses = { StyleNano.StyleClassTooltipActionCharges }
- };
- chargesLabel.SetMessage(charges);
- vbox.AddChild(chargesLabel);
- }
-
vbox.AddChild(_cooldownLabel = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,
diff --git a/Content.Client/Administration/Managers/ClientAdminManager.cs b/Content.Client/Administration/Managers/ClientAdminManager.cs
index 0f740c8104..3f072691de 100644
--- a/Content.Client/Administration/Managers/ClientAdminManager.cs
+++ b/Content.Client/Administration/Managers/ClientAdminManager.cs
@@ -15,6 +15,7 @@ namespace Content.Client.Administration.Managers
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IClientNetManager _netMgr = default!;
[Dependency] private readonly IClientConGroupController _conGroup = default!;
+ [Dependency] private readonly IClientConsoleHost _host = default!;
[Dependency] private readonly IResourceManager _res = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IUserInterfaceManager _userInterface = default!;
@@ -86,12 +87,12 @@ namespace Content.Client.Administration.Managers
private void UpdateMessageRx(MsgUpdateAdminStatus message)
{
_availableCommands.Clear();
- var host = IoCManager.Resolve();
// Anything marked as Any we'll just add even if the server doesn't know about it.
- foreach (var (command, instance) in host.AvailableCommands)
+ foreach (var (command, instance) in _host.AvailableCommands)
{
- if (Attribute.GetCustomAttribute(instance.GetType(), typeof(AnyCommandAttribute)) == null) continue;
+ if (Attribute.GetCustomAttribute(instance.GetType(), typeof(AnyCommandAttribute)) == null)
+ continue;
_availableCommands.Add(command);
}
diff --git a/Content.Client/Administration/Systems/AdminFrozenSystem.cs b/Content.Client/Administration/Systems/AdminFrozenSystem.cs
deleted file mode 100644
index 885585f985..0000000000
--- a/Content.Client/Administration/Systems/AdminFrozenSystem.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-using Content.Shared.Administration;
-
-namespace Content.Client.Administration.Systems;
-
-public sealed class AdminFrozenSystem : SharedAdminFrozenSystem
-{
-}
diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs
index de9ccbbf50..fa92a3e18f 100644
--- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs
+++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs
@@ -57,12 +57,43 @@ public sealed partial class ObjectsTab : Control
private void TeleportTo(NetEntity nent)
{
- _console.ExecuteCommand($"tpto {nent}");
+ var selection = _selections[ObjectTypeOptions.SelectedId];
+ switch (selection)
+ {
+ case ObjectsTabSelection.Grids:
+ {
+ // directly teleport to the entity
+ _console.ExecuteCommand($"tpto {nent}");
+ }
+ break;
+ case ObjectsTabSelection.Maps:
+ {
+ // teleport to the map, not to the map entity (which is in nullspace)
+ if (!_entityManager.TryGetEntity(nent, out var map) || !_entityManager.TryGetComponent(map, out var mapComp))
+ break;
+ _console.ExecuteCommand($"tp 0 0 {mapComp.MapId}");
+ break;
+ }
+ case ObjectsTabSelection.Stations:
+ {
+ // teleport to the station's largest grid, not to the station entity (which is in nullspace)
+ if (!_entityManager.TryGetEntity(nent, out var station))
+ break;
+ var largestGrid = _entityManager.EntitySysManager.GetEntitySystem().GetLargestGrid(station.Value);
+ if (largestGrid == null)
+ break;
+ _console.ExecuteCommand($"tpto {largestGrid.Value}");
+ break;
+ }
+ default:
+ throw new NotImplementedException();
+ }
}
private void Delete(NetEntity nent)
{
_console.ExecuteCommand($"delete {nent}");
+ RefreshObjectList();
}
public void RefreshObjectList()
@@ -79,25 +110,21 @@ public sealed partial class ObjectsTab : Control
entities.AddRange(_entityManager.EntitySysManager.GetEntitySystem().GetStationNames());
break;
case ObjectsTabSelection.Grids:
- {
- var query = _entityManager.AllEntityQueryEnumerator();
- while (query.MoveNext(out var uid, out _, out var metadata))
{
- entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
- }
+ var query = _entityManager.AllEntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out _, out var metadata))
+ entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
- break;
- }
+ break;
+ }
case ObjectsTabSelection.Maps:
- {
- var query = _entityManager.AllEntityQueryEnumerator();
- while (query.MoveNext(out var uid, out _, out var metadata))
{
- entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
- }
+ var query = _entityManager.AllEntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out _, out var metadata))
+ entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
- break;
- }
+ break;
+ }
default:
throw new ArgumentOutOfRangeException(nameof(selection), selection, null);
}
diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml
index c561125a30..7c9dde79b5 100644
--- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml
+++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml
@@ -1,5 +1,6 @@
-
diff --git a/Content.Client/Lathe/UI/LatheMenu.xaml.cs b/Content.Client/Lathe/UI/LatheMenu.xaml.cs
index 66d875b0f2..a0dc241c29 100644
--- a/Content.Client/Lathe/UI/LatheMenu.xaml.cs
+++ b/Content.Client/Lathe/UI/LatheMenu.xaml.cs
@@ -11,6 +11,7 @@ using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
namespace Content.Client.Lathe.UI;
@@ -26,6 +27,10 @@ public sealed partial class LatheMenu : DefaultWindow
public event Action? OnServerListButtonPressed;
public event Action? RecipeQueueAction;
+ public event Action? QueueDeleteAction;
+ public event Action? QueueMoveUpAction;
+ public event Action? QueueMoveDownAction;
+ public event Action? DeleteFabricatingAction;
public List> Recipes = new();
@@ -50,12 +55,21 @@ public sealed partial class LatheMenu : DefaultWindow
};
AmountLineEdit.OnTextChanged += _ =>
{
+ if (int.TryParse(AmountLineEdit.Text, out var amount))
+ {
+ if (amount > LatheSystem.MaxItemsPerRequest)
+ AmountLineEdit.Text = LatheSystem.MaxItemsPerRequest.ToString();
+ else if (amount < 0)
+ AmountLineEdit.Text = "0";
+ }
+
PopulateRecipes();
};
FilterOption.OnItemSelected += OnItemSelected;
ServerListButton.OnPressed += a => OnServerListButtonPressed?.Invoke(a);
+ DeleteFabricating.OnPressed += _ => DeleteFabricatingAction?.Invoke();
}
public void SetEntity(EntityUid uid)
@@ -115,21 +129,50 @@ public sealed partial class LatheMenu : DefaultWindow
RecipeCount.Text = Loc.GetString("lathe-menu-recipe-count", ("count", recipesToShow.Count));
var sortedRecipesToShow = recipesToShow.OrderBy(_lathe.GetRecipeName);
- RecipeList.Children.Clear();
+
+ // Get the existing list of queue controls
+ var oldChildCount = RecipeList.ChildCount;
_entityManager.TryGetComponent(Entity, out LatheComponent? lathe);
+ int idx = 0;
foreach (var prototype in sortedRecipesToShow)
{
var canProduce = _lathe.CanProduce(Entity, prototype, quantity, component: lathe);
+ var tooltipFunction = () => GenerateTooltipText(prototype);
- var control = new RecipeControl(_lathe, prototype, () => GenerateTooltipText(prototype), canProduce, GetRecipeDisplayControl(prototype));
- control.OnButtonPressed += s =>
+ if (idx >= oldChildCount)
{
- if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount <= 0)
- amount = 1;
- RecipeQueueAction?.Invoke(s, amount);
- };
- RecipeList.AddChild(control);
+ var control = new RecipeControl(_lathe, prototype, tooltipFunction, canProduce, GetRecipeDisplayControl(prototype));
+ control.OnButtonPressed += s =>
+ {
+ if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount <= 0)
+ amount = 1;
+ RecipeQueueAction?.Invoke(s, amount);
+ };
+ RecipeList.AddChild(control);
+ }
+ else
+ {
+ var child = RecipeList.GetChild(idx) as RecipeControl;
+
+ if (child == null)
+ {
+ DebugTools.Assert($"Lathe menu recipe control at {idx} is not of type RecipeControl"); // Something's gone terribly wrong.
+ continue;
+ }
+
+ child.SetRecipe(prototype);
+ child.SetTooltipSupplier(tooltipFunction);
+ child.SetCanProduce(canProduce);
+ child.SetDisplayControl(GetRecipeDisplayControl(prototype));
+ }
+ idx++;
+ }
+
+ // Shrink list if new list is shorter than old list.
+ for (var childIdx = oldChildCount - 1; idx <= childIdx; childIdx--)
+ {
+ RecipeList.RemoveChild(childIdx);
}
}
@@ -223,25 +266,53 @@ public sealed partial class LatheMenu : DefaultWindow
/// Populates the build queue list with all queued items
///
///
- public void PopulateQueueList(IReadOnlyCollection> queue)
+ public void PopulateQueueList(IReadOnlyCollection queue)
{
- QueueList.DisposeAllChildren();
+ // Get the existing list of queue controls
+ var oldChildCount = QueueList.ChildCount;
- var idx = 1;
- foreach (var recipeProto in queue)
+ var idx = 0;
+ foreach (var batch in queue)
{
- var recipe = _prototypeManager.Index(recipeProto);
- var queuedRecipeBox = new BoxContainer();
- queuedRecipeBox.Orientation = BoxContainer.LayoutOrientation.Horizontal;
+ var recipe = _prototypeManager.Index(batch.Recipe);
- queuedRecipeBox.AddChild(GetRecipeDisplayControl(recipe));
+ var itemName = _lathe.GetRecipeName(batch.Recipe);
+ string displayText;
+ if (batch.ItemsRequested > 1)
+ displayText = Loc.GetString("lathe-menu-item-batch", ("index", idx + 1), ("name", itemName), ("printed", batch.ItemsPrinted), ("total", batch.ItemsRequested));
+ else
+ displayText = Loc.GetString("lathe-menu-item-single", ("index", idx + 1), ("name", itemName));
- var queuedRecipeLabel = new Label();
- queuedRecipeLabel.Text = $"{idx}. {_lathe.GetRecipeName(recipe)}";
- queuedRecipeBox.AddChild(queuedRecipeLabel);
- QueueList.AddChild(queuedRecipeBox);
+ if (idx >= oldChildCount)
+ {
+ var queuedRecipeBox = new QueuedRecipeControl(displayText, idx, GetRecipeDisplayControl(recipe));
+ queuedRecipeBox.OnDeletePressed += s => QueueDeleteAction?.Invoke(s);
+ queuedRecipeBox.OnMoveUpPressed += s => QueueMoveUpAction?.Invoke(s);
+ queuedRecipeBox.OnMoveDownPressed += s => QueueMoveDownAction?.Invoke(s);
+ QueueList.AddChild(queuedRecipeBox);
+ }
+ else
+ {
+ var child = QueueList.GetChild(idx) as QueuedRecipeControl;
+
+ if (child == null)
+ {
+ DebugTools.Assert($"Lathe menu queued recipe control at {idx} is not of type QueuedRecipeControl"); // Something's gone terribly wrong.
+ continue;
+ }
+
+ child.SetDisplayText(displayText);
+ child.SetIndex(idx);
+ child.SetDisplayControl(GetRecipeDisplayControl(recipe));
+ }
idx++;
}
+
+ // Shrink list if new list is shorter than old list.
+ for (var childIdx = oldChildCount - 1; idx <= childIdx; childIdx--)
+ {
+ QueueList.RemoveChild(childIdx);
+ }
}
public void SetQueueInfo(ProtoId? recipeProto)
diff --git a/Content.Client/Lathe/UI/QueuedRecipeControl.xaml b/Content.Client/Lathe/UI/QueuedRecipeControl.xaml
new file mode 100644
index 0000000000..b1d4b496a1
--- /dev/null
+++ b/Content.Client/Lathe/UI/QueuedRecipeControl.xaml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+ Caution
+ OpenLeft
+
+
+
+
diff --git a/Content.Client/Lathe/UI/QueuedRecipeControl.xaml.cs b/Content.Client/Lathe/UI/QueuedRecipeControl.xaml.cs
new file mode 100644
index 0000000000..69c8da6d7b
--- /dev/null
+++ b/Content.Client/Lathe/UI/QueuedRecipeControl.xaml.cs
@@ -0,0 +1,56 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Lathe.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class QueuedRecipeControl : Control
+{
+ public Action? OnDeletePressed;
+ public Action? OnMoveUpPressed;
+ public Action? OnMoveDownPressed;
+
+ private int _index;
+
+ public QueuedRecipeControl(string displayText, int index, Control displayControl)
+ {
+ RobustXamlLoader.Load(this);
+
+ SetDisplayText(displayText);
+ SetDisplayControl(displayControl);
+ SetIndex(index);
+ _index = index;
+
+ MoveUp.OnPressed += (_) =>
+ {
+ OnMoveUpPressed?.Invoke(_index);
+ };
+
+ MoveDown.OnPressed += (_) =>
+ {
+ OnMoveDownPressed?.Invoke(_index);
+ };
+
+ Delete.OnPressed += (_) =>
+ {
+ OnDeletePressed?.Invoke(_index);
+ };
+ }
+
+ public void SetDisplayText(string displayText)
+ {
+ RecipeName.Text = displayText;
+ }
+
+ public void SetDisplayControl(Control displayControl)
+ {
+ RecipeDisplayContainer.Children.Clear();
+ RecipeDisplayContainer.AddChild(displayControl);
+ }
+
+ public void SetIndex(int index)
+ {
+ _index = index;
+ }
+}
diff --git a/Content.Client/Lathe/UI/RecipeControl.xaml.cs b/Content.Client/Lathe/UI/RecipeControl.xaml.cs
index 4f438c8a8e..277fe12c04 100644
--- a/Content.Client/Lathe/UI/RecipeControl.xaml.cs
+++ b/Content.Client/Lathe/UI/RecipeControl.xaml.cs
@@ -2,6 +2,7 @@ using Content.Shared.Research.Prototypes;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
namespace Content.Client.Lathe.UI;
@@ -11,20 +12,47 @@ public sealed partial class RecipeControl : Control
public Action? OnButtonPressed;
public Func TooltipTextSupplier;
+ private ProtoId _recipeId;
+ private LatheSystem _latheSystem;
+
public RecipeControl(LatheSystem latheSystem, LatheRecipePrototype recipe, Func tooltipTextSupplier, bool canProduce, Control displayControl)
{
RobustXamlLoader.Load(this);
- RecipeName.Text = latheSystem.GetRecipeName(recipe);
- RecipeDisplayContainer.AddChild(displayControl);
- Button.Disabled = !canProduce;
+ _latheSystem = latheSystem;
+ _recipeId = recipe.ID;
TooltipTextSupplier = tooltipTextSupplier;
- Button.TooltipSupplier = SupplyTooltip;
+ SetRecipe(recipe);
+ SetCanProduce(canProduce);
+ SetDisplayControl(displayControl);
Button.OnPressed += (_) =>
{
- OnButtonPressed?.Invoke(recipe.ID);
+ OnButtonPressed?.Invoke(_recipeId);
};
+ Button.TooltipSupplier = SupplyTooltip;
+ }
+
+ public void SetRecipe(LatheRecipePrototype recipe)
+ {
+ RecipeName.Text = _latheSystem.GetRecipeName(recipe);
+ _recipeId = recipe.ID;
+ }
+
+ public void SetTooltipSupplier(Func tooltipTextSupplier)
+ {
+ TooltipTextSupplier = tooltipTextSupplier;
+ }
+
+ public void SetCanProduce(bool canProduce)
+ {
+ Button.Disabled = !canProduce;
+ }
+
+ public void SetDisplayControl(Control displayControl)
+ {
+ RecipeDisplayContainer.Children.Clear();
+ RecipeDisplayContainer.AddChild(displayControl);
}
private Control? SupplyTooltip(Control sender)
diff --git a/Content.Client/Light/EntitySystems/LightBulbSystem.cs b/Content.Client/Light/EntitySystems/LightBulbSystem.cs
index c028cc64c6..a3698fc199 100644
--- a/Content.Client/Light/EntitySystems/LightBulbSystem.cs
+++ b/Content.Client/Light/EntitySystems/LightBulbSystem.cs
@@ -1,36 +1,46 @@
using Content.Shared.Light.Components;
+using Content.Shared.Light.EntitySystems;
using Robust.Client.GameObjects;
-namespace Content.Client.Light.Visualizers;
+namespace Content.Client.Light.EntitySystems;
-public sealed class LightBulbSystem : VisualizerSystem
+public sealed class LightBulbSystem : SharedLightBulbSystem
{
- protected override void OnAppearanceChange(EntityUid uid, LightBulbComponent comp, ref AppearanceChangeEvent args)
+ [Dependency] private readonly AppearanceSystem _appearance = default!;
+ [Dependency] private readonly SpriteSystem _sprite = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnAppearanceChange);
+ }
+
+ private void OnAppearanceChange(EntityUid uid, LightBulbComponent comp, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
// update sprite state
- if (AppearanceSystem.TryGetData(uid, LightBulbVisuals.State, out var state, args.Component))
+ if (_appearance.TryGetData(uid, LightBulbVisuals.State, out var state, args.Component))
{
switch (state)
{
case LightBulbState.Normal:
- SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.NormalSpriteState);
+ _sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.NormalSpriteState);
break;
case LightBulbState.Broken:
- SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BrokenSpriteState);
+ _sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BrokenSpriteState);
break;
case LightBulbState.Burned:
- SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BurnedSpriteState);
+ _sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BurnedSpriteState);
break;
}
}
// also update sprites color
- if (AppearanceSystem.TryGetData(uid, LightBulbVisuals.Color, out var color, args.Component))
+ if (_appearance.TryGetData(uid, LightBulbVisuals.Color, out var color, args.Component))
{
- SpriteSystem.SetColor((uid, args.Sprite), color);
+ _sprite.SetColor((uid, args.Sprite), color);
}
}
}
diff --git a/Content.Client/Light/EntitySystems/PoweredLightSystem.cs b/Content.Client/Light/EntitySystems/PoweredLightSystem.cs
new file mode 100644
index 0000000000..b8a6b16da4
--- /dev/null
+++ b/Content.Client/Light/EntitySystems/PoweredLightSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared.Light.EntitySystems;
+
+namespace Content.Client.Light.EntitySystems;
+
+public sealed class PoweredLightSystem : SharedPoweredLightSystem;
diff --git a/Content.Client/Lobby/LobbyUIController.cs b/Content.Client/Lobby/LobbyUIController.cs
index 121e8dbe71..ec052adea5 100644
--- a/Content.Client/Lobby/LobbyUIController.cs
+++ b/Content.Client/Lobby/LobbyUIController.cs
@@ -72,6 +72,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered RefreshProfileEditor());
+ _configurationManager.OnValueChanged(CCVars.GameRoleLoadoutTimers, _ => RefreshProfileEditor());
_configurationManager.OnValueChanged(CCVars.GameRoleWhitelist, _ => RefreshProfileEditor());
}
diff --git a/Content.Client/Medical/SuitSensors/SuitSensorSystem.cs b/Content.Client/Medical/SuitSensors/SuitSensorSystem.cs
new file mode 100644
index 0000000000..75868e08d9
--- /dev/null
+++ b/Content.Client/Medical/SuitSensors/SuitSensorSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared.Medical.SuitSensors;
+
+namespace Content.Client.Medical.SuitSensors;
+
+public sealed class SuitSensorSystem : SharedSuitSensorSystem;
diff --git a/Content.Client/Morgue/CrematoriumSystem.cs b/Content.Client/Morgue/CrematoriumSystem.cs
new file mode 100644
index 0000000000..66eac263c2
--- /dev/null
+++ b/Content.Client/Morgue/CrematoriumSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared.Morgue;
+
+namespace Content.Client.Morgue;
+
+public sealed class CrematoriumSystem : SharedCrematoriumSystem;
diff --git a/Content.Client/Morgue/MorgueSystem.cs b/Content.Client/Morgue/MorgueSystem.cs
new file mode 100644
index 0000000000..b8d2f109fb
--- /dev/null
+++ b/Content.Client/Morgue/MorgueSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared.Morgue;
+
+namespace Content.Client.Morgue;
+
+public sealed class MorgueSystem : SharedMorgueSystem;
diff --git a/Content.Client/Movement/Systems/ContentEyeSystem.cs b/Content.Client/Movement/Systems/ContentEyeSystem.cs
index 518a4a1bd4..a332d25f9a 100644
--- a/Content.Client/Movement/Systems/ContentEyeSystem.cs
+++ b/Content.Client/Movement/Systems/ContentEyeSystem.cs
@@ -1,7 +1,6 @@
using System.Numerics;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
-using Robust.Client.GameObjects;
using Robust.Client.Player;
namespace Content.Client.Movement.Systems;
@@ -63,4 +62,15 @@ public sealed class ContentEyeSystem : SharedContentEyeSystem
UpdateEyeOffset((entity, eyeComponent));
}
}
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+ // TODO: Ideally we wouldn't want this to run in both FrameUpdate and Update, but we kind of have to since the visual update happens in FrameUpdate, but interaction update happens in Update. It's a workaround and a better solution should be found.
+ var eyeEntities = AllEntityQuery();
+ while (eyeEntities.MoveNext(out var entity, out ContentEyeComponent? contentComponent, out EyeComponent? eyeComponent))
+ {
+ UpdateEyeOffset((entity, eyeComponent));
+ }
+ }
}
diff --git a/Content.Client/Movement/Systems/EyeCursorOffsetSystem.cs b/Content.Client/Movement/Systems/EyeCursorOffsetSystem.cs
index eb524cf4ee..174ae2dd97 100644
--- a/Content.Client/Movement/Systems/EyeCursorOffsetSystem.cs
+++ b/Content.Client/Movement/Systems/EyeCursorOffsetSystem.cs
@@ -1,10 +1,10 @@
using System.Numerics;
using Content.Client.Movement.Components;
+using Content.Client.Viewport;
using Content.Shared.Camera;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Shared.Map;
-using Robust.Client.Player;
namespace Content.Client.Movement.Systems;
@@ -12,13 +12,10 @@ public sealed partial class EyeCursorOffsetSystem : EntitySystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
- [Dependency] private readonly IPlayerManager _player = default!;
- [Dependency] private readonly SharedTransformSystem _transform = default!;
- [Dependency] private readonly IClyde _clyde = default!;
// This value is here to make sure the user doesn't have to move their mouse
// all the way out to the edge of the screen to get the full offset.
- static private float _edgeOffset = 0.9f;
+ private static float _edgeOffset = 0.8f;
public override void Initialize()
{
@@ -38,25 +35,29 @@ public sealed partial class EyeCursorOffsetSystem : EntitySystem
public Vector2? OffsetAfterMouse(EntityUid uid, EyeCursorOffsetComponent? component)
{
- var localPlayer = _player.LocalEntity;
- var mousePos = _inputManager.MouseScreenPosition;
- var screenSize = _clyde.MainWindow.Size;
- var minValue = MathF.Min(screenSize.X / 2, screenSize.Y / 2) * _edgeOffset;
-
- var mouseNormalizedPos = new Vector2(-(mousePos.X - screenSize.X / 2) / minValue, (mousePos.Y - screenSize.Y / 2) / minValue); // X needs to be inverted here for some reason, otherwise it ends up flipped.
-
- if (localPlayer == null)
+ // We need the main viewport where the game content is displayed, as certain UI layouts (e.g. Separated HUD) can make it a different size to the game window.
+ if (_eyeManager.MainViewport is not ScalingViewport vp)
return null;
- var playerPos = _transform.GetWorldPosition(localPlayer.Value);
+ var mousePos = _inputManager.MouseScreenPosition.Position; // TODO: If we ever get a right-aligned Separated HUD setting, this might need to be adjusted for that.
+
+ var viewportSize = vp.PixelSize; // The size of the game viewport, including black bars - does not include the chatbox in Separated HUD view.
+ var scalingViewportSize = vp.ViewportSize * vp.CurrentRenderScale; // The size of the viewport in which the game is rendered (i.e. not including black bars). Note! Can extend outside the game window with certain zoom settings!
+ var visibleViewportSize = Vector2.Min(viewportSize, scalingViewportSize); // The size of the game viewport that is "actually visible" to the player, cutting off over-extensions and not counting black bar padding.
+
+ Matrix3x2.Invert(_eyeManager.MainViewport.GetLocalToScreenMatrix(), out var matrix);
+ var mouseCoords = Vector2.Transform(mousePos, matrix); // Gives the mouse position inside of the *scaling viewport*, i.e. 0,0 is inside the black bars. Note! 0,0 can be outside the game window with certain zoom settings!
+
+ var boundedMousePos = Vector2.Clamp(Vector2.Min(mouseCoords, mousePos), Vector2.Zero, visibleViewportSize); // Mouse position inside the visible game viewport's bounds.
+
+ var offsetRadius = MathF.Min(visibleViewportSize.X / 2f, visibleViewportSize.Y / 2f) * _edgeOffset;
+ var mouseNormalizedPos = new Vector2(-(boundedMousePos.X - visibleViewportSize.X / 2f) / offsetRadius, (boundedMousePos.Y - visibleViewportSize.Y / 2f) / offsetRadius);
if (component == null)
- {
component = EnsureComp(uid);
- }
// Doesn't move the offset if the mouse has left the game window!
- if (mousePos.Window != WindowId.Invalid)
+ if (_inputManager.MouseScreenPosition.Window != WindowId.Invalid)
{
// The offset must account for the in-world rotation.
var eyeRotation = _eyeManager.CurrentEye.Rotation;
@@ -77,7 +78,7 @@ public sealed partial class EyeCursorOffsetSystem : EntitySystem
Vector2 vectorOffset = component.TargetPosition - component.CurrentPosition;
if (vectorOffset.Length() > component.OffsetSpeed)
{
- vectorOffset = vectorOffset.Normalized() * component.OffsetSpeed;
+ vectorOffset = vectorOffset.Normalized() * component.OffsetSpeed; // TODO: Probably needs to properly account for time delta or something.
}
component.CurrentPosition += vectorOffset;
}
diff --git a/Content.Client/NPC/PathfindingSystem.cs b/Content.Client/NPC/PathfindingSystem.cs
index 0c72a8f99f..dc8fd98433 100644
--- a/Content.Client/NPC/PathfindingSystem.cs
+++ b/Content.Client/NPC/PathfindingSystem.cs
@@ -20,6 +20,7 @@ namespace Content.Client.NPC
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
+ [Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IResourceCache _cache = default!;
[Dependency] private readonly NPCSteeringSystem _steering = default!;
[Dependency] private readonly MapSystem _mapSystem = default!;
@@ -30,17 +31,15 @@ namespace Content.Client.NPC
get => _modes;
set
{
- var overlayManager = IoCManager.Resolve();
-
if (value == PathfindingDebugMode.None)
{
Breadcrumbs.Clear();
Polys.Clear();
- overlayManager.RemoveOverlay();
+ _overlayManager.RemoveOverlay();
}
- else if (!overlayManager.HasOverlay())
+ else if (!_overlayManager.HasOverlay())
{
- overlayManager.AddOverlay(new PathfindingOverlay(EntityManager, _eyeManager, _inputManager, _mapManager, _cache, this, _mapSystem, _transformSystem));
+ _overlayManager.AddOverlay(new PathfindingOverlay(EntityManager, _eyeManager, _inputManager, _mapManager, _cache, this, _mapSystem, _transformSystem));
}
if ((value & PathfindingDebugMode.Steering) != 0x0)
diff --git a/Content.Client/Physics/Controllers/MoverController.cs b/Content.Client/Physics/Controllers/MoverController.cs
index 2fe5c18fe0..0f95a817c9 100644
--- a/Content.Client/Physics/Controllers/MoverController.cs
+++ b/Content.Client/Physics/Controllers/MoverController.cs
@@ -120,8 +120,8 @@ public sealed class MoverController : SharedMoverController
base.SetSprinting(entity, subTick, walking);
if (walking && _cfg.GetCVar(CCVars.ToggleWalk))
- _alerts.ShowAlert(entity, WalkingAlert, showCooldown: false, autoRemove: false);
+ _alerts.ShowAlert(entity.Owner, WalkingAlert, showCooldown: false, autoRemove: false);
else
- _alerts.ClearAlert(entity, WalkingAlert);
+ _alerts.ClearAlert(entity.Owner, WalkingAlert);
}
}
diff --git a/Content.Client/Power/Components/ApcPowerReceiverComponent.cs b/Content.Client/Power/Components/ApcPowerReceiverComponent.cs
index fbebcb7cf8..ead686189e 100644
--- a/Content.Client/Power/Components/ApcPowerReceiverComponent.cs
+++ b/Content.Client/Power/Components/ApcPowerReceiverComponent.cs
@@ -5,4 +5,5 @@ namespace Content.Client.Power.Components;
[RegisterComponent]
public sealed partial class ApcPowerReceiverComponent : SharedApcPowerReceiverComponent
{
+ public override float Load { get; set; }
}
diff --git a/Content.Client/Radiation/Overlays/RadiationDebugOverlay.cs b/Content.Client/Radiation/Overlays/RadiationDebugOverlay.cs
index 784c39a6ce..1f060e532d 100644
--- a/Content.Client/Radiation/Overlays/RadiationDebugOverlay.cs
+++ b/Content.Client/Radiation/Overlays/RadiationDebugOverlay.cs
@@ -11,6 +11,8 @@ namespace Content.Client.Radiation.Overlays;
public sealed class RadiationDebugOverlay : Overlay
{
[Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IResourceCache _cache = default!;
+
private readonly SharedMapSystem _mapSystem;
private readonly RadiationSystem _radiation;
@@ -24,8 +26,7 @@ public sealed class RadiationDebugOverlay : Overlay
_radiation = _entityManager.System();
_mapSystem = _entityManager.System();
- var cache = IoCManager.Resolve();
- _font = new VectorFont(cache.GetResource("/Fonts/NotoSans/NotoSans-Regular.ttf"), 8);
+ _font = new VectorFont(_cache.GetResource("/Fonts/NotoSans/NotoSans-Regular.ttf"), 8);
}
protected override void Draw(in OverlayDrawArgs args)
diff --git a/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml.cs b/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml.cs
index 06e5674d9c..24583d2776 100644
--- a/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml.cs
+++ b/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml.cs
@@ -28,6 +28,8 @@ public sealed partial class RoboticsConsoleWindow : FancyWindow
public EntityUid Entity;
+ private bool _allowBorgControl = true;
+
public RoboticsConsoleWindow()
{
RobustXamlLoader.Load(this);
@@ -72,6 +74,7 @@ public sealed partial class RoboticsConsoleWindow : FancyWindow
public void UpdateState(RoboticsConsoleState state)
{
_cyborgs = state.Cyborgs;
+ _allowBorgControl = state.AllowBorgControl;
// clear invalid selection
if (_selected is {} selected && !_cyborgs.ContainsKey(selected))
@@ -85,8 +88,8 @@ public sealed partial class RoboticsConsoleWindow : FancyWindow
PopulateData();
var locked = _lock.IsLocked(Entity);
- DangerZone.Visible = !locked;
- LockedMessage.Visible = locked;
+ DangerZone.Visible = !locked && _allowBorgControl;
+ LockedMessage.Visible = locked && _allowBorgControl; // Only show if locked AND control is allowed
}
private void PopulateCyborgs()
@@ -120,11 +123,19 @@ public sealed partial class RoboticsConsoleWindow : FancyWindow
BorgSprite.Texture = _sprite.Frame0(data.ChassisSprite!);
var batteryColor = data.Charge switch {
- < 0.2f => "red",
- < 0.4f => "orange",
- < 0.6f => "yellow",
- < 0.8f => "green",
- _ => "blue"
+ < 0.2f => "#FF6C7F", // red
+ < 0.4f => "#EF973C", // orange
+ < 0.6f => "#E8CB2D", // yellow
+ < 0.8f => "#30CC19", // green
+ _ => "#00D3B8" // cyan
+ };
+
+ var hpPercentColor = data.HpPercent switch {
+ < 0.2f => "#FF6C7F", // red
+ < 0.4f => "#EF973C", // orange
+ < 0.6f => "#E8CB2D", // yellow
+ < 0.8f => "#30CC19", // green
+ _ => "#00D3B8" // cyan
};
var text = new FormattedMessage();
@@ -132,12 +143,14 @@ public sealed partial class RoboticsConsoleWindow : FancyWindow
text.AddMarkupOrThrow(Loc.GetString("robotics-console-designation"));
text.AddText($" {data.Name}\n"); // prevent players trolling by naming borg [color=red]satan[/color]
text.AddMarkupOrThrow($"{Loc.GetString("robotics-console-battery", ("charge", (int)(data.Charge * 100f)), ("color", batteryColor))}\n");
+ text.AddMarkupOrThrow($"{Loc.GetString("robotics-console-hp", ("hp", (int)(data.HpPercent * 100f)), ("color", hpPercentColor))}\n");
text.AddMarkupOrThrow($"{Loc.GetString("robotics-console-brain", ("brain", data.HasBrain))}\n");
text.AddMarkupOrThrow(Loc.GetString("robotics-console-modules", ("count", data.ModuleCount)));
BorgInfo.SetMessage(text);
// how the turntables
- DisableButton.Disabled = !(data.HasBrain && data.CanDisable);
+ DisableButton.Disabled = !_allowBorgControl || !(data.HasBrain && data.CanDisable);
+ DestroyButton.Disabled = !_allowBorgControl;
}
protected override void FrameUpdate(FrameEventArgs args)
diff --git a/Content.Client/Shuttles/Systems/ShuttleSystem.EmergencyConsole.cs b/Content.Client/Shuttles/Systems/ShuttleSystem.EmergencyConsole.cs
index 56b54c176a..f6e6594af5 100644
--- a/Content.Client/Shuttles/Systems/ShuttleSystem.EmergencyConsole.cs
+++ b/Content.Client/Shuttles/Systems/ShuttleSystem.EmergencyConsole.cs
@@ -16,20 +16,20 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem
get => _enableShuttlePosition;
set
{
- if (_enableShuttlePosition == value) return;
+ if (_enableShuttlePosition == value)
+ return;
_enableShuttlePosition = value;
- var overlayManager = IoCManager.Resolve();
if (_enableShuttlePosition)
{
_overlay = new EmergencyShuttleOverlay(EntityManager.TransformQuery, XformSystem);
- overlayManager.AddOverlay(_overlay);
+ _overlays.AddOverlay(_overlay);
RaiseNetworkEvent(new EmergencyShuttleRequestPositionMessage());
}
else
{
- overlayManager.RemoveOverlay(_overlay!);
+ _overlays.RemoveOverlay(_overlay!);
_overlay = null;
}
}
diff --git a/Content.Client/Shuttles/UI/ShuttleDockControl.xaml.cs b/Content.Client/Shuttles/UI/ShuttleDockControl.xaml.cs
index 2b575b4805..449323c746 100644
--- a/Content.Client/Shuttles/UI/ShuttleDockControl.xaml.cs
+++ b/Content.Client/Shuttles/UI/ShuttleDockControl.xaml.cs
@@ -40,6 +40,8 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
private readonly HashSet _drawnDocks = new();
private readonly Dictionary _dockButtons = new();
+ private readonly Color _fallbackHighlightedColor = Color.Magenta;
+
///
/// Store buttons for every other dock
///
@@ -213,11 +215,11 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
if (HighlightedDock == dock.Entity)
{
- otherDockColor = Color.ToSrgb(Color.Magenta);
+ otherDockColor = Color.ToSrgb(dock.HighlightedColor);
}
else
{
- otherDockColor = Color.ToSrgb(Color.Purple);
+ otherDockColor = Color.ToSrgb(dock.Color);
}
/*
@@ -311,7 +313,7 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
ScalePosition(Vector2.Transform(new Vector2(-0.5f, 0.5f), rotation)),
ScalePosition(Vector2.Transform(new Vector2(0.5f, -0.5f), rotation)));
- var dockColor = Color.Magenta;
+ var dockColor = _viewedState?.HighlightedColor ?? _fallbackHighlightedColor;
var connectionColor = Color.Pink;
handle.DrawRect(ourDockConnection, connectionColor.WithAlpha(0.2f));
diff --git a/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs b/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs
index 2dcec6b44a..7899a5ef3e 100644
--- a/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs
+++ b/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs
@@ -308,7 +308,7 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
-dockRadius * UIScale,
(Size.X + dockRadius) * UIScale,
(Size.Y + dockRadius) * UIScale);
-
+
if (_docks.TryGetValue(nent, out var docks))
{
foreach (var state in docks)
@@ -321,7 +321,7 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
continue;
}
- var color = Color.ToSrgb(Color.Magenta);
+ var color = Color.ToSrgb(state.HighlightedColor);
var verts = new[]
{
diff --git a/Content.Client/Stack/StackSystem.cs b/Content.Client/Stack/StackSystem.cs
index d12e9900a6..182daa73a5 100644
--- a/Content.Client/Stack/StackSystem.cs
+++ b/Content.Client/Stack/StackSystem.cs
@@ -12,7 +12,6 @@ namespace Content.Client.Stack
{
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly ItemCounterSystem _counterSystem = default!;
- [Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
@@ -28,22 +27,8 @@ namespace Content.Client.Stack
base.SetCount(uid, amount, component);
- if (component.Lingering &&
- TryComp(uid, out var sprite))
- {
- // tint the stack gray and make it transparent if it's lingering.
- var color = component.Count == 0 && component.Lingering
- ? Color.DarkGray.WithAlpha(0.65f)
- : Color.White;
-
- for (var i = 0; i < sprite.AllLayers.Count(); i++)
- {
- _sprite.LayerSetColor((uid, sprite), i, color);
- }
- }
-
// TODO PREDICT ENTITY DELETION: This should really just be a normal entity deletion call.
- if (component.Count <= 0 && !component.Lingering)
+ if (component.Count <= 0)
{
Xform.DetachEntity(uid, Transform(uid));
return;
diff --git a/Content.Client/Storage/Components/EntityStorageComponent.cs b/Content.Client/Storage/Components/EntityStorageComponent.cs
deleted file mode 100644
index 514d5d6595..0000000000
--- a/Content.Client/Storage/Components/EntityStorageComponent.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using Content.Shared.Storage.Components;
-using Robust.Shared.GameStates;
-
-namespace Content.Client.Storage.Components;
-
-[RegisterComponent]
-public sealed partial class EntityStorageComponent : SharedEntityStorageComponent
-{
-
-}
diff --git a/Content.Client/Storage/Systems/EntityStorageSystem.cs b/Content.Client/Storage/Systems/EntityStorageSystem.cs
index dd3f8d3860..ca2b986667 100644
--- a/Content.Client/Storage/Systems/EntityStorageSystem.cs
+++ b/Content.Client/Storage/Systems/EntityStorageSystem.cs
@@ -31,7 +31,7 @@ public sealed class EntityStorageSystem : SharedEntityStorageSystem
SubscribeLocalEvent(OnHandleState);
}
- public override bool ResolveStorage(EntityUid uid, [NotNullWhen(true)] ref SharedEntityStorageComponent? component)
+ public override bool ResolveStorage(EntityUid uid, [NotNullWhen(true)] ref EntityStorageComponent? component)
{
if (component != null)
return true;
diff --git a/Content.Client/UserInterface/Controls/MenuButton.cs b/Content.Client/UserInterface/Controls/MenuButton.cs
index 540a8ecb57..a0ba21da74 100644
--- a/Content.Client/UserInterface/Controls/MenuButton.cs
+++ b/Content.Client/UserInterface/Controls/MenuButton.cs
@@ -25,7 +25,7 @@ public sealed class MenuButton : ContainerButton
private Color NormalColor => HasStyleClass(StyleClassRedTopButton) ? ColorRedNormal : ColorNormal;
private Color HoveredColor => HasStyleClass(StyleClassRedTopButton) ? ColorRedHovered : ColorHovered;
- private BoundKeyFunction _function;
+ private BoundKeyFunction? _function;
private readonly BoxContainer _root;
private readonly TextureRect? _buttonIcon;
private readonly Label? _buttonLabel;
@@ -33,13 +33,13 @@ public sealed class MenuButton : ContainerButton
public string AppendStyleClass { set => AddStyleClass(value); }
public Texture? Icon { get => _buttonIcon!.Texture; set => _buttonIcon!.Texture = value; }
- public BoundKeyFunction BoundKey
+ public BoundKeyFunction? BoundKey
{
get => _function;
set
{
_function = value;
- _buttonLabel!.Text = BoundKeyHelper.ShortKeyName(value);
+ _buttonLabel!.Text = _function == null ? "" : BoundKeyHelper.ShortKeyName(_function.Value);
}
}
@@ -95,12 +95,12 @@ public sealed class MenuButton : ContainerButton
private void OnKeyBindingChanged(IKeyBinding obj)
{
- _buttonLabel!.Text = BoundKeyHelper.ShortKeyName(_function);
+ _buttonLabel!.Text = _function == null ? "" : BoundKeyHelper.ShortKeyName(_function.Value);
}
private void OnKeyBindingChanged()
{
- _buttonLabel!.Text = BoundKeyHelper.ShortKeyName(_function);
+ _buttonLabel!.Text = _function == null ? "" : BoundKeyHelper.ShortKeyName(_function.Value);
}
protected override void StylePropertiesChanged()
diff --git a/Content.Client/UserInterface/Controls/SlotControl.cs b/Content.Client/UserInterface/Controls/SlotControl.cs
index a684bb05ef..2b43f2397d 100644
--- a/Content.Client/UserInterface/Controls/SlotControl.cs
+++ b/Content.Client/UserInterface/Controls/SlotControl.cs
@@ -1,9 +1,11 @@
using System.Numerics;
using Content.Client.Cooldown;
using Content.Client.UserInterface.Systems.Inventory.Controls;
+using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input;
+using Robust.Shared.Prototypes;
namespace Content.Client.UserInterface.Controls
{
@@ -20,6 +22,7 @@ namespace Content.Client.UserInterface.Controls
public CooldownGraphic CooldownDisplay { get; }
private SpriteView SpriteView { get; }
+ private EntityPrototypeView ProtoView { get; }
public EntityUid? Entity => SpriteView.Entity;
@@ -141,6 +144,13 @@ namespace Content.Client.UserInterface.Controls
SetSize = new Vector2(DefaultButtonSize, DefaultButtonSize),
OverrideDirection = Direction.South
});
+ AddChild(ProtoView = new EntityPrototypeView
+ {
+ Visible = false,
+ Scale = new Vector2(2, 2),
+ SetSize = new Vector2(DefaultButtonSize, DefaultButtonSize),
+ OverrideDirection = Direction.South
+ });
AddChild(HoverSpriteView = new SpriteView
{
@@ -209,12 +219,35 @@ namespace Content.Client.UserInterface.Controls
HoverSpriteView.SetEntity(null);
}
+ ///
+ /// Causes the control to display a placeholder prototype, optionally faded
+ ///
public void SetEntity(EntityUid? ent)
{
SpriteView.SetEntity(ent);
+ SpriteView.Visible = true;
+ ProtoView.Visible = false;
UpdateButtonTexture();
}
+ ///
+ /// Causes the control to display a placeholder prototype, optionally faded
+ ///
+ public void SetPrototype(EntProtoId? proto, bool fade)
+ {
+ ProtoView.SetPrototype(proto);
+ SpriteView.Visible = false;
+ ProtoView.Visible = true;
+
+ UpdateButtonTexture();
+
+ if (ProtoView.Entity is not { } ent || !fade)
+ return;
+
+ var sprites = IoCManager.Resolve().GetEntitySystem();
+ sprites.SetColor((ent.Owner, ent.Comp1), Color.DarkGray.WithAlpha(0.65f));
+ }
+
private void UpdateButtonTexture()
{
var fullTexture = Theme.ResolveTextureOrNull(_fullButtonTexturePath);
diff --git a/Content.Client/UserInterface/Systems/Actions/Controls/ActionButton.cs b/Content.Client/UserInterface/Systems/Actions/Controls/ActionButton.cs
index cad9045fa8..be3af28b15 100644
--- a/Content.Client/UserInterface/Systems/Actions/Controls/ActionButton.cs
+++ b/Content.Client/UserInterface/Systems/Actions/Controls/ActionButton.cs
@@ -3,12 +3,12 @@ using Content.Client.Actions;
using Content.Client.Actions.UI;
using Content.Client.Cooldown;
using Content.Client.Stylesheets;
-using Content.Shared.Actions;
using Content.Shared.Actions.Components;
-using Content.Shared.Charges.Components;
using Content.Shared.Charges.Systems;
+using Content.Shared.Examine;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
+using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input;
@@ -23,9 +23,9 @@ namespace Content.Client.UserInterface.Systems.Actions.Controls;
public sealed class ActionButton : Control, IEntityControl
{
private IEntityManager _entities;
+ private IPlayerManager _player;
private SpriteSystem? _spriteSys;
private ActionUIController? _controller;
- private SharedChargesSystem _sharedChargesSys;
private bool _beingHovered;
private bool _depressed;
private bool _toggled;
@@ -67,8 +67,8 @@ public sealed class ActionButton : Control, IEntityControl
// TODO why is this constructor so slooooow. The rest of the code is fine
_entities = entities;
+ _player = IoCManager.Resolve();
_spriteSys = spriteSys;
- _sharedChargesSys = _entities.System();
_controller = controller;
MouseFilter = MouseFilterMode.Pass;
@@ -197,23 +197,17 @@ public sealed class ActionButton : Control, IEntityControl
return null;
var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityName));
- var decr = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityDescription));
- FormattedMessage? chargesText = null;
+ var desc = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityDescription));
- // TODO: Don't touch this use an event make callers able to add their own shit for actions or I kill you.
- if (_entities.TryGetComponent(Action, out LimitedChargesComponent? actionCharges))
- {
- var charges = _sharedChargesSys.GetCurrentCharges((Action.Value, actionCharges, null));
- chargesText = FormattedMessage.FromMarkupPermissive(Loc.GetString($"Charges: {charges.ToString()}/{actionCharges.MaxCharges}"));
+ if (_player.LocalEntity is null)
+ return null;
- if (_entities.TryGetComponent(Action, out AutoRechargeComponent? autoRecharge))
- {
- var chargeTimeRemaining = _sharedChargesSys.GetNextRechargeTime((Action.Value, actionCharges, autoRecharge));
- chargesText.AddText(Loc.GetString($"{Environment.NewLine}Time Til Recharge: {chargeTimeRemaining}"));
- }
- }
+ var ev = new ExaminedEvent(desc, Action.Value, _player.LocalEntity.Value, true, !desc.IsEmpty);
+ _entities.EventBus.RaiseLocalEvent(Action.Value.Owner, ev);
- return new ActionAlertTooltip(name, decr, charges: chargesText);
+ var newDesc = ev.GetTotalMessage();
+
+ return new ActionAlertTooltip(name, newDesc);
}
protected override void ControlFocusExited()
diff --git a/Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs b/Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs
index 1b22f9460a..17cbcc38ac 100644
--- a/Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs
+++ b/Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs
@@ -16,6 +16,7 @@ using Content.Shared.Input;
using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Graphics;
+using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
@@ -37,6 +38,7 @@ public sealed class AHelpUIController: UIController, IOnSystemChanged ToggleWindow()))
- .Register();
+ _input.SetInputCommand(ContentKeyFunctions.OpenAHelp,
+ InputCmdHandler.FromDelegate(_ => ToggleWindow()));
}
public void OnSystemUnloaded(BwoinkSystem system)
{
- CommandBinds.Unregister();
+ _input.SetInputCommand(ContentKeyFunctions.OpenAHelp, null);
DebugTools.Assert(_bwoinkSystem != null);
_bwoinkSystem!.OnBwoinkTextMessageRecieved -= ReceivedBwoink;
diff --git a/Content.Client/UserInterface/Systems/Chat/ChatUIController.Highlighting.cs b/Content.Client/UserInterface/Systems/Chat/ChatUIController.Highlighting.cs
index 1670823aab..46e06865cf 100644
--- a/Content.Client/UserInterface/Systems/Chat/ChatUIController.Highlighting.cs
+++ b/Content.Client/UserInterface/Systems/Chat/ChatUIController.Highlighting.cs
@@ -116,8 +116,9 @@ public sealed partial class ChatUIController : IOnSystemChanged entity, string name)
@@ -139,7 +141,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered(_entity, out var meta) || meta.Deleted)
{
- Update(null);
+ Update(null, hand);
return;
}
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs
index 6abebda6ee..c27e81b5c7 100644
--- a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs
@@ -30,6 +30,7 @@ public sealed partial class GunSystem : SharedGunSystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IInputManager _inputManager = 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!;
@@ -50,11 +51,10 @@ public sealed partial class GunSystem : SharedGunSystem
return;
_spreadOverlay = value;
- var overlayManager = IoCManager.Resolve();
if (_spreadOverlay)
{
- overlayManager.AddOverlay(new GunSpreadOverlay(
+ _overlayManager.AddOverlay(new GunSpreadOverlay(
EntityManager,
_eyeManager,
Timing,
@@ -65,7 +65,7 @@ public sealed partial class GunSystem : SharedGunSystem
}
else
{
- overlayManager.RemoveOverlay();
+ _overlayManager.RemoveOverlay();
}
}
}
diff --git a/Content.Client/Wieldable/WieldableSystem.cs b/Content.Client/Wieldable/WieldableSystem.cs
index 2de837923c..e40544e39d 100644
--- a/Content.Client/Wieldable/WieldableSystem.cs
+++ b/Content.Client/Wieldable/WieldableSystem.cs
@@ -29,7 +29,10 @@ public sealed class WieldableSystem : SharedWieldableSystem
return;
if (_gameTiming.IsFirstTimePredicted)
+ {
cursorOffsetComp.CurrentPosition = Vector2.Zero;
+ cursorOffsetComp.TargetPosition = Vector2.Zero;
+ }
}
public void OnGetEyeOffset(Entity entity, ref HeldRelayedEvent args)
diff --git a/Content.IntegrationTests/AssemblyInfo.cs b/Content.IntegrationTests/AssemblyInfo.cs
index 76fc42f3a9..b8a88e2623 100644
--- a/Content.IntegrationTests/AssemblyInfo.cs
+++ b/Content.IntegrationTests/AssemblyInfo.cs
@@ -5,4 +5,4 @@
// https://github.com/dotnet/runtime/issues/107197
// So we can't really parallelize integration tests harder either until the runtime fixes that,
// *or* we fix serv3 to not spam expression trees.
-[assembly: LevelOfParallelism(3)]
+[assembly: LevelOfParallelism(2)]
diff --git a/Content.IntegrationTests/PoolManager.Cvars.cs b/Content.IntegrationTests/PoolManager.Cvars.cs
index 2c51bdbc3a..8cf2b626dc 100644
--- a/Content.IntegrationTests/PoolManager.Cvars.cs
+++ b/Content.IntegrationTests/PoolManager.Cvars.cs
@@ -21,6 +21,7 @@ public static partial class PoolManager
(CCVars.NPCMaxUpdates.Name, "999999"),
(CVars.ThreadParallelCount.Name, "1"),
(CCVars.GameRoleTimers.Name, "false"),
+ (CCVars.GameRoleLoadoutTimers.Name, "false"),
(CCVars.GameRoleWhitelist.Name, "false"),
(CCVars.GridFill.Name, "false"),
(CCVars.PreloadGrids.Name, "false"),
diff --git a/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs b/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs
new file mode 100644
index 0000000000..9dda130847
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs
@@ -0,0 +1,417 @@
+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 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;
+
+///
+/// Tests for AtmosphereSystem.DeltaPressure and surrounding systems
+/// handling the DeltaPressureComponent.
+///
+[TestFixture]
+[TestOf(typeof(DeltaPressureSystem))]
+public sealed class DeltaPressureTest
+{
+ #region Prototypes
+
+ [TestPrototypes]
+ private const string Prototypes = @"
+- type: entity
+ parent: BaseStructure
+ id: DeltaPressureSolidTest
+ placement:
+ mode: SnapgridCenter
+ snap:
+ - Wall
+ components:
+ - type: Physics
+ bodyType: Static
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape:
+ !type:PhysShapeAabb
+ bounds: ""-0.5,-0.5,0.5,0.5""
+ mask:
+ - FullTileMask
+ layer:
+ - WallLayer
+ density: 1000
+ - type: Airtight
+ - type: DeltaPressure
+ minPressure: 15000
+ minPressureDelta: 10000
+ scalingType: Threshold
+ baseDamage:
+ types:
+ Structural: 1000
+ - type: Damageable
+ - type: Destructible
+ thresholds:
+ - trigger:
+ !type:DamageTrigger
+ damage: 300
+ behaviors:
+ - !type:SpawnEntitiesBehavior
+ spawn:
+ Girder:
+ min: 1
+ max: 1
+ - !type:DoActsBehavior
+ acts: [ ""Destruction"" ]
+
+- type: entity
+ parent: DeltaPressureSolidTest
+ id: DeltaPressureSolidTestNoAutoJoin
+ components:
+ - type: DeltaPressure
+ autoJoinProcessingList: false
+
+- type: entity
+ parent: DeltaPressureSolidTest
+ id: DeltaPressureSolidTestAbsolute
+ components:
+ - type: DeltaPressure
+ minPressure: 10000
+ minPressureDelta: 15000
+ scalingType: Threshold
+ baseDamage:
+ types:
+ Structural: 1000
+";
+
+ #endregion
+
+ private readonly ResPath _testMap = new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml");
+
+ ///
+ /// Asserts that an entity with a DeltaPressureComponent with autoJoinProcessingList
+ /// set to true is automatically added to the DeltaPressure processing list
+ /// on the grid's GridAtmosphereComponent.
+ ///
+ /// Also asserts that an entity with a DeltaPressureComponent with autoJoinProcessingList
+ /// set to false is not automatically added to the DeltaPressure processing list.
+ ///
+ [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(() =>
+ {
+#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();
+ });
+
+ 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();
+ }
+
+ ///
+ /// Asserts that an entity that doesn't need to be damaged by DeltaPressure
+ /// is not damaged by DeltaPressure.
+ ///
+ [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(() =>
+ {
+#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!");
+ });
+
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ await server.WaitPost(() =>
+ {
+ var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
+ var gridAtmosComp = entMan.GetComponent(grid);
+
+ direction = (AtmosDirection)(1 << i);
+ var offsetIndices = indices.Offset(direction);
+ tile = gridAtmosComp.Tiles[offsetIndices];
+
+ Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
+
+ var toPressurize = dpEnt.Comp!.MinPressureDelta - 10;
+ var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
+
+ 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(() =>
+ {
+ Assert.That(!entMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold pressure from {direction} side!");
+ tile.Air!.Clear();
+ });
+
+ await server.WaitRunTicks(30);
+ }
+
+ await pair.CleanReturnAsync();
+ }
+
+ ///
+ /// Asserts that an entity that needs to be damaged by DeltaPressure
+ /// is damaged by DeltaPressure when the pressure is above the threshold.
+ ///
+ [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(() =>
+ {
+#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(() =>
+ {
+ // 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 indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
+ var gridAtmosComp = entMan.GetComponent(grid);
+
+ direction = (AtmosDirection)(1 << i);
+ var offsetIndices = indices.Offset(direction);
+ tile = gridAtmosComp.Tiles[offsetIndices];
+
+ Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
+
+ var toPressurize = dpEnt.Comp!.MinPressureDelta + 10;
+ var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
+
+ 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(() =>
+ {
+ Assert.That(entMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold pressure from {direction} side!");
+ tile.Air!.Clear();
+ });
+
+ await server.WaitRunTicks(30);
+ }
+
+ await pair.CleanReturnAsync();
+ }
+
+ ///
+ /// Asserts that an entity that doesn't need to be damaged by DeltaPressure
+ /// is not damaged by DeltaPressure when using absolute pressure thresholds.
+ ///
+ [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(() =>
+ {
+#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!");
+ });
+
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ await server.WaitPost(() =>
+ {
+ var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
+ var gridAtmosComp = entMan.GetComponent(grid);
+
+ direction = (AtmosDirection)(1 << i);
+ var offsetIndices = indices.Offset(direction);
+ tile = gridAtmosComp.Tiles[offsetIndices];
+ Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
+
+ var toPressurize = dpEnt.Comp!.MinPressure - 10; // just below absolute threshold
+ var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
+ tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
+ });
+
+ await server.WaitRunTicks(30);
+
+ await server.WaitAssertion(() =>
+ {
+ Assert.That(!entMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold absolute pressure from {direction} side!");
+ tile.Air!.Clear();
+ });
+
+ await server.WaitRunTicks(30);
+ }
+
+ await pair.CleanReturnAsync();
+ }
+
+ ///
+ /// Asserts that an entity that needs to be damaged by DeltaPressure
+ /// is damaged by DeltaPressure when the pressure is above the absolute threshold.
+ ///
+ [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(() =>
+ {
+ // 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 indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
+ var gridAtmosComp = entMan.GetComponent(grid);
+
+ direction = (AtmosDirection)(1 << i);
+ var offsetIndices = indices.Offset(direction);
+ tile = gridAtmosComp.Tiles[offsetIndices];
+ Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
+
+ // Above absolute threshold but below delta threshold to ensure absolute alone causes damage
+ var toPressurize = dpEnt.Comp!.MinPressure + 10;
+ var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
+ tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
+ });
+
+ await server.WaitRunTicks(30);
+
+ await server.WaitAssertion(() =>
+ {
+ Assert.That(entMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold absolute pressure from {direction} side!");
+ tile.Air!.Clear();
+ });
+
+ await server.WaitRunTicks(30);
+ }
+
+ await pair.CleanReturnAsync();
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Atmos/GasArrayTest.cs b/Content.IntegrationTests/Tests/Atmos/GasArrayTest.cs
new file mode 100644
index 0000000000..07caf447bd
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Atmos/GasArrayTest.cs
@@ -0,0 +1,85 @@
+using System.Linq;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.Components;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.Atmos;
+
+[TestFixture]
+[TestOf(typeof(Atmospherics))]
+public sealed class GasArrayTest
+{
+ private const string GasTankTestDummyId = "GasTankTestDummy";
+
+ private const string GasTankLegacyTestDummyId = "GasTankLegacyTestDummy";
+
+ [TestPrototypes]
+ private const string Prototypes = $@"
+- type: entity
+ id: {GasTankTestDummyId}
+ components:
+ - type: GasTank
+ air:
+ volume: 5
+ moles:
+ Frezon: 20
+ Oxygen: 10
+
+- type: entity
+ id: {GasTankLegacyTestDummyId}
+ components:
+ - type: GasTank
+ air:
+ volume: 5
+ moles:
+ - 0
+ - 0
+ - 0
+ - 10
+";
+
+ [Test]
+ public async Task TestGasArrayDeserialization()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var compFactory = server.ResolveDependency();
+ var prototypeManager = server.ResolveDependency();
+
+ await server.WaitAssertion(() =>
+ {
+ var gasTank = prototypeManager.Index(GasTankTestDummyId);
+ Assert.Multiple(() =>
+ {
+ Assert.That(gasTank.TryGetComponent(out var gasTankComponent, compFactory));
+
+ Assert.That(gasTankComponent!.Air.GetMoles(Gas.Oxygen), Is.EqualTo(10));
+ Assert.That(gasTankComponent!.Air.GetMoles(Gas.Frezon), Is.EqualTo(20));
+ foreach (var gas in Enum.GetValues().Where(p => p != Gas.Oxygen && p != Gas.Frezon))
+ {
+ Assert.That(gasTankComponent!.Air.GetMoles(gas), Is.EqualTo(0));
+ }
+ });
+
+ var legacyGasTank = prototypeManager.Index(GasTankLegacyTestDummyId);
+ Assert.Multiple(() =>
+ {
+ Assert.That(legacyGasTank.TryGetComponent(out var gasTankComponent, compFactory));
+
+ Assert.That(gasTankComponent!.Air.GetMoles(3), Is.EqualTo(10));
+
+ // Iterate through all other gases: check for 0 values
+ for (var i = 0; i < Atmospherics.AdjustedNumberOfGases; i++)
+ {
+ if (i == 3) // our case with a value.
+ continue;
+
+ Assert.That(gasTankComponent!.Air.GetMoles(i), Is.EqualTo(0));
+ }
+ });
+ });
+ await pair.CleanReturnAsync();
+ }
+}
diff --git a/Content.IntegrationTests/Tests/CargoTest.cs b/Content.IntegrationTests/Tests/CargoTest.cs
index e5f9fa1e81..aad87b711a 100644
--- a/Content.IntegrationTests/Tests/CargoTest.cs
+++ b/Content.IntegrationTests/Tests/CargoTest.cs
@@ -6,6 +6,7 @@ using Content.Server.Cargo.Systems;
using Content.Server.Nutrition.Components;
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Cargo.Prototypes;
+using Content.Shared.Mobs.Components;
using Content.Shared.Prototypes;
using Content.Shared.Stacks;
using Content.Shared.Whitelist;
@@ -250,4 +251,25 @@ public sealed class CargoTest
await pair.CleanReturnAsync();
}
+
+ [Test]
+ public async Task MobPrice()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+
+ var componentFactory = pair.Server.ResolveDependency();
+
+ await pair.Server.WaitAssertion(() =>
+ {
+ Assert.Multiple(() =>
+ {
+ foreach (var (proto, comp) in pair.GetPrototypesWithComponent())
+ {
+ Assert.That(proto.TryGetComponent(out _, componentFactory), $"Found MobPriceComponent on {proto.ID}, but no MobStateComponent!");
+ }
+ });
+ });
+
+ await pair.CleanReturnAsync();
+ }
}
diff --git a/Content.IntegrationTests/Tests/Construction/Interaction/EdgeClobbering.cs b/Content.IntegrationTests/Tests/Construction/Interaction/EdgeClobbering.cs
new file mode 100644
index 0000000000..9f578148cf
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Construction/Interaction/EdgeClobbering.cs
@@ -0,0 +1,49 @@
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Server.Construction.Components;
+using Content.Shared.Temperature;
+
+namespace Content.IntegrationTests.Tests.Construction.Interaction;
+
+public sealed class EdgeClobbering : InteractionTest
+{
+ [TestPrototypes]
+ private const string Prototypes = @"
+- type: constructionGraph
+ id: ExampleGraph
+ start: A
+ graph:
+ - node: A
+ edges:
+ - to: B
+ steps:
+ - tool: Anchoring
+ doAfter: 1
+ - to: C
+ steps:
+ - tool: Screwing
+ doAfter: 1
+ - node: B
+ - node: C
+
+- type: entity
+ id: ExampleEntity
+ components:
+ - type: Construction
+ graph: ExampleGraph
+ node: A
+
+ ";
+
+ [Test]
+ public async Task EnsureNoEdgeClobbering()
+ {
+ await SpawnTarget("ExampleEntity");
+ var sTarget = SEntMan.GetEntity(Target!.Value);
+
+ await InteractUsing(Screw, false);
+ SEntMan.EventBus.RaiseLocalEvent(sTarget, new OnTemperatureChangeEvent(0f, 0f, 0f));
+ await AwaitDoAfters();
+
+ Assert.That(SEntMan.GetComponent(sTarget).Node, Is.EqualTo("C"));
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Disposal/DisposalUnitInteractionTest.cs b/Content.IntegrationTests/Tests/Disposal/DisposalUnitInteractionTest.cs
new file mode 100644
index 0000000000..d4b20a55a8
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Disposal/DisposalUnitInteractionTest.cs
@@ -0,0 +1,54 @@
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Server.Containers;
+using Robust.Shared.Containers;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.Disposal;
+
+public sealed class DisposalUnitInteractionTest : InteractionTest
+{
+ private static readonly EntProtoId DisposalUnit = "DisposalUnit";
+ private static readonly EntProtoId TrashItem = "BrokenBottle";
+
+ private const string TestDisposalUnitId = "TestDisposalUnit";
+
+ [TestPrototypes]
+ private static readonly string TestPrototypes = $@"
+# A modified disposal unit with a 100% chance of a thrown item being inserted
+- type: entity
+ parent: {DisposalUnit.Id}
+ id: {TestDisposalUnitId}
+ components:
+ - type: ThrowInsertContainer
+ probability: 1
+";
+
+ ///
+ /// Spawns a disposal unit, gives the player a trash item, and makes the
+ /// player throw the item at the disposal unit.
+ /// After a short delay, verifies that the thrown item is contained inside
+ /// the disposal unit.
+ ///
+ [Test]
+ public async Task ThrowItemIntoDisposalUnitTest()
+ {
+ var containerSys = Server.System();
+
+ // Spawn the target disposal unit
+ var disposalUnit = await SpawnTarget(TestDisposalUnitId);
+
+ // Give the player some trash to throw
+ var trash = await PlaceInHands(TrashItem);
+
+ // Throw the item at the disposal unit
+ await ThrowItem();
+
+ // Wait a moment
+ await RunTicks(10);
+
+ // Make sure the trash is in the disposal unit
+ var throwInsertComp = Comp();
+ var container = containerSys.GetContainer(ToServer(disposalUnit), throwInsertComp.ContainerId);
+ Assert.That(container.ContainedEntities, Contains.Item(ToServer(trash)));
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Doors/AirlockTest.cs b/Content.IntegrationTests/Tests/Doors/AirlockTest.cs
index e47c73611a..69fe66039b 100644
--- a/Content.IntegrationTests/Tests/Doors/AirlockTest.cs
+++ b/Content.IntegrationTests/Tests/Doors/AirlockTest.cs
@@ -21,6 +21,7 @@ namespace Content.IntegrationTests.Tests.Doors
components:
- type: Physics
bodyType: Dynamic
+ - type: GravityAffected
- type: Fixtures
fixtures:
fix1:
diff --git a/Content.IntegrationTests/Tests/Engineering/InflatablesDeflateTest.cs b/Content.IntegrationTests/Tests/Engineering/InflatablesDeflateTest.cs
new file mode 100644
index 0000000000..a7203d9259
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Engineering/InflatablesDeflateTest.cs
@@ -0,0 +1,20 @@
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Shared.Engineering.Systems;
+
+namespace Content.IntegrationTests.Tests.Engineering;
+
+[TestFixture]
+[TestOf(typeof(InflatableSafeDisassemblySystem))]
+public sealed class InflatablesDeflateTest : InteractionTest
+{
+ [Test]
+ public async Task Test()
+ {
+ await SpawnTarget(InflatableWall);
+
+ await InteractUsing(Needle);
+
+ AssertDeleted();
+ await AssertEntityLookup(new EntitySpecifier(InflatableWallStack.Id, 1));
+ }
+}
diff --git a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs
index cf16617479..4f92fd4e55 100644
--- a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs
+++ b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs
@@ -9,7 +9,6 @@ using Content.Server.Mind;
using Content.Server.Roles;
using Content.Server.RoundEnd;
using Content.Server.Shuttles.Components;
-using Content.Server.Station.Components;
using Content.Shared.CCVar;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
@@ -20,6 +19,7 @@ using Content.Shared.NPC.Prototypes;
using Content.Shared.NPC.Systems;
using Content.Shared.NukeOps;
using Content.Shared.Pinpointer;
+using Content.Shared.Roles.Components;
using Content.Shared.Station.Components;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
diff --git a/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs b/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs
index 6aa2763888..0951e7e260 100644
--- a/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs
+++ b/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs
@@ -19,6 +19,7 @@ namespace Content.IntegrationTests.Tests.Gravity
- type: Alerts
- type: Physics
bodyType: Dynamic
+ - type: GravityAffected
- type: entity
name: WeightlessGravityGeneratorDummy
diff --git a/Content.IntegrationTests/Tests/GravityGridTest.cs b/Content.IntegrationTests/Tests/GravityGridTest.cs
index b32d6c2b8d..8257035de6 100644
--- a/Content.IntegrationTests/Tests/GravityGridTest.cs
+++ b/Content.IntegrationTests/Tests/GravityGridTest.cs
@@ -76,8 +76,8 @@ namespace Content.IntegrationTests.Tests
Assert.Multiple(() =>
{
Assert.That(generatorComponent.GravityActive, Is.True);
- Assert.That(!entityMan.GetComponent(grid1).EnabledVV);
- Assert.That(entityMan.GetComponent(grid2).EnabledVV);
+ Assert.That(!entityMan.GetComponent(grid1).Enabled);
+ Assert.That(entityMan.GetComponent(grid2).Enabled);
});
// Re-enable needs power so it turns off again.
@@ -94,7 +94,7 @@ namespace Content.IntegrationTests.Tests
Assert.Multiple(() =>
{
Assert.That(generatorComponent.GravityActive, Is.False);
- Assert.That(entityMan.GetComponent(grid2).EnabledVV, Is.False);
+ Assert.That(entityMan.GetComponent(grid2).Enabled, Is.False);
});
});
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs
index 5db5d91d0d..8917ba7ead 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs
@@ -1,3 +1,6 @@
+using Content.Shared.Stacks;
+using Robust.Shared.Prototypes;
+
namespace Content.IntegrationTests.Tests.Interaction;
// This partial class contains various constant prototype IDs common to interaction tests.
@@ -32,4 +35,9 @@ public abstract partial class InteractionTest
protected const string Manipulator1 = "MicroManipulatorStockPart";
protected const string Battery1 = "PowerCellSmall";
protected const string Battery4 = "PowerCellHyper";
+
+ // Inflatables & Needle used to pop them
+ protected static readonly EntProtoId InflatableWall = "InflatableWall";
+ protected static readonly EntProtoId Needle = "WeaponMeleeNeedle";
+ protected static readonly ProtoId InflatableWallStack = "InflatableWall";
}
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
index 3302b1bafc..8a5859fe06 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
@@ -264,9 +264,10 @@ public abstract partial class InteractionTest
/// The entity or stack prototype to spawn and place into the users hand
/// The number of entities to spawn. If the prototype is a stack, this sets the stack count.
/// Whether or not to wait for any do-afters to complete
- protected async Task InteractUsing(string id, int quantity = 1, bool awaitDoAfters = true)
+ /// If true, perform an alternate interaction instead of a standard one.
+ protected async Task InteractUsing(string id, int quantity = 1, bool awaitDoAfters = true, bool altInteract = false)
{
- await InteractUsing((id, quantity), awaitDoAfters);
+ await InteractUsing((id, quantity), awaitDoAfters, altInteract);
}
///
@@ -274,7 +275,8 @@ public abstract partial class InteractionTest
///
/// The entity type & quantity to spawn and place into the users hand
/// Whether or not to wait for any do-afters to complete
- protected async Task InteractUsing(EntitySpecifier entity, bool awaitDoAfters = true)
+ /// If true, perform an alternate interaction instead of a standard one.
+ protected async Task InteractUsing(EntitySpecifier entity, bool awaitDoAfters = true, bool altInteract = false)
{
// For every interaction, we will also examine the entity, just in case this breaks something, somehow.
// (e.g., servers attempt to assemble construction examine hints).
@@ -284,18 +286,19 @@ public abstract partial class InteractionTest
}
await PlaceInHands(entity);
- await Interact(awaitDoAfters);
+ await Interact(awaitDoAfters, altInteract);
}
///
/// Interact with an entity using the currently held entity.
///
/// Whether or not to wait for any do-afters to complete
- protected async Task Interact(bool awaitDoAfters = true)
+ /// If true, performs an alternate interaction instead of a standard one.
+ protected async Task Interact(bool awaitDoAfters = true, bool altInteract = false)
{
if (Target == null || !Target.Value.IsClientSide())
{
- await Interact(Target, TargetCoords, awaitDoAfters);
+ await Interact(Target, TargetCoords, awaitDoAfters, altInteract);
return;
}
@@ -311,23 +314,23 @@ public abstract partial class InteractionTest
await CheckTargetChange();
}
- ///
- protected async Task Interact(NetEntity? target, NetCoordinates coordinates, bool awaitDoAfters = true)
+ ///
+ protected async Task Interact(NetEntity? target, NetCoordinates coordinates, bool awaitDoAfters = true, bool altInteract = false)
{
Assert.That(SEntMan.TryGetEntity(target, out var sTarget) || target == null);
var coords = SEntMan.GetCoordinates(coordinates);
Assert.That(coords.IsValid(SEntMan));
- await Interact(sTarget, coords, awaitDoAfters);
+ await Interact(sTarget, coords, awaitDoAfters, altInteract);
}
///
/// Interact with an entity using the currently held entity.
///
- protected async Task Interact(EntityUid? target, EntityCoordinates coordinates, bool awaitDoAfters = true)
+ protected async Task Interact(EntityUid? target, EntityCoordinates coordinates, bool awaitDoAfters = true, bool altInteract = false)
{
Assert.That(SEntMan.TryGetEntity(Player, out var player));
- await Server.WaitPost(() => InteractSys.UserInteraction(player!.Value, coordinates, target));
+ await Server.WaitPost(() => InteractSys.UserInteraction(player!.Value, coordinates, target, altInteract: altInteract));
await RunTicks(1);
if (awaitDoAfters)
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
index 79756ea5b4..0ed42d3476 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
@@ -144,6 +144,7 @@ public abstract partial class InteractionTest
- type: Stripping
- type: Puller
- type: Physics
+ - type: GravityAffected
- type: Tag
tags:
- CanPilot
diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.cs b/Content.IntegrationTests/Tests/Minds/MindTests.cs
index 48e11e4648..2f77519829 100644
--- a/Content.IntegrationTests/Tests/Minds/MindTests.cs
+++ b/Content.IntegrationTests/Tests/Minds/MindTests.cs
@@ -3,7 +3,6 @@ using System.Linq;
using Content.Server.Ghost.Roles;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Mind;
-using Content.Server.Roles;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
@@ -11,7 +10,7 @@ using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Players;
using Content.Shared.Roles;
-using Content.Shared.Roles.Jobs;
+using Content.Shared.Roles.Components;
using Robust.Server.Console;
using Robust.Server.GameObjects;
using Robust.Server.Player;
diff --git a/Content.IntegrationTests/Tests/Minds/RoleTests.cs b/Content.IntegrationTests/Tests/Minds/RoleTests.cs
index 8acfff3fb9..f0a7268a3d 100644
--- a/Content.IntegrationTests/Tests/Minds/RoleTests.cs
+++ b/Content.IntegrationTests/Tests/Minds/RoleTests.cs
@@ -1,7 +1,5 @@
using System.Linq;
-using Content.Server.Roles;
-using Content.Shared.Roles;
-using Content.Shared.Roles.Jobs;
+using Content.Shared.Roles.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Reflection;
diff --git a/Content.IntegrationTests/Tests/Nutrition/WaterCoolerInteractionTest.cs b/Content.IntegrationTests/Tests/Nutrition/WaterCoolerInteractionTest.cs
new file mode 100644
index 0000000000..c15de639de
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Nutrition/WaterCoolerInteractionTest.cs
@@ -0,0 +1,99 @@
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.FixedPoint;
+using Content.Shared.Storage.Components;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.Nutrition;
+
+public sealed class WaterCoolerInteractionTest : InteractionTest
+{
+ ///
+ /// ProtoId of the water cooler entity.
+ ///
+ private static readonly EntProtoId WaterCooler = "WaterCooler";
+
+ ///
+ /// ProtoId of the paper cup entity dispensed by the water cooler.
+ ///
+ private static readonly EntProtoId PaperCup = "DrinkWaterCup";
+
+ ///
+ /// ProtoId of the water reagent that is stored in the water cooler.
+ ///
+ private static readonly ProtoId Water = "Water";
+
+ ///
+ /// Spawns a water cooler and tests that the player can retrieve a paper cup
+ /// by interacting with it, and can return the paper cup by alt-interacting with it.
+ ///
+ [Test]
+ public async Task GetAndReturnCup()
+ {
+ // Spawn the water cooler
+ var cooler = await SpawnTarget(WaterCooler);
+
+ // Record how many paper cups are in the cooler
+ var binComp = Comp(cooler);
+ var initialCount = binComp.Items.Count;
+ Assert.That(binComp.Items, Is.Not.Empty, "Water cooler didn't start with any cups");
+
+ // Interact with the water cooler using an empty hand to grab a paper cup
+ await Interact();
+
+ var cup = HandSys.GetActiveItem((SPlayer, Hands));
+
+ Assert.Multiple(() =>
+ {
+ // Make sure the player is now holding a cup
+ Assert.That(cup, Is.Not.Null, "Player's hand is empty");
+ AssertPrototype(PaperCup, SEntMan.GetNetEntity(cup));
+
+ // Make sure the number of cups in the cooler has decreased by one
+ Assert.That(binComp.Items, Has.Count.EqualTo(initialCount - 1), "Number of cups in cooler bin did not decrease by one");
+
+ // Make sure the cup isn't somehow still in the cooler too
+ Assert.That(binComp.Items, Does.Not.Contain(cup));
+ });
+
+ // Alt-interact with the water cooler while holding the cup to put it back
+ await Interact(altInteract: true);
+
+ Assert.Multiple(() =>
+ {
+ // Make sure the player's hand is empty
+ Assert.That(HandSys.ActiveHandIsEmpty((SPlayer, Hands)), "Player's hand is not empty");
+
+ // Make sure the count has gone back up by one
+ Assert.That(binComp.Items, Has.Count.EqualTo(initialCount), "Number of cups in cooler bin did not return to initial count");
+
+ // Make sure the cup is in the cooler
+ Assert.That(binComp.Items, Contains.Item(cup), "Cup was not returned to cooler");
+ });
+ }
+
+ ///
+ /// Spawns a water cooler and gives the player an empty paper cup.
+ /// Tests that the player can put water into the cup by interacting
+ /// with the water cooler while holding the cup.
+ ///
+ [Test]
+ public async Task FillCup()
+ {
+ var solutionSys = Server.System();
+
+ // Spawn the water cooler
+ await SpawnTarget(WaterCooler);
+
+ // Give the player a cup
+ var cup = await PlaceInHands(PaperCup);
+
+ // Make the player interact with the water cooler using the held cup
+ await Interact();
+
+ // Make sure the cup now contains water
+ Assert.That(solutionSys.GetTotalPrototypeQuantity(ToServer(cup), Water), Is.GreaterThan(FixedPoint2.Zero),
+ "Cup does not contain any water");
+ }
+}
diff --git a/Content.Packaging/ClientPackaging.cs b/Content.Packaging/ClientPackaging.cs
index 6d0a462790..21215d3bcb 100644
--- a/Content.Packaging/ClientPackaging.cs
+++ b/Content.Packaging/ClientPackaging.cs
@@ -78,7 +78,11 @@ public static class ClientPackaging
new[] { "Content.Client", "Content.Shared", "Content.Shared.Database" },
cancel: cancel);
- await RobustClientPackaging.WriteClientResources(contentDir, inputPass, cancel);
+ await RobustClientPackaging.WriteClientResources(
+ contentDir,
+ inputPass,
+ SharedPackaging.AdditionalIgnoredResources,
+ cancel);
inputPass.InjectFinished();
}
diff --git a/Content.Packaging/ServerPackaging.cs b/Content.Packaging/ServerPackaging.cs
index 91ebc41226..a15dc7244f 100644
--- a/Content.Packaging/ServerPackaging.cs
+++ b/Content.Packaging/ServerPackaging.cs
@@ -25,6 +25,12 @@ public static class ServerPackaging
new PlatformReg("freebsd-x64", "FreeBSD", false),
};
+ private static IReadOnlySet ServerContentIgnoresResources { get; } = new HashSet
+ {
+ "ServerInfo",
+ "Changelog",
+ };
+
private static List PlatformRids => Platforms
.Select(o => o.Rid)
.ToList();
@@ -211,7 +217,11 @@ public static class ServerPackaging
contentAssemblies,
cancel: cancel);
- await RobustServerPackaging.WriteServerResources(contentDir, inputPassResources, cancel);
+ await RobustServerPackaging.WriteServerResources(
+ contentDir,
+ inputPassResources,
+ ServerContentIgnoresResources.Concat(SharedPackaging.AdditionalIgnoredResources).ToHashSet(),
+ cancel);
if (hybridAcz)
{
diff --git a/Content.Packaging/SharedPackaging.cs b/Content.Packaging/SharedPackaging.cs
new file mode 100644
index 0000000000..5888845588
--- /dev/null
+++ b/Content.Packaging/SharedPackaging.cs
@@ -0,0 +1,10 @@
+namespace Content.Packaging;
+
+public sealed class SharedPackaging
+{
+ public static readonly IReadOnlySet AdditionalIgnoredResources = new HashSet
+ {
+ // MapRenderer outputs into Resources. Avoid these getting included in packaging.
+ "MapImages",
+ };
+}
diff --git a/Content.Server/Access/Systems/AgentIDCardSystem.cs b/Content.Server/Access/Systems/AgentIDCardSystem.cs
index 6385274336..0df760baef 100644
--- a/Content.Server/Access/Systems/AgentIDCardSystem.cs
+++ b/Content.Server/Access/Systems/AgentIDCardSystem.cs
@@ -13,6 +13,7 @@ using Content.Server.Clothing.Systems;
using Content.Server.Implants;
using Content.Shared.Implants;
using Content.Shared.Inventory;
+using Content.Shared.Lock;
using Content.Shared.PDA;
namespace Content.Server.Access.Systems
@@ -25,6 +26,7 @@ namespace Content.Server.Access.Systems
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ChameleonClothingSystem _chameleon = default!;
[Dependency] private readonly ChameleonControllerSystem _chamController = default!;
+ [Dependency] private readonly LockSystem _lock = default!;
public override void Initialize()
{
@@ -79,7 +81,8 @@ namespace Content.Server.Access.Systems
private void OnAfterInteract(EntityUid uid, AgentIDCardComponent component, AfterInteractEvent args)
{
- if (args.Target == null || !args.CanReach || !TryComp(args.Target, out var targetAccess) || !HasComp(args.Target))
+ if (args.Target == null || !args.CanReach || _lock.IsLocked(uid) ||
+ !TryComp(args.Target, out var targetAccess) || !HasComp(args.Target))
return;
if (!TryComp(uid, out var access) || !HasComp(uid))
diff --git a/Content.Server/Administration/Commands/AddEntityStorageCommand.cs b/Content.Server/Administration/Commands/AddEntityStorageCommand.cs
index 4c562d606d..b7b6ad89e9 100644
--- a/Content.Server/Administration/Commands/AddEntityStorageCommand.cs
+++ b/Content.Server/Administration/Commands/AddEntityStorageCommand.cs
@@ -1,4 +1,4 @@
-using Content.Server.Storage.Components;
+using Content.Shared.Storage.Components;
using Content.Server.Storage.EntitySystems;
using Content.Shared.Administration;
using Robust.Shared.Console;
diff --git a/Content.Server/Administration/Commands/AddPolymorphActionCommand.cs b/Content.Server/Administration/Commands/AddPolymorphActionCommand.cs
index b92cbfc0de..f089268bea 100644
--- a/Content.Server/Administration/Commands/AddPolymorphActionCommand.cs
+++ b/Content.Server/Administration/Commands/AddPolymorphActionCommand.cs
@@ -6,17 +6,13 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Fun)]
-public sealed class AddPolymorphActionCommand : IConsoleCommand
+public sealed class AddPolymorphActionCommand : LocalizedEntityCommands
{
- [Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly PolymorphSystem _polySystem = default!;
- public string Command => "addpolymorphaction";
+ public override string Command => "addpolymorphaction";
- public string Description => Loc.GetString("add-polymorph-action-command-description");
-
- public string Help => Loc.GetString("add-polymorph-action-command-help-text");
-
- public void Execute(IConsoleShell shell, string argStr, string[] args)
+ public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 2)
{
@@ -24,15 +20,13 @@ public sealed class AddPolymorphActionCommand : IConsoleCommand
return;
}
- if (!NetEntity.TryParse(args[0], out var entityUidNet) || !_entityManager.TryGetEntity(entityUidNet, out var entityUid))
+ if (!NetEntity.TryParse(args[0], out var entityUidNet) || !EntityManager.TryGetEntity(entityUidNet, out var entityUid))
{
- shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number"));
+ shell.WriteError(Loc.GetString("shell-could-not-find-entity-with-uid", ("uid", args[0])));
return;
}
- var polySystem = _entityManager.EntitySysManager.GetEntitySystem();
-
- var polymorphable = _entityManager.EnsureComponent(entityUid.Value);
- polySystem.CreatePolymorphAction(args[1], (entityUid.Value, polymorphable));
+ var polymorphable = EntityManager.EnsureComponent(entityUid.Value);
+ _polySystem.CreatePolymorphAction(args[1], (entityUid.Value, polymorphable));
}
}
diff --git a/Content.Server/Administration/Commands/LinkBluespaceLocker.cs b/Content.Server/Administration/Commands/LinkBluespaceLocker.cs
index 9fac72664b..47d87565d2 100644
--- a/Content.Server/Administration/Commands/LinkBluespaceLocker.cs
+++ b/Content.Server/Administration/Commands/LinkBluespaceLocker.cs
@@ -1,5 +1,6 @@
using Content.Server.Storage.Components;
using Content.Shared.Administration;
+using Content.Shared.Storage.Components;
using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
diff --git a/Content.Server/Administration/Commands/RemoveEntityStorageCommand.cs b/Content.Server/Administration/Commands/RemoveEntityStorageCommand.cs
index 858e48a71e..f267cd28cb 100644
--- a/Content.Server/Administration/Commands/RemoveEntityStorageCommand.cs
+++ b/Content.Server/Administration/Commands/RemoveEntityStorageCommand.cs
@@ -1,4 +1,4 @@
-using Content.Server.Storage.Components;
+using Content.Shared.Storage.Components;
using Content.Server.Storage.EntitySystems;
using Content.Shared.Administration;
using Robust.Shared.Console;
diff --git a/Content.Server/Administration/Logs/AdminLogManager.Json.cs b/Content.Server/Administration/Logs/AdminLogManager.Json.cs
index 9e6274a493..a0a3b920bd 100644
--- a/Content.Server/Administration/Logs/AdminLogManager.Json.cs
+++ b/Content.Server/Administration/Logs/AdminLogManager.Json.cs
@@ -2,9 +2,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Content.Server.Administration.Logs.Converters;
-using Robust.Server.GameObjects;
-using Robust.Shared.Map;
-using Robust.Shared.Player;
+using Robust.Shared.Collections;
namespace Content.Server.Administration.Logs;
@@ -22,55 +20,25 @@ public sealed partial class AdminLogManager
PropertyNamingPolicy = NamingPolicy
};
+ var interfaces = new ValueList();
+
foreach (var converter in _reflection.FindTypesWithAttribute())
{
var instance = _typeFactory.CreateInstance(converter);
- (instance as IAdminLogConverter)?.Init(_dependencies);
+ if (instance is IAdminLogConverter converterInterface)
+ {
+ interfaces.Add(converterInterface);
+ converterInterface.Init(_dependencies);
+ }
_jsonOptions.Converters.Add(instance);
}
+ foreach (var @interface in interfaces)
+ {
+ @interface.Init2(_jsonOptions);
+ }
+
var converterNames = _jsonOptions.Converters.Select(converter => converter.GetType().Name);
_sawmill.Debug($"Admin log converters found: {string.Join(" ", converterNames)}");
}
-
- private (JsonDocument Json, HashSet Players) ToJson(
- Dictionary properties)
- {
- var players = new HashSet();
- var parsed = new Dictionary();
-
- foreach (var key in properties.Keys)
- {
- var value = properties[key];
- value = value switch
- {
- ICommonSession player => new SerializablePlayer(player),
- EntityCoordinates entityCoordinates => new SerializableEntityCoordinates(_entityManager, entityCoordinates),
- _ => value
- };
-
- var parsedKey = NamingPolicy.ConvertName(key);
- parsed.Add(parsedKey, value);
-
- var entityId = properties[key] switch
- {
- EntityUid id => id,
- EntityStringRepresentation rep => rep.Uid,
- ICommonSession {AttachedEntity: {Valid: true}} session => session.AttachedEntity,
- IComponent component => component.Owner,
- _ => null
- };
-
- if (_entityManager.TryGetComponent(entityId, out ActorComponent? actor))
- {
- players.Add(actor.PlayerSession.UserId.UserId);
- }
- else if (value is SerializablePlayer player)
- {
- players.Add(player.Player.UserId.UserId);
- }
- }
-
- return (JsonSerializer.SerializeToDocument(parsed, _jsonOptions), players);
- }
}
diff --git a/Content.Server/Administration/Logs/AdminLogManager.cs b/Content.Server/Administration/Logs/AdminLogManager.cs
index 600311a651..e7682cf559 100644
--- a/Content.Server/Administration/Logs/AdminLogManager.cs
+++ b/Content.Server/Administration/Logs/AdminLogManager.cs
@@ -25,7 +25,6 @@ namespace Content.Server.Administration.Logs;
public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogManager
{
[Dependency] private readonly IConfigurationManager _configuration = default!;
- [Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IGameTiming _timing = default!;
@@ -72,7 +71,6 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
// CVars
private bool _metricsEnabled;
- private bool _enabled;
private TimeSpan _queueSendDelay;
private int _queueMax;
private int _preRoundQueueMax;
@@ -103,7 +101,7 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
_configuration.OnValueChanged(CVars.MetricsEnabled,
value => _metricsEnabled = value, true);
_configuration.OnValueChanged(CCVars.AdminLogsEnabled,
- value => _enabled = value, true);
+ value => Enabled = value, true);
_configuration.OnValueChanged(CCVars.AdminLogsQueueSendDelay,
value => _queueSendDelay = TimeSpan.FromSeconds(value), true);
_configuration.OnValueChanged(CCVars.AdminLogsQueueMax,
@@ -123,6 +121,12 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
}
}
+ public override string ConvertName(string name)
+ {
+ // JsonNamingPolicy is not whitelisted by the sandbox.
+ return NamingPolicy.ConvertName(name);
+ }
+
public async Task Shutdown()
{
if (!_logQueue.IsEmpty)
@@ -292,8 +296,17 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
}
}
- private void Add(LogType type, LogImpact impact, string message, JsonDocument json, HashSet players)
+ public override void Add(LogType type, [System.Runtime.CompilerServices.InterpolatedStringHandlerArgument("")] ref LogStringHandler handler)
{
+ Add(type, LogImpact.Medium, ref handler);
+ }
+
+ public override void Add(LogType type, LogImpact impact, [System.Runtime.CompilerServices.InterpolatedStringHandlerArgument("")] ref LogStringHandler handler)
+ {
+ var message = handler.ToStringAndClear();
+ if (!Enabled)
+ return;
+
var preRound = _runLevel == GameRunLevel.PreRoundLobby;
var count = preRound ? _preRoundLogQueue.Count : _logQueue.Count;
if (count >= _dropThreshold)
@@ -302,6 +315,10 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
return;
}
+ var json = JsonSerializer.SerializeToDocument(handler.Values, _jsonOptions);
+ var id = NextLogId;
+ var players = GetPlayers(handler.Values, id);
+
// PostgreSQL does not support storing null chars in text values.
if (message.Contains('\0'))
{
@@ -311,31 +328,85 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
var log = new AdminLog
{
- Id = NextLogId,
+ Id = id,
RoundId = _currentRoundId,
Type = type,
Impact = impact,
Date = DateTime.UtcNow,
Message = message,
Json = json,
- Players = new List(players.Count)
+ Players = players,
};
+ DoAdminAlerts(players, message, impact);
+
+ if (preRound)
+ {
+ _preRoundLogQueue.Enqueue(log);
+ }
+ else
+ {
+ _logQueue.Enqueue(log);
+ CacheLog(log);
+ }
+ }
+
+ private List GetPlayers(Dictionary values, int logId)
+ {
+ List players = new();
+ foreach (var value in values.Values)
+ {
+ switch (value)
+ {
+ case SerializablePlayer player:
+ AddPlayer(players, player.UserId, logId);
+ continue;
+
+ case EntityStringRepresentation rep:
+ if (rep.Session is {} session)
+ AddPlayer(players, session.UserId.UserId, logId);
+ continue;
+
+ case IAdminLogsPlayerValue playerValue:
+ foreach (var player in playerValue.Players)
+ {
+ AddPlayer(players, player, logId);
+ }
+
+ break;
+ }
+ }
+
+ return players;
+ }
+
+ private void AddPlayer(List players, Guid user, int logId)
+ {
+ // The majority of logs have a single player, or maybe two. Instead of allocating a List and
+ // HashSet, we just iterate over the list to check for duplicates.
+ foreach (var player in players)
+ {
+ if (player.PlayerUserId == user)
+ return;
+ }
+
+ players.Add(new AdminLogPlayer
+ {
+ LogId = logId,
+ PlayerUserId = user
+ });
+ }
+
+ private void DoAdminAlerts(List players, string message, LogImpact impact)
+ {
var adminLog = false;
- var adminSys = _entityManager.SystemOrNull();
var logMessage = message;
- foreach (var id in players)
+ foreach (var player in players)
{
- var player = new AdminLogPlayer
- {
- LogId = log.Id,
- PlayerUserId = id
- };
+ var id = player.PlayerUserId;
- log.Players.Add(player);
-
- if (adminSys != null)
+ if (EntityManager.TrySystem(out AdminSystem? adminSys))
{
var cachedInfo = adminSys.GetCachedPlayerInfo(new NetUserId(id));
if (cachedInfo != null && cachedInfo.Antag)
@@ -372,35 +443,6 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
if (adminLog)
_chat.SendAdminAlert(logMessage);
-
- if (preRound)
- {
- _preRoundLogQueue.Enqueue(log);
- }
- else
- {
- _logQueue.Enqueue(log);
- CacheLog(log);
- }
- }
-
- public override void Add(LogType type, LogImpact impact, ref LogStringHandler handler)
- {
- if (!_enabled)
- {
- handler.ToStringAndClear();
- return;
- }
-
- var (json, players) = ToJson(handler.Values);
- var message = handler.ToStringAndClear();
-
- Add(type, impact, message, json, players);
- }
-
- public override void Add(LogType type, ref LogStringHandler handler)
- {
- Add(type, LogImpact.Medium, ref handler);
}
public async Task> All(LogFilter? filter = null, Func>? listProvider = null)
diff --git a/Content.Server/Administration/Logs/Converters/AdminLogConverter.cs b/Content.Server/Administration/Logs/Converters/AdminLogConverter.cs
index 7eaab9ba28..778f84c1ac 100644
--- a/Content.Server/Administration/Logs/Converters/AdminLogConverter.cs
+++ b/Content.Server/Administration/Logs/Converters/AdminLogConverter.cs
@@ -6,6 +6,13 @@ namespace Content.Server.Administration.Logs.Converters;
public interface IAdminLogConverter
{
void Init(IDependencyCollection dependencies);
+
+ ///
+ /// Called after all converters have been added to the .
+ ///
+ void Init2(JsonSerializerOptions options)
+ {
+ }
}
public abstract class AdminLogConverter : JsonConverter, IAdminLogConverter
@@ -14,6 +21,10 @@ public abstract class AdminLogConverter : JsonConverter, IAdminLogConverte
{
}
+ public virtual void Init2(JsonSerializerOptions options)
+ {
+ }
+
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotSupportedException();
diff --git a/Content.Server/Administration/Logs/Converters/EntityCoordinatesConverter.cs b/Content.Server/Administration/Logs/Converters/EntityCoordinatesConverter.cs
index fb5c6a6fe5..3a0ffeb758 100644
--- a/Content.Server/Administration/Logs/Converters/EntityCoordinatesConverter.cs
+++ b/Content.Server/Administration/Logs/Converters/EntityCoordinatesConverter.cs
@@ -6,7 +6,7 @@ using Robust.Shared.Map.Components;
namespace Content.Server.Administration.Logs.Converters;
[AdminLogConverter]
-public sealed class EntityCoordinatesConverter : AdminLogConverter
+public sealed class EntityCoordinatesConverter : AdminLogConverter
{
// System.Text.Json actually keeps hold of your JsonSerializerOption instances in a cache on .NET 7.
// Use a weak reference to avoid holding server instances live too long in integration tests.
@@ -17,15 +17,16 @@ public sealed class EntityCoordinatesConverter : AdminLogConverter(dependencies.Resolve());
}
- public void Write(Utf8JsonWriter writer, SerializableEntityCoordinates value, JsonSerializerOptions options, IEntityManager entities)
+ public void Write(Utf8JsonWriter writer, EntityCoordinates value, JsonSerializerOptions options, IEntityManager entities)
{
writer.WriteStartObject();
- WriteEntityInfo(writer, value.EntityUid, entities, "parent");
+ WriteEntityInfo(writer, value.EntityId, entities, "parent");
writer.WriteNumber("x", value.X);
writer.WriteNumber("y", value.Y);
- if (value.MapUid.HasValue)
+ var mapUid = value.GetMapUid(entities);
+ if (mapUid.HasValue)
{
- WriteEntityInfo(writer, value.MapUid.Value, entities, "map");
+ WriteEntityInfo(writer, mapUid.Value, entities, "map");
}
writer.WriteEndObject();
}
@@ -33,7 +34,7 @@ public sealed class EntityCoordinatesConverter : AdminLogConverter().GetMap(coordinates);
- }
-}
diff --git a/Content.Server/Administration/Logs/Converters/EntityStringRepresentationConverter.cs b/Content.Server/Administration/Logs/Converters/EntityStringRepresentationConverter.cs
index 39d34e5f18..9a92a2cb46 100644
--- a/Content.Server/Administration/Logs/Converters/EntityStringRepresentationConverter.cs
+++ b/Content.Server/Administration/Logs/Converters/EntityStringRepresentationConverter.cs
@@ -1,6 +1,5 @@
using System.Text.Json;
using Content.Server.Administration.Managers;
-using Robust.Server.Player;
namespace Content.Server.Administration.Logs.Converters;
@@ -24,7 +23,7 @@ public sealed class EntityStringRepresentationConverter : AdminLogConverter
+{
+ private JsonConverter _converter = null!;
+
+ public override void Init2(JsonSerializerOptions options)
+ {
+ base.Init2(options);
+
+ _converter = (JsonConverter)
+ options.GetConverter(typeof(EntityStringRepresentation));
+ }
+
+ public override void Write(Utf8JsonWriter writer, MindStringRepresentation value, JsonSerializerOptions options)
+ {
+ writer.WriteStartObject();
+
+ if (value.OwnedEntity is { } owned)
+ {
+ writer.WritePropertyName("owned");
+ _converter.Write(writer, owned, options);
+ }
+
+ if (value.Player is { } player)
+ {
+ writer.WriteString("player", player);
+ writer.WriteBoolean("present", value.PlayerPresent);
+ }
+
+ writer.WriteEndObject();
+ }
+}
diff --git a/Content.Server/Administration/Logs/Converters/PlayerSessionConverter.cs b/Content.Server/Administration/Logs/Converters/PlayerSessionConverter.cs
index c1567448cc..d1a009b8cd 100644
--- a/Content.Server/Administration/Logs/Converters/PlayerSessionConverter.cs
+++ b/Content.Server/Administration/Logs/Converters/PlayerSessionConverter.cs
@@ -1,45 +1,23 @@
using System.Text.Json;
-using Robust.Shared.Player;
+using Content.Shared.Administration.Logs;
namespace Content.Server.Administration.Logs.Converters;
[AdminLogConverter]
public sealed class PlayerSessionConverter : AdminLogConverter
{
- // System.Text.Json actually keeps hold of your JsonSerializerOption instances in a cache on .NET 7.
- // Use a weak reference to avoid holding server instances live too long in integration tests.
- private WeakReference _entityManager = default!;
-
- public override void Init(IDependencyCollection dependencies)
- {
- _entityManager = new WeakReference(dependencies.Resolve());
- }
-
public override void Write(Utf8JsonWriter writer, SerializablePlayer value, JsonSerializerOptions options)
{
writer.WriteStartObject();
- if (value.Player.AttachedEntity is {Valid: true} playerEntity)
+ if (value.Uid is {Valid: true} playerEntity)
{
- if (!_entityManager.TryGetTarget(out var entityManager))
- throw new InvalidOperationException("EntityManager got garbage collected!");
-
- writer.WriteNumber("id", (int) value.Player.AttachedEntity);
- writer.WriteString("name", entityManager.GetComponent(playerEntity).EntityName);
+ writer.WriteNumber("id", playerEntity.Id);
+ writer.WriteString("name", value.Name);
}
- writer.WriteString("player", value.Player.UserId.UserId);
+ writer.WriteString("player", value.UserId);
writer.WriteEndObject();
}
}
-
-public readonly struct SerializablePlayer
-{
- public readonly ICommonSession Player;
-
- public SerializablePlayer(ICommonSession player)
- {
- Player = player;
- }
-}
diff --git a/Content.Server/Administration/Systems/AdminFrozenSystem.cs b/Content.Server/Administration/Systems/AdminFrozenSystem.cs
deleted file mode 100644
index baf7b682b8..0000000000
--- a/Content.Server/Administration/Systems/AdminFrozenSystem.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using Content.Shared.Administration;
-
-namespace Content.Server.Administration.Systems;
-
-public sealed class AdminFrozenSystem : SharedAdminFrozenSystem
-{
- ///
- /// Freezes and mutes the given entity.
- ///
- public void FreezeAndMute(EntityUid uid)
- {
- var comp = EnsureComp(uid);
- comp.Muted = true;
- Dirty(uid, comp);
- }
-}
diff --git a/Content.Server/Administration/Systems/AdminSystem.cs b/Content.Server/Administration/Systems/AdminSystem.cs
index 78433db129..677522a83b 100644
--- a/Content.Server/Administration/Systems/AdminSystem.cs
+++ b/Content.Server/Administration/Systems/AdminSystem.cs
@@ -1,7 +1,6 @@
using System.Linq;
using Content.Server.Administration.Managers;
using Content.Server.Chat.Managers;
-using Content.Server.Forensics;
using Content.Server.GameTicking;
using Content.Server.Hands.Systems;
using Content.Server.Mind;
@@ -21,6 +20,7 @@ using Content.Shared.PDA;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Popups;
using Content.Shared.Roles;
+using Content.Shared.Roles.Components;
using Content.Shared.Roles.Jobs;
using Content.Shared.StationRecords;
using Content.Shared.Throwing;
diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
index 1bc4b65999..90e5e46d65 100644
--- a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
+++ b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
@@ -1,10 +1,8 @@
using System.Threading;
using Content.Server.Administration.Components;
-using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
-using Content.Server.Clothing.Systems;
using Content.Server.Electrocution;
using Content.Server.Explosion.EntitySystems;
using Content.Server.GhostKick;
@@ -14,12 +12,12 @@ using Content.Server.Pointing.Components;
using Content.Server.Polymorph.Systems;
using Content.Server.Popups;
using Content.Server.Speech.Components;
-using Content.Server.Storage.Components;
using Content.Server.Storage.EntitySystems;
using Content.Server.Tabletop;
using Content.Server.Tabletop.Components;
using Content.Shared.Administration;
using Content.Shared.Administration.Components;
+using Content.Shared.Atmos.Components;
using Content.Shared.Body.Components;
using Content.Shared.Body.Part;
using Content.Shared.Clumsy;
@@ -29,6 +27,7 @@ using Content.Shared.Damage;
using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.Electrocution;
+using Content.Shared.Gravity;
using Content.Shared.Interaction.Components;
using Content.Shared.Inventory;
using Content.Shared.Mobs;
@@ -39,7 +38,7 @@ using Content.Shared.Movement.Systems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Slippery;
-using Content.Shared.Stunnable;
+using Content.Shared.Storage.Components;
using Content.Shared.Tabletop.Components;
using Content.Shared.Tools.Systems;
using Content.Shared.Verbs;
@@ -49,7 +48,6 @@ using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Random;
-using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Timer = Robust.Shared.Timing.Timer;
@@ -675,6 +673,11 @@ public sealed partial class AdminVerbSystem
grav.Weightless = true;
Dirty(args.Target, grav);
+
+ EnsureComp(args.Target, out var weightless);
+ weightless.Weightless = true;
+
+ Dirty(args.Target, weightless);
},
Impact = LogImpact.Extreme,
Message = string.Join(": ", noGravityName, Loc.GetString("admin-smite-remove-gravity-description"))
diff --git a/Content.Server/Animals/Systems/ParrotMemorySystem.cs b/Content.Server/Animals/Systems/ParrotMemorySystem.cs
index a88957913f..6d8192f5d5 100644
--- a/Content.Server/Animals/Systems/ParrotMemorySystem.cs
+++ b/Content.Server/Animals/Systems/ParrotMemorySystem.cs
@@ -29,12 +29,10 @@ public sealed partial class ParrotMemorySystem : SharedParrotMemorySystem
{
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
- [Dependency] private readonly IAdminManager _admin = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
- [Dependency] private readonly PopupSystem _popup = default!;
public override void Initialize()
{
diff --git a/Content.Server/Anomaly/AnomalyScannerSystem.cs b/Content.Server/Anomaly/AnomalyScannerSystem.cs
new file mode 100644
index 0000000000..ba657cf056
--- /dev/null
+++ b/Content.Server/Anomaly/AnomalyScannerSystem.cs
@@ -0,0 +1,185 @@
+using Content.Server.Anomaly.Components;
+using Content.Server.Anomaly.Effects;
+using Content.Shared.Anomaly;
+using Content.Shared.Anomaly.Components;
+using Content.Shared.DoAfter;
+
+namespace Content.Server.Anomaly;
+
+///
+public sealed class AnomalyScannerSystem : SharedAnomalyScannerSystem
+{
+ [Dependency] private readonly SecretDataAnomalySystem _secretData = default!;
+ [Dependency] private readonly AnomalySystem _anomaly = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnScannerAnomalySeverityChanged);
+ SubscribeLocalEvent(OnScannerAnomalyStabilityChanged);
+ SubscribeLocalEvent(OnScannerAnomalyHealthChanged);
+ SubscribeLocalEvent(OnScannerAnomalyBehaviorChanged);
+
+ Subs.BuiEvents(
+ AnomalyScannerUiKey.Key,
+ subs => subs.Event(OnScannerUiOpened)
+ );
+ }
+
+ /// Updates device with passed anomaly data.
+ public void UpdateScannerWithNewAnomaly(EntityUid scanner, EntityUid anomaly, AnomalyScannerComponent? scannerComp = null, AnomalyComponent? anomalyComp = null)
+ {
+ if (!Resolve(scanner, ref scannerComp) || !Resolve(anomaly, ref anomalyComp))
+ return;
+
+ scannerComp.ScannedAnomaly = anomaly;
+ UpdateScannerUi(scanner, scannerComp);
+
+ TryComp(scanner, out var appearanceComp);
+ TryComp(anomaly, out var secretDataComp);
+
+ Appearance.SetData(scanner, AnomalyScannerVisuals.HasAnomaly, true, appearanceComp);
+
+ var stability = _secretData.IsSecret(anomaly, AnomalySecretData.Stability, secretDataComp)
+ ? AnomalyStabilityVisuals.Stable
+ : _anomaly.GetStabilityVisualOrStable((anomaly, anomalyComp));
+ Appearance.SetData(scanner, AnomalyScannerVisuals.AnomalyStability, stability, appearanceComp);
+
+ var severity = _secretData.IsSecret(anomaly, AnomalySecretData.Severity, secretDataComp)
+ ? 0
+ : anomalyComp.Severity;
+ Appearance.SetData(scanner, AnomalyScannerVisuals.AnomalySeverity, severity, appearanceComp);
+ }
+
+ /// Update scanner interface.
+ public void UpdateScannerUi(EntityUid uid, AnomalyScannerComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+
+ TimeSpan? nextPulse = null;
+ if (TryComp(component.ScannedAnomaly, out var anomalyComponent))
+ nextPulse = anomalyComponent.NextPulseTime;
+
+ var state = new AnomalyScannerUserInterfaceState(_anomaly.GetScannerMessage(component), nextPulse);
+ UI.SetUiState(uid, AnomalyScannerUiKey.Key, state);
+ }
+
+ ///
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var anomalyQuery = EntityQueryEnumerator();
+ while (anomalyQuery.MoveNext(out var ent, out var anomaly))
+ {
+ var secondsUntilNextPulse = (anomaly.NextPulseTime - Timing.CurTime).TotalSeconds;
+ UpdateScannerPulseTimers((ent, anomaly), secondsUntilNextPulse);
+ }
+ }
+
+ ///
+ protected override void OnDoAfter(EntityUid uid, AnomalyScannerComponent component, DoAfterEvent args)
+ {
+ if (args.Cancelled || args.Handled || args.Args.Target == null)
+ return;
+
+ base.OnDoAfter(uid, component, args);
+
+ UpdateScannerWithNewAnomaly(uid, args.Args.Target.Value, component);
+ }
+
+ private void OnScannerAnomalyHealthChanged(ref AnomalyHealthChangedEvent args)
+ {
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var component))
+ {
+ if (component.ScannedAnomaly != args.Anomaly)
+ continue;
+
+ UpdateScannerUi(uid, component);
+ }
+ }
+
+ private void OnScannerUiOpened(EntityUid uid, AnomalyScannerComponent component, BoundUIOpenedEvent args)
+ {
+ UpdateScannerUi(uid, component);
+ }
+
+ private void OnScannerAnomalySeverityChanged(ref AnomalySeverityChangedEvent args)
+ {
+ var severity = _secretData.IsSecret(args.Anomaly, AnomalySecretData.Severity) ? 0 : args.Severity;
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var component))
+ {
+ if (component.ScannedAnomaly != args.Anomaly)
+ continue;
+
+ UpdateScannerUi(uid, component);
+ Appearance.SetData(uid, AnomalyScannerVisuals.AnomalySeverity, severity);
+ }
+ }
+
+ private void OnScannerAnomalyStabilityChanged(ref AnomalyStabilityChangedEvent args)
+ {
+ var stability = _secretData.IsSecret(args.Anomaly, AnomalySecretData.Stability)
+ ? AnomalyStabilityVisuals.Stable
+ : _anomaly.GetStabilityVisualOrStable(args.Anomaly);
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var component))
+ {
+ if (component.ScannedAnomaly != args.Anomaly)
+ continue;
+
+ UpdateScannerUi(uid, component);
+ Appearance.SetData(uid, AnomalyScannerVisuals.AnomalyStability, stability);
+ }
+ }
+
+ private void OnScannerAnomalyBehaviorChanged(ref AnomalyBehaviorChangedEvent args)
+ {
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var component))
+ {
+ if (component.ScannedAnomaly != args.Anomaly)
+ continue;
+
+ UpdateScannerUi(uid, component);
+ // If a field becomes secret, we want to set it to 0 or stable
+ // If a field becomes visible, we need to set it to the correct value, so we need to get the AnomalyComponent
+ if (!TryComp(args.Anomaly, out var anomalyComp))
+ return;
+
+ TryComp(uid, out var appearanceComp);
+ TryComp(args.Anomaly, out var secretDataComp);
+
+ var severity = _secretData.IsSecret(args.Anomaly, AnomalySecretData.Severity, secretDataComp)
+ ? 0
+ : anomalyComp.Severity;
+ Appearance.SetData(uid, AnomalyScannerVisuals.AnomalySeverity, severity, appearanceComp);
+
+ var stability = _secretData.IsSecret(args.Anomaly, AnomalySecretData.Stability, secretDataComp)
+ ? AnomalyStabilityVisuals.Stable
+ : _anomaly.GetStabilityVisualOrStable((args.Anomaly, anomalyComp));
+ Appearance.SetData(uid, AnomalyScannerVisuals.AnomalyStability, stability, appearanceComp);
+ }
+ }
+
+ private void UpdateScannerPulseTimers(Entity anomalyEnt, double secondsUntilNextPulse)
+ {
+ if (secondsUntilNextPulse > 5)
+ return;
+
+ var rounded = Math.Max(0, (int)Math.Ceiling(secondsUntilNextPulse));
+
+ var scannerQuery = EntityQueryEnumerator();
+ while (scannerQuery.MoveNext(out var scannerUid, out var scanner))
+ {
+ if (scanner.ScannedAnomaly != anomalyEnt)
+ continue;
+
+ Appearance.SetData(scannerUid, AnomalyScannerVisuals.AnomalyNextPulse, rounded);
+ }
+ }
+}
diff --git a/Content.Server/Anomaly/AnomalySystem.Scanner.cs b/Content.Server/Anomaly/AnomalySystem.Scanner.cs
deleted file mode 100644
index 9d81878cd8..0000000000
--- a/Content.Server/Anomaly/AnomalySystem.Scanner.cs
+++ /dev/null
@@ -1,241 +0,0 @@
-using Content.Server.Anomaly.Components;
-using Content.Shared.Anomaly;
-using Content.Shared.Anomaly.Components;
-using Content.Shared.DoAfter;
-using Content.Shared.Interaction;
-using Robust.Shared.Player;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Anomaly;
-
-///
-/// This handles the anomaly scanner and it's UI updates.
-///
-public sealed partial class AnomalySystem
-{
- private void InitializeScanner()
- {
- SubscribeLocalEvent(OnScannerUiOpened);
- SubscribeLocalEvent(OnScannerAfterInteract);
- SubscribeLocalEvent(OnDoAfter);
-
- SubscribeLocalEvent(OnScannerAnomalySeverityChanged);
- SubscribeLocalEvent(OnScannerAnomalyHealthChanged);
- SubscribeLocalEvent(OnScannerAnomalyBehaviorChanged);
- }
-
- private void OnScannerAnomalyShutdown(ref AnomalyShutdownEvent args)
- {
- var query = EntityQueryEnumerator();
- while (query.MoveNext(out var uid, out var component))
- {
- if (component.ScannedAnomaly != args.Anomaly)
- continue;
-
- _ui.CloseUi(uid, AnomalyScannerUiKey.Key);
- }
- }
-
- private void OnScannerAnomalySeverityChanged(ref AnomalySeverityChangedEvent args)
- {
- var query = EntityQueryEnumerator();
- while (query.MoveNext(out var uid, out var component))
- {
- if (component.ScannedAnomaly != args.Anomaly)
- continue;
- UpdateScannerUi(uid, component);
- }
- }
-
- private void OnScannerAnomalyStabilityChanged(ref AnomalyStabilityChangedEvent args)
- {
- var query = EntityQueryEnumerator();
- while (query.MoveNext(out var uid, out var component))
- {
- if (component.ScannedAnomaly != args.Anomaly)
- continue;
- UpdateScannerUi(uid, component);
- }
- }
-
- private void OnScannerAnomalyHealthChanged(ref AnomalyHealthChangedEvent args)
- {
- var query = EntityQueryEnumerator();
- while (query.MoveNext(out var uid, out var component))
- {
- if (component.ScannedAnomaly != args.Anomaly)
- continue;
- UpdateScannerUi(uid, component);
- }
- }
-
- private void OnScannerAnomalyBehaviorChanged(ref AnomalyBehaviorChangedEvent args)
- {
- var query = EntityQueryEnumerator();
- while (query.MoveNext(out var uid, out var component))
- {
- if (component.ScannedAnomaly != args.Anomaly)
- continue;
- UpdateScannerUi(uid, component);
- }
- }
-
- private void OnScannerUiOpened(EntityUid uid, AnomalyScannerComponent component, BoundUIOpenedEvent args)
- {
- UpdateScannerUi(uid, component);
- }
-
- private void OnScannerAfterInteract(EntityUid uid, AnomalyScannerComponent component, AfterInteractEvent args)
- {
- if (args.Target is not { } target)
- return;
- if (!HasComp(target))
- return;
- if (!args.CanReach)
- return;
-
- _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, component.ScanDoAfterDuration, new ScannerDoAfterEvent(), uid, target: target, used: uid)
- {
- DistanceThreshold = 2f
- });
- }
-
- private void OnDoAfter(EntityUid uid, AnomalyScannerComponent component, DoAfterEvent args)
- {
- if (args.Cancelled || args.Handled || args.Args.Target == null)
- return;
-
- Audio.PlayPvs(component.CompleteSound, uid);
- Popup.PopupEntity(Loc.GetString("anomaly-scanner-component-scan-complete"), uid);
- UpdateScannerWithNewAnomaly(uid, args.Args.Target.Value, component);
-
- _ui.OpenUi(uid, AnomalyScannerUiKey.Key, args.User);
-
- args.Handled = true;
- }
-
- public void UpdateScannerUi(EntityUid uid, AnomalyScannerComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
-
- TimeSpan? nextPulse = null;
- if (TryComp(component.ScannedAnomaly, out var anomalyComponent))
- nextPulse = anomalyComponent.NextPulseTime;
-
- var state = new AnomalyScannerUserInterfaceState(GetScannerMessage(component), nextPulse);
- _ui.SetUiState(uid, AnomalyScannerUiKey.Key, state);
- }
-
- public void UpdateScannerWithNewAnomaly(EntityUid scanner, EntityUid anomaly, AnomalyScannerComponent? scannerComp = null, AnomalyComponent? anomalyComp = null)
- {
- if (!Resolve(scanner, ref scannerComp) || !Resolve(anomaly, ref anomalyComp))
- return;
-
- scannerComp.ScannedAnomaly = anomaly;
- UpdateScannerUi(scanner, scannerComp);
- }
-
- public FormattedMessage GetScannerMessage(AnomalyScannerComponent component)
- {
- var msg = new FormattedMessage();
- if (component.ScannedAnomaly is not { } anomaly || !TryComp(anomaly, out var anomalyComp))
- {
- msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-no-anomaly"));
- return msg;
- }
-
- TryComp(anomaly, out var secret);
-
- //Severity
- if (secret != null && secret.Secret.Contains(AnomalySecretData.Severity))
- msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-severity-percentage-unknown"));
- else
- msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-severity-percentage", ("percent", anomalyComp.Severity.ToString("P"))));
- msg.PushNewline();
-
- //Stability
- if (secret != null && secret.Secret.Contains(AnomalySecretData.Stability))
- msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-stability-unknown"));
- else
- {
- string stateLoc;
- if (anomalyComp.Stability < anomalyComp.DecayThreshold)
- stateLoc = Loc.GetString("anomaly-scanner-stability-low");
- else if (anomalyComp.Stability > anomalyComp.GrowthThreshold)
- stateLoc = Loc.GetString("anomaly-scanner-stability-high");
- else
- stateLoc = Loc.GetString("anomaly-scanner-stability-medium");
- msg.AddMarkupOrThrow(stateLoc);
- }
- msg.PushNewline();
-
- //Point output
- if (secret != null && secret.Secret.Contains(AnomalySecretData.OutputPoint))
- msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-point-output-unknown"));
- else
- msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-point-output", ("point", GetAnomalyPointValue(anomaly, anomalyComp))));
- msg.PushNewline();
- msg.PushNewline();
-
- //Particles title
- msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-readout"));
- msg.PushNewline();
-
- //Danger
- if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleDanger))
- msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-danger-unknown"));
- else
- msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-danger", ("type", GetParticleLocale(anomalyComp.SeverityParticleType))));
- msg.PushNewline();
-
- //Unstable
- if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleUnstable))
- msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-unstable-unknown"));
- else
- msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-unstable", ("type", GetParticleLocale(anomalyComp.DestabilizingParticleType))));
- msg.PushNewline();
-
- //Containment
- if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleContainment))
- msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-containment-unknown"));
- else
- msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-containment", ("type", GetParticleLocale(anomalyComp.WeakeningParticleType))));
- msg.PushNewline();
-
- //Transformation
- if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleTransformation))
- msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-transformation-unknown"));
- else
- msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-transformation", ("type", GetParticleLocale(anomalyComp.TransformationParticleType))));
-
-
- //Behavior
- msg.PushNewline();
- msg.PushNewline();
- msg.AddMarkupOrThrow(Loc.GetString("anomaly-behavior-title"));
- msg.PushNewline();
-
- if (secret != null && secret.Secret.Contains(AnomalySecretData.Behavior))
- msg.AddMarkupOrThrow(Loc.GetString("anomaly-behavior-unknown"));
- else
- {
- if (anomalyComp.CurrentBehavior != null)
- {
- var behavior = _prototype.Index(anomalyComp.CurrentBehavior.Value);
-
- msg.AddMarkupOrThrow("- " + Loc.GetString(behavior.Description));
- msg.PushNewline();
- var mod = Math.Floor((behavior.EarnPointModifier) * 100);
- msg.AddMarkupOrThrow("- " + Loc.GetString("anomaly-behavior-point", ("mod", mod)));
- }
- else
- {
- msg.AddMarkupOrThrow(Loc.GetString("anomaly-behavior-balanced"));
- }
- }
-
- //The timer at the end here is actually added in the ui itself.
- return msg;
- }
-}
diff --git a/Content.Server/Anomaly/AnomalySystem.Vessel.cs b/Content.Server/Anomaly/AnomalySystem.Vessel.cs
index 98e56a8844..0900f3e96f 100644
--- a/Content.Server/Anomaly/AnomalySystem.Vessel.cs
+++ b/Content.Server/Anomaly/AnomalySystem.Vessel.cs
@@ -22,20 +22,7 @@ public sealed partial class AnomalySystem
SubscribeLocalEvent(OnVesselInteractUsing);
SubscribeLocalEvent(OnExamined);
SubscribeLocalEvent(OnVesselGetPointsPerSecond);
- SubscribeLocalEvent(OnShutdown);
- SubscribeLocalEvent(OnStabilityChanged);
- }
-
- private void OnStabilityChanged(ref AnomalyStabilityChangedEvent args)
- {
- OnVesselAnomalyStabilityChanged(ref args);
- OnScannerAnomalyStabilityChanged(ref args);
- }
-
- private void OnShutdown(ref AnomalyShutdownEvent args)
- {
- OnVesselAnomalyShutdown(ref args);
- OnScannerAnomalyShutdown(ref args);
+ SubscribeLocalEvent(OnVesselAnomalyShutdown);
}
private void OnExamined(EntityUid uid, AnomalyVesselComponent component, ExaminedEvent args)
@@ -141,21 +128,10 @@ public sealed partial class AnomalySystem
if (_pointLight.TryGetLight(uid, out var pointLightComponent))
_pointLight.SetEnabled(uid, on, pointLightComponent);
- // arbitrary value for the generic visualizer to use.
- // i didn't feel like making an enum for this.
- var value = 1;
- if (TryComp(component.Anomaly, out var anomalyComp))
- {
- if (anomalyComp.Stability <= anomalyComp.DecayThreshold)
- {
- value = 2;
- }
- else if (anomalyComp.Stability >= anomalyComp.GrowthThreshold)
- {
- value = 3;
- }
- }
- Appearance.SetData(uid, AnomalyVesselVisuals.AnomalyState, value, appearanceComponent);
+ if (component.Anomaly == null || !TryGetStabilityVisual(component.Anomaly.Value, out var visual))
+ visual = AnomalyStabilityVisuals.Stable;
+
+ Appearance.SetData(uid, AnomalyVesselVisuals.AnomalySeverity, visual, appearanceComponent);
_ambient.SetAmbience(uid, on);
}
diff --git a/Content.Server/Anomaly/AnomalySystem.cs b/Content.Server/Anomaly/AnomalySystem.cs
index 9ac0ce7c94..69f18e5eeb 100644
--- a/Content.Server/Anomaly/AnomalySystem.cs
+++ b/Content.Server/Anomaly/AnomalySystem.cs
@@ -9,7 +9,6 @@ using Content.Server.Station.Systems;
using Content.Shared.Anomaly;
using Content.Shared.Anomaly.Components;
using Content.Shared.Anomaly.Prototypes;
-using Content.Shared.DoAfter;
using Content.Shared.Random;
using Content.Shared.Random.Helpers;
using Robust.Server.GameObjects;
@@ -18,6 +17,7 @@ using Robust.Shared.Configuration;
using Robust.Shared.Physics.Events;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
+using Robust.Shared.Utility;
namespace Content.Server.Anomaly;
@@ -30,7 +30,6 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly AmbientSoundSystem _ambient = default!;
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
- [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly ExplosionSystem _explosion = default!;
[Dependency] private readonly MaterialStorageSystem _material = default!;
[Dependency] private readonly SharedPointLightSystem _pointLight = default!;
@@ -53,10 +52,9 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
SubscribeLocalEvent(OnMapInit);
SubscribeLocalEvent(OnShutdown);
SubscribeLocalEvent(OnStartCollide);
-
+ SubscribeLocalEvent(OnVesselAnomalyStabilityChanged);
InitializeGenerator();
- InitializeScanner();
InitializeVessel();
InitializeCommands();
}
@@ -218,4 +216,112 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
EntityManager.RemoveComponents(anomaly, behavior.Components);
}
#endregion
+
+ #region Information
+ ///
+ /// Get a formatted message with a summary of all anomaly information for putting on a UI.
+ ///
+ public FormattedMessage GetScannerMessage(AnomalyScannerComponent component)
+ {
+ var msg = new FormattedMessage();
+ if (component.ScannedAnomaly is not { } anomaly || !TryComp(anomaly, out var anomalyComp))
+ {
+ msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-no-anomaly"));
+ return msg;
+ }
+
+ TryComp(anomaly, out var secret);
+
+ //Severity
+ if (secret != null && secret.Secret.Contains(AnomalySecretData.Severity))
+ msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-severity-percentage-unknown"));
+ else
+ msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-severity-percentage", ("percent", anomalyComp.Severity.ToString("P"))));
+ msg.PushNewline();
+
+ //Stability
+ if (secret != null && secret.Secret.Contains(AnomalySecretData.Stability))
+ msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-stability-unknown"));
+ else
+ {
+ string stateLoc;
+ if (anomalyComp.Stability < anomalyComp.DecayThreshold)
+ stateLoc = Loc.GetString("anomaly-scanner-stability-low");
+ else if (anomalyComp.Stability > anomalyComp.GrowthThreshold)
+ stateLoc = Loc.GetString("anomaly-scanner-stability-high");
+ else
+ stateLoc = Loc.GetString("anomaly-scanner-stability-medium");
+ msg.AddMarkupOrThrow(stateLoc);
+ }
+ msg.PushNewline();
+
+ //Point output
+ if (secret != null && secret.Secret.Contains(AnomalySecretData.OutputPoint))
+ msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-point-output-unknown"));
+ else
+ msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-point-output", ("point", GetAnomalyPointValue(anomaly, anomalyComp))));
+ msg.PushNewline();
+ msg.PushNewline();
+
+ //Particles title
+ msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-readout"));
+ msg.PushNewline();
+
+ //Danger
+ if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleDanger))
+ msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-danger-unknown"));
+ else
+ msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-danger", ("type", GetParticleLocale(anomalyComp.SeverityParticleType))));
+ msg.PushNewline();
+
+ //Unstable
+ if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleUnstable))
+ msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-unstable-unknown"));
+ else
+ msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-unstable", ("type", GetParticleLocale(anomalyComp.DestabilizingParticleType))));
+ msg.PushNewline();
+
+ //Containment
+ if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleContainment))
+ msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-containment-unknown"));
+ else
+ msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-containment", ("type", GetParticleLocale(anomalyComp.WeakeningParticleType))));
+ msg.PushNewline();
+
+ //Transformation
+ if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleTransformation))
+ msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-transformation-unknown"));
+ else
+ msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-transformation", ("type", GetParticleLocale(anomalyComp.TransformationParticleType))));
+
+
+ //Behavior
+ msg.PushNewline();
+ msg.PushNewline();
+ msg.AddMarkupOrThrow(Loc.GetString("anomaly-behavior-title"));
+ msg.PushNewline();
+
+ if (secret != null && secret.Secret.Contains(AnomalySecretData.Behavior))
+ msg.AddMarkupOrThrow(Loc.GetString("anomaly-behavior-unknown"));
+ else
+ {
+ if (anomalyComp.CurrentBehavior != null)
+ {
+ var behavior = _prototype.Index(anomalyComp.CurrentBehavior.Value);
+
+ msg.AddMarkupOrThrow("- " + Loc.GetString(behavior.Description));
+ msg.PushNewline();
+ var mod = Math.Floor((behavior.EarnPointModifier) * 100);
+ msg.AddMarkupOrThrow("- " + Loc.GetString("anomaly-behavior-point", ("mod", mod)));
+ }
+ else
+ {
+ msg.AddMarkupOrThrow(Loc.GetString("anomaly-behavior-balanced"));
+ }
+ }
+
+ //The timer at the end here is actually added in the ui itself.
+ return msg;
+ }
+ #endregion
}
diff --git a/Content.Server/Anomaly/Effects/PyroclasticAnomalySystem.cs b/Content.Server/Anomaly/Effects/PyroclasticAnomalySystem.cs
index d38bda562b..5ceb9888f4 100644
--- a/Content.Server/Anomaly/Effects/PyroclasticAnomalySystem.cs
+++ b/Content.Server/Anomaly/Effects/PyroclasticAnomalySystem.cs
@@ -1,7 +1,7 @@
-using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Anomaly.Components;
using Content.Shared.Anomaly.Effects.Components;
+using Content.Shared.Atmos.Components;
using Robust.Shared.Map;
namespace Content.Server.Anomaly.Effects;
diff --git a/Content.Server/Anomaly/Effects/SecretDataAnomalySystem.cs b/Content.Server/Anomaly/Effects/SecretDataAnomalySystem.cs
index cbdc4b04df..0515ed855e 100644
--- a/Content.Server/Anomaly/Effects/SecretDataAnomalySystem.cs
+++ b/Content.Server/Anomaly/Effects/SecretDataAnomalySystem.cs
@@ -36,5 +36,13 @@ public sealed class SecretDataAnomalySystem : EntitySystem
component.Secret.Add(_random.PickAndTake(_deita));
}
}
+
+ public bool IsSecret(EntityUid uid, AnomalySecretData item, SecretDataAnomalyComponent? component = null)
+ {
+ if (!Resolve(uid, ref component, logMissing: false))
+ return false;
+
+ return component.Secret.Contains(item);
+ }
}
diff --git a/Content.Server/Atmos/Components/DeltaPressureComponent.cs b/Content.Server/Atmos/Components/DeltaPressureComponent.cs
new file mode 100644
index 0000000000..f90c133dea
--- /dev/null
+++ b/Content.Server/Atmos/Components/DeltaPressureComponent.cs
@@ -0,0 +1,139 @@
+using Content.Server.Atmos.EntitySystems;
+using Content.Shared.Damage;
+using Content.Shared.FixedPoint;
+
+namespace Content.Server.Atmos.Components;
+
+///
+/// Entities that have this component will have damage done to them depending on the local pressure
+/// environment that they reside in.
+///
+/// Atmospherics.DeltaPressure batch-processes entities with this component in a list on
+/// the grid's .
+/// The entities are automatically added and removed from this list, and automatically
+/// added on initialization.
+///
+/// Note that the entity should have an and be a grid structure.
+[RegisterComponent]
+public sealed partial class DeltaPressureComponent : Component
+{
+ ///
+ /// Whether the entity is currently in the processing list of the grid's .
+ ///
+ [DataField(readOnly: true)]
+ [ViewVariables(VVAccess.ReadOnly)]
+ [Access(typeof(DeltaPressureSystem), typeof(AtmosphereSystem))]
+ public bool InProcessingList;
+
+ ///
+ /// Whether this entity is currently taking damage from pressure.
+ ///
+ [DataField(readOnly: true)]
+ [ViewVariables(VVAccess.ReadOnly)]
+ [Access(typeof(DeltaPressureSystem), typeof(AtmosphereSystem))]
+ public bool IsTakingDamage;
+
+ ///
+ /// The current cached position of this entity on the grid.
+ /// Updated via MoveEvent.
+ ///
+ [DataField(readOnly: true)]
+ public Vector2i CurrentPosition = Vector2i.Zero;
+
+ ///
+ /// The grid this entity is currently joined to for processing.
+ /// Required for proper deletion, as we cannot reference the grid
+ /// for removal while the entity is being deleted.
+ ///
+ [DataField]
+ public EntityUid? GridUid;
+
+ ///
+ /// The percent chance that the entity will take damage each atmos tick,
+ /// when the entity is above the damage threshold.
+ /// Makes it so that windows don't all break in one go.
+ /// Float is from 0 to 1, where 1 means 100% chance.
+ /// If this is set to 0, the entity will never take damage.
+ ///
+ [DataField]
+ public float RandomDamageChance = 1f;
+
+ ///
+ /// The base damage applied to the entity per atmos tick when it is above the damage threshold.
+ /// This damage will be scaled as defined by the enum
+ /// depending on the current effective pressure this entity is experiencing.
+ /// Note that this damage will scale depending on the pressure above the minimum pressure,
+ /// not at the current pressure.
+ ///
+ [DataField]
+ public DamageSpecifier BaseDamage = new()
+ {
+ DamageDict = new Dictionary
+ {
+ { "Structural", 10 },
+ },
+ };
+
+ ///
+ /// The minimum pressure in kPa at which the entity will start taking damage.
+ /// This doesn't depend on the difference in pressure.
+ /// The entity will start to take damage if it is exposed to this pressure.
+ /// This is needed because we don't correctly handle 2-layer windows yet.
+ ///
+ [DataField]
+ public float MinPressure = 10000;
+
+ ///
+ /// The minimum difference in pressure between any side required for the entity to start taking damage.
+ ///
+ [DataField]
+ public float MinPressureDelta = 7500;
+
+ ///
+ /// The maximum pressure at which damage will no longer scale.
+ /// If the effective pressure goes beyond this, the damage will be considered at this pressure.
+ ///
+ [DataField]
+ public float MaxEffectivePressure = 10000;
+
+ ///
+ /// Simple constant to affect the scaling behavior.
+ /// See comments in the types to see how this affects scaling.
+ ///
+ [DataField]
+ public float ScalingPower = 1;
+
+ ///
+ /// Defines the scaling behavior for the damage.
+ ///
+ [DataField]
+ public DeltaPressureDamageScalingType ScalingType = DeltaPressureDamageScalingType.Threshold;
+}
+
+///
+/// An enum that defines how the damage dealt by the scales
+/// depending on the pressure experienced by the entity.
+/// The scaling is done on the effective pressure, which is the pressure above the minimum pressure.
+/// See https://www.desmos.com/calculator/9ctlq3zpnt for a visual representation of the scaling types.
+///
+[Serializable]
+public enum DeltaPressureDamageScalingType : byte
+{
+ ///
+ /// Damage dealt will be constant as long as the minimum values are met.
+ /// Scaling power is ignored.
+ ///
+ Threshold,
+
+ ///
+ /// Damage dealt will be a linear function.
+ /// Scaling power determines the slope of the function.
+ ///
+ Linear,
+
+ ///
+ /// Damage dealt will be a logarithmic function.
+ /// Scaling power determines the base of the log.
+ ///
+ Log,
+}
diff --git a/Content.Server/Atmos/Components/GridAtmosphereComponent.cs b/Content.Server/Atmos/Components/GridAtmosphereComponent.cs
index e682fd0964..2d36d2bd14 100644
--- a/Content.Server/Atmos/Components/GridAtmosphereComponent.cs
+++ b/Content.Server/Atmos/Components/GridAtmosphereComponent.cs
@@ -1,3 +1,4 @@
+using System.Collections.Concurrent;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Serialization;
@@ -61,6 +62,39 @@ namespace Content.Server.Atmos.Components
[ViewVariables]
public int HighPressureDeltaCount => HighPressureDelta.Count;
+ ///
+ /// A list of entities that have a and are to
+ /// be processed by the , if enabled.
+ ///
+ /// To prevent massive bookkeeping overhead, this list is processed in-place,
+ /// with add/remove/find operations helped via a dict.
+ ///
+ /// If you want to add/remove/find entities in this list,
+ /// use the API methods in the Atmospherics API.
+ [ViewVariables]
+ public readonly List> DeltaPressureEntities =
+ new(AtmosphereSystem.DeltaPressurePreAllocateLength);
+
+ ///
+ /// An index lookup for the list.
+ /// Used for add/remove/find operations to speed up processing.
+ ///
+ public readonly Dictionary DeltaPressureEntityLookup =
+ new(AtmosphereSystem.DeltaPressurePreAllocateLength);
+
+ ///
+ /// Integer that indicates the current position in the
+ /// list that is being processed.
+ ///
+ [ViewVariables(VVAccess.ReadOnly)]
+ public int DeltaPressureCursor;
+
+ ///
+ /// Queue of entities that need to have damage applied to them.
+ ///
+ [ViewVariables]
+ public readonly ConcurrentQueue DeltaPressureDamageResults = new();
+
[ViewVariables]
public readonly HashSet PipeNets = new();
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs
index 67f3a20779..87cfce135d 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics;
using System.Linq;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.Piping.Components;
@@ -5,6 +6,7 @@ using Content.Server.NodeContainer.NodeGroups;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.Reactions;
+using JetBrains.Annotations;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
@@ -319,6 +321,107 @@ public partial class AtmosphereSystem
return true;
}
+ ///
+ /// Adds an entity with a DeltaPressureComponent to the DeltaPressure processing list.
+ /// Also fills in important information on the component itself.
+ ///
+ /// The grid to add the entity to.
+ /// The entity to add.
+ /// True if the entity was added to the list, false if it could not be added or
+ /// if the entity was already present in the list.
+ [PublicAPI]
+ public bool TryAddDeltaPressureEntity(Entity grid, Entity ent)
+ {
+ // The entity needs to be part of a grid, and it should be the right one :)
+ var xform = Transform(ent);
+
+ // The entity is not on a grid, so it cannot possibly have an atmosphere that affects it.
+ if (xform.GridUid == null)
+ {
+ return false;
+ }
+
+ // Entity should be on the grid it's being added to.
+ Debug.Assert(xform.GridUid == grid.Owner);
+
+ if (!_atmosQuery.Resolve(grid, ref grid.Comp, false))
+ return false;
+
+ if (grid.Comp.DeltaPressureEntityLookup.ContainsKey(ent.Owner))
+ {
+ return false;
+ }
+
+ grid.Comp.DeltaPressureEntityLookup[ent.Owner] = grid.Comp.DeltaPressureEntities.Count;
+ grid.Comp.DeltaPressureEntities.Add(ent);
+
+ ent.Comp.CurrentPosition = _map.CoordinatesToTile(grid,
+ Comp(grid),
+ xform.Coordinates);
+
+ ent.Comp.GridUid = grid.Owner;
+ ent.Comp.InProcessingList = true;
+
+ return true;
+ }
+
+ ///
+ /// Removes an entity with a DeltaPressureComponent from the DeltaPressure processing list.
+ ///
+ /// The grid to remove the entity from.
+ /// The entity to remove.
+ /// True if the entity was removed from the list, false if it could not be removed or
+ /// if the entity was not present in the list.
+ [PublicAPI]
+ public bool TryRemoveDeltaPressureEntity(Entity grid, Entity ent)
+ {
+ if (!_atmosQuery.Resolve(grid, ref grid.Comp, false))
+ return false;
+
+ if (!grid.Comp.DeltaPressureEntityLookup.TryGetValue(ent.Owner, out var index))
+ return false;
+
+ var lastIndex = grid.Comp.DeltaPressureEntities.Count - 1;
+ if (lastIndex < 0)
+ return false;
+
+ if (index != lastIndex)
+ {
+ var lastEnt = grid.Comp.DeltaPressureEntities[lastIndex];
+ grid.Comp.DeltaPressureEntities[index] = lastEnt;
+ grid.Comp.DeltaPressureEntityLookup[lastEnt.Owner] = index;
+ }
+
+ grid.Comp.DeltaPressureEntities.RemoveAt(lastIndex);
+ grid.Comp.DeltaPressureEntityLookup.Remove(ent.Owner);
+
+ if (grid.Comp.DeltaPressureCursor > grid.Comp.DeltaPressureEntities.Count)
+ grid.Comp.DeltaPressureCursor = grid.Comp.DeltaPressureEntities.Count;
+
+ ent.Comp.InProcessingList = false;
+ ent.Comp.GridUid = null;
+ return true;
+ }
+
+ ///
+ /// Checks if a DeltaPressureComponent is currently considered for processing on a grid.
+ ///
+ /// The grid that the entity may belong to.
+ /// The entity to check.
+ /// True if the entity is part of the processing list, false otherwise.
+ [PublicAPI]
+ public bool IsDeltaPressureEntityInList(Entity grid, Entity ent)
+ {
+ // Dict and list must be in sync - deep-fried if we aren't.
+ if (!_atmosQuery.Resolve(grid, ref grid.Comp, false))
+ return false;
+
+ var contains = grid.Comp.DeltaPressureEntityLookup.ContainsKey(ent.Owner);
+ Debug.Assert(contains == grid.Comp.DeltaPressureEntities.Contains(ent));
+
+ return contains;
+ }
+
[ByRefEvent] private record struct SetSimulatedGridMethodEvent
(EntityUid Grid, bool Simulated, bool Handled = false);
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.BenchmarkHelpers.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.BenchmarkHelpers.cs
new file mode 100644
index 0000000000..f86ebcee73
--- /dev/null
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.BenchmarkHelpers.cs
@@ -0,0 +1,49 @@
+using Content.Server.Atmos.Components;
+using Content.Shared.Atmos.Components;
+using Robust.Shared.Map.Components;
+
+namespace Content.Server.Atmos.EntitySystems;
+
+public sealed partial class AtmosphereSystem
+{
+ /*
+ Helper methods to assist in getting very low overhead profiling of individual stages of the atmospherics simulation.
+ Ideal for benchmarking and performance testing.
+ These methods obviously aren't to be used in production code. Don't call them. They know my voice.
+ */
+
+ ///
+ /// Runs the grid entity through a single processing stage of the atmosphere simulation.
+ /// Ideal for benchmarking single stages of the simulation.
+ ///
+ /// The entity to profile Atmospherics with.
+ /// The state to profile on the entity.
+ /// The optional mapEntity to provide when benchmarking ProcessAtmosDevices.
+ /// True if the processing stage completed, false if the processing stage had to pause processing due to time constraints.
+ public bool RunProcessingStage(
+ Entity ent,
+ AtmosphereProcessingState state,
+ Entity? mapEnt = null)
+ {
+ var processingPaused = state switch
+ {
+ AtmosphereProcessingState.Revalidate => ProcessRevalidate(ent),
+ AtmosphereProcessingState.TileEqualize => ProcessTileEqualize(ent),
+ AtmosphereProcessingState.ActiveTiles => ProcessActiveTiles(ent),
+ AtmosphereProcessingState.ExcitedGroups => ProcessExcitedGroups(ent),
+ AtmosphereProcessingState.HighPressureDelta => ProcessHighPressureDelta(ent),
+ AtmosphereProcessingState.DeltaPressure => ProcessDeltaPressure(ent),
+ AtmosphereProcessingState.Hotspots => ProcessHotspots(ent),
+ AtmosphereProcessingState.Superconductivity => ProcessSuperconductivity(ent),
+ AtmosphereProcessingState.PipeNet => ProcessPipeNets(ent),
+ AtmosphereProcessingState.AtmosDevices => mapEnt is not null
+ ? ProcessAtmosDevices(ent, mapEnt.Value)
+ : throw new ArgumentException(
+ "An Entity must be provided when benchmarking ProcessAtmosDevices."),
+ _ => throw new ArgumentOutOfRangeException(),
+ };
+ ent.Comp1.ProcessingPaused = !processingPaused;
+
+ return processingPaused;
+ }
+}
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.CVars.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.CVars.cs
index 3aaa5429fb..f24f0ae171 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.CVars.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.CVars.cs
@@ -26,6 +26,9 @@ namespace Content.Server.Atmos.EntitySystems
public float AtmosTickRate { get; private set; }
public float Speedup { get; private set; }
public float HeatScale { get; private set; }
+ public bool DeltaPressureDamage { get; private set; }
+ public int DeltaPressureParallelProcessPerIteration { get; private set; }
+ public int DeltaPressureParallelBatchSize { get; private set; }
///
/// Time between each atmos sub-update. If you are writing an atmos device, use AtmosDeviceUpdateEvent.dt
@@ -55,6 +58,9 @@ namespace Content.Server.Atmos.EntitySystems
Subs.CVar(_cfg, CCVars.AtmosHeatScale, value => { HeatScale = value; InitializeGases(); }, true);
Subs.CVar(_cfg, CCVars.ExcitedGroups, value => ExcitedGroups = value, true);
Subs.CVar(_cfg, CCVars.ExcitedGroupsSpaceIsAllConsuming, value => ExcitedGroupsSpaceIsAllConsuming = value, true);
+ Subs.CVar(_cfg, CCVars.DeltaPressureDamage, value => DeltaPressureDamage = value, true);
+ Subs.CVar(_cfg, CCVars.DeltaPressureParallelToProcessPerIteration, value => DeltaPressureParallelProcessPerIteration = value, true);
+ Subs.CVar(_cfg, CCVars.DeltaPressureParallelBatchSize, value => DeltaPressureParallelBatchSize = value, true);
}
}
}
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.DeltaPressure.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.DeltaPressure.cs
new file mode 100644
index 0000000000..9d72b195fe
--- /dev/null
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.DeltaPressure.cs
@@ -0,0 +1,260 @@
+using Content.Server.Atmos.Components;
+using Content.Shared.Atmos;
+using Content.Shared.Damage;
+using Robust.Shared.Random;
+using Robust.Shared.Threading;
+
+namespace Content.Server.Atmos.EntitySystems;
+
+public sealed partial class AtmosphereSystem
+{
+ ///
+ /// The number of pairs of opposing directions we can have.
+ /// This is Atmospherics.Directions / 2, since we always compare opposing directions
+ /// (e.g. North vs South, East vs West, etc.).
+ /// Used to determine the size of the opposing groups when processing delta pressure entities.
+ ///
+ private const int DeltaPressurePairCount = Atmospherics.Directions / 2;
+
+ ///
+ /// The length to pre-allocate list/dicts of delta pressure entities on a .
+ ///
+ public const int DeltaPressurePreAllocateLength = 1000;
+
+ ///
+ /// Processes a singular entity, determining the pressures it's experiencing and applying damage based on that.
+ ///
+ /// The entity to process.
+ /// The that belongs to the entity's GridUid.
+ private void ProcessDeltaPressureEntity(Entity ent, GridAtmosphereComponent gridAtmosComp)
+ {
+ if (!_random.Prob(ent.Comp.RandomDamageChance))
+ return;
+
+ /*
+ To make our comparisons a little bit faster, we take advantage of SIMD-accelerated methods
+ in the NumericsHelpers class.
+
+ This involves loading our values into a span in the form of opposing pairs,
+ so simple vector operations like min/max/abs can be performed on them.
+ */
+
+ var tiles = new TileAtmosphere?[Atmospherics.Directions];
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ var direction = (AtmosDirection)(1 << i);
+ var offset = ent.Comp.CurrentPosition.Offset(direction);
+ tiles[i] = gridAtmosComp.Tiles.GetValueOrDefault(offset);
+ }
+
+ Span pressures = stackalloc float[Atmospherics.Directions];
+
+ GetBulkTileAtmospherePressures(tiles, pressures);
+
+ Span opposingGroupA = stackalloc float[DeltaPressurePairCount];
+ Span