diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 5017422e13..f099682b6a 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -26,6 +26,9 @@
/Content.*/Trigger/ @slarticodefast
+/Content.*/Stunnable/ @Princess-Cheeseballs
+/Content.*/Nutrition/ @Princess-Cheeseballs
+
# SKREEEE
/Content.*.Database/ @PJB3005 @DrSmugleaf
/Content.Shared.Database/Log*.cs @PJB3005 @DrSmugleaf @crazybrain23
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/Access/UI/AccessOverriderWindow.xaml.cs b/Content.Client/Access/UI/AccessOverriderWindow.xaml.cs
index 092a0071fb..8e527d7343 100644
--- a/Content.Client/Access/UI/AccessOverriderWindow.xaml.cs
+++ b/Content.Client/Access/UI/AccessOverriderWindow.xaml.cs
@@ -29,7 +29,7 @@ namespace Content.Client.Access.UI
foreach (var access in accessLevels)
{
- if (!protoManager.TryIndex(access, out var accessLevel))
+ if (!protoManager.Resolve(access, out var accessLevel))
{
continue;
}
diff --git a/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs b/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs
index 4f07c31009..41d5a84654 100644
--- a/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs
+++ b/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs
@@ -57,7 +57,7 @@ public sealed partial class GroupedAccessLevelChecklist : BoxContainer
foreach (var accessGroup in _accessGroups)
{
- if (!_protoManager.TryIndex(accessGroup, out var accessGroupProto))
+ if (!_protoManager.Resolve(accessGroup, out var accessGroupProto))
continue;
_groupedAccessLevels.Add(accessGroupProto, new());
@@ -65,13 +65,13 @@ public sealed partial class GroupedAccessLevelChecklist : BoxContainer
// Ensure that the 'general' access group is added to handle
// misc. access levels that aren't associated with any group
- if (_protoManager.TryIndex(GeneralAccessGroup, out var generalAccessProto))
+ if (_protoManager.Resolve(GeneralAccessGroup, out var generalAccessProto))
_groupedAccessLevels.TryAdd(generalAccessProto, new());
// Assign known access levels with their associated groups
foreach (var accessLevel in _accessLevels)
{
- if (!_protoManager.TryIndex(accessLevel, out var accessLevelProto))
+ if (!_protoManager.Resolve(accessLevel, out var accessLevelProto))
continue;
var assigned = false;
diff --git a/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs b/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs
index f3a37f054e..801140f517 100644
--- a/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs
+++ b/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs
@@ -4,6 +4,7 @@ using Content.Shared.Access.Systems;
using Content.Shared.CCVar;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.CrewManifest;
+using Content.Shared.Roles;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using static Content.Shared.Access.Components.IdCardConsoleComponent;
@@ -74,7 +75,7 @@ namespace Content.Client.Access.UI
_window?.UpdateState(castState);
}
- public void SubmitData(string newFullName, string newJobTitle, List> newAccessList, string newJobPrototype)
+ public void SubmitData(string newFullName, string newJobTitle, List> newAccessList, ProtoId newJobPrototype)
{
if (newFullName.Length > _maxNameLength)
newFullName = newFullName[.._maxNameLength];
diff --git a/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs b/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs
index 48ae1b0ced..202653f700 100644
--- a/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs
+++ b/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs
@@ -123,7 +123,7 @@ namespace Content.Client.Access.UI
foreach (var group in job.AccessGroups)
{
- if (!_prototypeManager.TryIndex(group, out AccessGroupPrototype? groupPrototype))
+ if (!_prototypeManager.Resolve(group, out AccessGroupPrototype? groupPrototype))
{
continue;
}
diff --git a/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs b/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs
index 46090a6f3d..c7aed30c1c 100644
--- a/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs
+++ b/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs
@@ -316,8 +316,9 @@ public sealed partial class BanPanel : DefaultWindow
};
// This is adding the icon before the role name
- // Yeah, this is sus, but having to split the functions up and stuff is worse imo.
- if (_protoMan.TryIndex(role, out var jobPrototype) && _protoMan.TryIndex(jobPrototype.Icon, out var iconProto))
+ // TODO: This should not be using raw strings for prototypes as it means it won't be validated at all.
+ // I know the ban manager is doing the same thing, but that should not leak into UI code.
+ if (_protoMan.TryIndex(role, out var jobPrototype) && _protoMan.Resolve(jobPrototype.Icon, out var iconProto))
{
var jobIconTexture = new TextureRect
{
diff --git a/Content.Client/Anomaly/AnomalyScannerScreenComponent.cs b/Content.Client/Anomaly/AnomalyScannerScreenComponent.cs
new file mode 100644
index 0000000000..8e0b911fb7
--- /dev/null
+++ b/Content.Client/Anomaly/AnomalyScannerScreenComponent.cs
@@ -0,0 +1,40 @@
+using Robust.Client.Graphics;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace Content.Client.Anomaly;
+
+///
+/// This component creates and handles the drawing of a ScreenTexture to be used on the Anomaly Scanner
+/// for an indicator of Anomaly Severity.
+///
+///
+/// In the future I would like to make this a more generic "DynamicTextureComponent" that can contain a dictionary
+/// of texture components like "Bar(offset, size, minimumValue, maximumValue, AppearanceKey, LayerMapKey)" that can
+/// just draw a bar or other basic drawn element that will show up on a texture layer.
+///
+[RegisterComponent]
+[Access(typeof(AnomalyScannerSystem))]
+public sealed partial class AnomalyScannerScreenComponent : Component
+{
+ ///
+ /// This is the texture drawn as a layer on the Anomaly Scanner device.
+ ///
+ public OwnedTexture? ScreenTexture;
+
+ ///
+ /// A small buffer that we can reuse to draw the severity bar.
+ ///
+ public Rgba32[]? BarBuf;
+
+ ///
+ /// The position of the top-left of the severity bar in pixels.
+ ///
+ [DataField(readOnly: true)]
+ public Vector2i Offset = new Vector2i(12, 17);
+
+ ///
+ /// The width and height of the severity bar in pixels.
+ ///
+ [DataField(readOnly: true)]
+ public Vector2i Size = new Vector2i(10, 3);
+}
diff --git a/Content.Client/Anomaly/AnomalyScannerSystem.cs b/Content.Client/Anomaly/AnomalyScannerSystem.cs
new file mode 100644
index 0000000000..f80e5ead54
--- /dev/null
+++ b/Content.Client/Anomaly/AnomalyScannerSystem.cs
@@ -0,0 +1,110 @@
+using System.Numerics;
+using Content.Shared.Anomaly;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Shared.Utility;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace Content.Client.Anomaly;
+
+///
+public sealed class AnomalyScannerSystem : SharedAnomalyScannerSystem
+{
+ [Dependency] private readonly IClyde _clyde = default!;
+ [Dependency] private readonly SpriteSystem _sprite = default!;
+
+ private const float MaxHueDegrees = 360f;
+ private const float GreenHueDegrees = 110f;
+ private const float RedHueDegrees = 0f;
+ private const float GreenHue = GreenHueDegrees / MaxHueDegrees;
+ private const float RedHue = RedHueDegrees / MaxHueDegrees;
+
+
+ // Just an array to initialize the pixels of a new OwnedTexture
+ private static readonly Rgba32[] EmptyTexture = new Rgba32[32*32];
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnComponentInit);
+ SubscribeLocalEvent(OnComponentStartup);
+ SubscribeLocalEvent(OnScannerAppearanceChanged);
+ }
+
+ private void OnComponentInit(Entity ent, ref ComponentInit args)
+ {
+ if(!_sprite.TryGetLayer(ent.Owner, AnomalyScannerVisualLayers.Base, out var layer, true))
+ return;
+
+ // Allocate the OwnedTexture
+ ent.Comp.ScreenTexture = _clyde.CreateBlankTexture(layer.PixelSize);
+
+ if (layer.PixelSize.X < ent.Comp.Offset.X + ent.Comp.Size.X ||
+ layer.PixelSize.Y < ent.Comp.Offset.Y + ent.Comp.Size.Y)
+ {
+ // If the bar doesn't fit, just bail here, ScreenTexture and BarBuf will remain null, and appearance updates
+ // will do nothing.
+ DebugTools.Assert(false, "AnomalyScannerScreenComponent: Bar does not fit within sprite");
+ return;
+ }
+
+
+ // Initialize the texture
+ ent.Comp.ScreenTexture.SetSubImage((0, 0), layer.PixelSize, new ReadOnlySpan(EmptyTexture));
+
+ // Initialize bar drawing buffer
+ ent.Comp.BarBuf = new Rgba32[ent.Comp.Size.X * ent.Comp.Size.Y];
+ }
+
+ private void OnComponentStartup(Entity ent, ref ComponentStartup args)
+ {
+ if (!TryComp(ent, out var sprite))
+ return;
+
+ _sprite.LayerSetTexture((ent, sprite), AnomalyScannerVisualLayers.Screen, ent.Comp.ScreenTexture);
+ }
+
+ private void OnScannerAppearanceChanged(Entity ent, ref AppearanceChangeEvent args)
+ {
+ if (args.Sprite is null || ent.Comp.ScreenTexture is null || ent.Comp.BarBuf is null)
+ return;
+
+ args.AppearanceData.TryGetValue(AnomalyScannerVisuals.AnomalySeverity, out var severityObj);
+ if (severityObj is not float severity)
+ severity = 0;
+
+ // Get the bar length
+ var barLength = (int)(severity * ent.Comp.Size.X);
+
+ // Calculate the bar color
+ // Hue "angle" of two colors to interpolate between depending on severity
+ // Just a lerp from Green hue at severity = 0.5 to Red hue at 1.0
+ var hue = Math.Clamp(2*GreenHue * (1 - severity), RedHue, GreenHue);
+ var color = new Rgba32(Color.FromHsv(new Vector4(hue, 1f, 1f, 1f)).RGBA);
+
+ var transparent = new Rgba32(0, 0, 0, 255);
+
+ for(var y = 0; y < ent.Comp.Size.Y; y++)
+ {
+ for (var x = 0; x < ent.Comp.Size.X; x++)
+ {
+ ent.Comp.BarBuf[y*ent.Comp.Size.X + x] = x < barLength ? color : transparent;
+ }
+ }
+
+ // Copy the buffer to the texture
+ try
+ {
+ ent.Comp.ScreenTexture.SetSubImage(
+ ent.Comp.Offset,
+ ent.Comp.Size,
+ new ReadOnlySpan(ent.Comp.BarBuf)
+ );
+ }
+ catch (IndexOutOfRangeException)
+ {
+ Log.Warning($"Bar dimensions out of bounds with the texture on entity {ent.Owner}");
+ }
+ }
+}
diff --git a/Content.Client/Anomaly/AnomalySystem.cs b/Content.Client/Anomaly/AnomalySystem.cs
index 4eee43fac6..b4bc6efdd2 100644
--- a/Content.Client/Anomaly/AnomalySystem.cs
+++ b/Content.Client/Anomaly/AnomalySystem.cs
@@ -7,7 +7,7 @@ using Robust.Shared.Timing;
namespace Content.Client.Anomaly;
-public sealed class AnomalySystem : SharedAnomalySystem
+public sealed partial class AnomalySystem : SharedAnomalySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly FloatingVisualizerSystem _floating = default!;
@@ -24,6 +24,7 @@ public sealed class AnomalySystem : SharedAnomalySystem
SubscribeLocalEvent(OnShutdown);
}
+
private void OnStartup(EntityUid uid, AnomalyComponent component, ComponentStartup args)
{
_floating.FloatAnimation(uid, component.FloatingOffset, component.AnimationKey, component.AnimationTime);
diff --git a/Content.Client/Atmos/AlignAtmosPipeLayers.cs b/Content.Client/Atmos/AlignAtmosPipeLayers.cs
index 1bf3310a6c..51a6ce0c02 100644
--- a/Content.Client/Atmos/AlignAtmosPipeLayers.cs
+++ b/Content.Client/Atmos/AlignAtmosPipeLayers.cs
@@ -134,7 +134,7 @@ public sealed class AlignAtmosPipeLayers : SnapgridCenter
var newProtoId = altPrototypes[(int)layer];
- if (!_protoManager.TryIndex(newProtoId, out var newProto))
+ if (!_protoManager.Resolve(newProtoId, out var newProto))
return;
if (newProto.Type != ConstructionType.Structure)
diff --git a/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs b/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs
index ad26436946..d7894265c8 100644
--- a/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs
+++ b/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs
@@ -19,6 +19,7 @@ namespace Content.Client.Atmos.EntitySystems
[Dependency] private readonly SharedTransformSystem _xformSys = default!;
private GasTileOverlay _overlay = default!;
+ private GasTileHeatOverlay _heatOverlay = default!;
public override void Initialize()
{
@@ -28,12 +29,16 @@ namespace Content.Client.Atmos.EntitySystems
_overlay = new GasTileOverlay(this, EntityManager, _resourceCache, ProtoMan, _spriteSys, _xformSys);
_overlayMan.AddOverlay(_overlay);
+
+ _heatOverlay = new GasTileHeatOverlay();
+ _overlayMan.AddOverlay(_heatOverlay);
}
public override void Shutdown()
{
base.Shutdown();
_overlayMan.RemoveOverlay();
+ _overlayMan.RemoveOverlay();
}
private void OnHandleState(EntityUid gridUid, GasTileOverlayComponent comp, ref ComponentHandleState args)
diff --git a/Content.Client/Atmos/Overlays/GasTileHeatOverlay.cs b/Content.Client/Atmos/Overlays/GasTileHeatOverlay.cs
new file mode 100644
index 0000000000..36f0a065c1
--- /dev/null
+++ b/Content.Client/Atmos/Overlays/GasTileHeatOverlay.cs
@@ -0,0 +1,210 @@
+using System.Numerics;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.Components;
+using Content.Client.Atmos.EntitySystems;
+using Content.Shared.CCVar;
+using Robust.Client.Graphics;
+using Robust.Shared.Configuration;
+using Robust.Shared.Enums;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Atmos.Overlays;
+
+public sealed class GasTileHeatOverlay : Overlay
+{
+ public override bool RequestScreenTexture { get; set; } = true;
+ private static readonly ProtoId UnshadedShader = "unshaded";
+ private static readonly ProtoId HeatOverlayShader = "Heat";
+
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ [Dependency] private readonly IMapManager _mapManager = default!;
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+ [Dependency] private readonly IClyde _clyde = default!;
+ [Dependency] private readonly IConfigurationManager _configManager = default!;
+ // We can't resolve this immediately, because it's an entitysystem, but we will attempt to resolve and cache this
+ // once we begin to draw.
+ private GasTileOverlaySystem? _gasTileOverlay;
+ private readonly SharedTransformSystem _xformSys;
+
+ private IRenderTexture? _heatTarget;
+ private IRenderTexture? _heatBlurTarget;
+
+ public override OverlaySpace Space => OverlaySpace.WorldSpace;
+ private readonly ShaderInstance _shader;
+
+ public GasTileHeatOverlay()
+ {
+ IoCManager.InjectDependencies(this);
+ _xformSys = _entManager.System();
+
+ _shader = _proto.Index(HeatOverlayShader).InstanceUnique();
+
+ _configManager.OnValueChanged(CCVars.ReducedMotion, SetReducedMotion, invokeImmediately: true);
+
+ }
+
+ private void SetReducedMotion(bool reducedMotion)
+ {
+ _shader.SetParameter("strength_scale", reducedMotion ? 0.5f : 1f);
+ _shader.SetParameter("speed_scale", reducedMotion ? 0.25f : 1f);
+ }
+
+ protected override bool BeforeDraw(in OverlayDrawArgs args)
+ {
+ if (args.MapId == MapId.Nullspace)
+ return false;
+
+ // If we haven't resolved this yet, give it a try or bail
+ _gasTileOverlay ??= _entManager.System();
+
+ if (_gasTileOverlay == null)
+ return false;
+
+ var target = args.Viewport.RenderTarget;
+
+ // Probably the resolution of the game window changed, remake the textures.
+ if (_heatTarget?.Texture.Size != target.Size)
+ {
+ _heatTarget?.Dispose();
+ _heatTarget = _clyde.CreateRenderTarget(
+ target.Size,
+ new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
+ name: nameof(GasTileHeatOverlay));
+ }
+ if (_heatBlurTarget?.Texture.Size != target.Size)
+ {
+ _heatBlurTarget?.Dispose();
+ _heatBlurTarget = _clyde.CreateRenderTarget(
+ target.Size,
+ new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
+ name: $"{nameof(GasTileHeatOverlay)}-blur");
+ }
+
+ var overlayQuery = _entManager.GetEntityQuery();
+
+ args.WorldHandle.UseShader(_proto.Index(UnshadedShader).Instance());
+
+ var mapId = args.MapId;
+ var worldAABB = args.WorldAABB;
+ var worldBounds = args.WorldBounds;
+ var worldHandle = args.WorldHandle;
+ var worldToViewportLocal = args.Viewport.GetWorldToLocalMatrix();
+
+ // If there is no distortion after checking all visible tiles, we can bail early
+ var anyDistortion = false;
+
+ // We're rendering in the context of the heat target texture, which will encode data as to where and how strong
+ // the heat distortion will be
+ args.WorldHandle.RenderInRenderTarget(_heatTarget,
+ () =>
+ {
+ List> grids = new();
+ _mapManager.FindGridsIntersecting(mapId, worldAABB, ref grids);
+ foreach (var grid in grids)
+ {
+ if (!overlayQuery.TryGetComponent(grid.Owner, out var comp))
+ continue;
+
+ var gridEntToWorld = _xformSys.GetWorldMatrix(grid.Owner);
+ var gridEntToViewportLocal = gridEntToWorld * worldToViewportLocal;
+
+ if (!Matrix3x2.Invert(gridEntToViewportLocal, out var viewportLocalToGridEnt))
+ continue;
+
+ var uvToUi = Matrix3Helpers.CreateScale(_heatTarget.Size.X, -_heatTarget.Size.Y);
+ var uvToGridEnt = uvToUi * viewportLocalToGridEnt;
+
+ // Because we want the actual distortion to be calculated based on the grid coordinates*, we need
+ // to pass a matrix transformation to go from the viewport coordinates to grid coordinates.
+ // * (why? because otherwise the effect would shimmer like crazy as you moved around, think
+ // moving a piece of warped glass above a picture instead of placing the warped glass on the
+ // paper and moving them together)
+ _shader.SetParameter("grid_ent_from_viewport_local", uvToGridEnt);
+
+ // Draw commands (like DrawRect) will be using grid coordinates from here
+ worldHandle.SetTransform(gridEntToViewportLocal);
+
+ // We only care about tiles that fit in these bounds
+ var floatBounds = worldToViewportLocal.TransformBox(worldBounds).Enlarged(grid.Comp.TileSize);
+ var localBounds = new Box2i(
+ (int)MathF.Floor(floatBounds.Left),
+ (int)MathF.Floor(floatBounds.Bottom),
+ (int)MathF.Ceiling(floatBounds.Right),
+ (int)MathF.Ceiling(floatBounds.Top));
+
+ // for each tile and its gas --->
+ foreach (var chunk in comp.Chunks.Values)
+ {
+ var enumerator = new GasChunkEnumerator(chunk);
+
+ while (enumerator.MoveNext(out var tileGas))
+ {
+ // --->
+ // Check and make sure the tile is within the viewport/screen
+ var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y);
+ if (!localBounds.Contains(tilePosition))
+ continue;
+
+ // Get the distortion strength from the temperature and bail if it's not hot enough
+ var strength = _gasTileOverlay.GetHeatDistortionStrength(tileGas.Temperature);
+ if (strength <= 0f)
+ continue;
+
+ anyDistortion = true;
+ // Encode the strength in the red channel, then 1.0 alpha if it's an active tile.
+ // BlurRenderTarget will then apply a blur around the edge, but we don't want it to bleed
+ // past the tile.
+ // So we use this alpha channel to chop the lower alpha values off in the shader to fit a
+ // fit mask back into the tile.
+ worldHandle.DrawRect(
+ Box2.CenteredAround(tilePosition + new Vector2(0.5f, 0.5f), grid.Comp.TileSizeVector),
+ new Color(strength,0f, 0f, strength > 0f ? 1.0f : 0f));
+ }
+ }
+ }
+ },
+ // This clears the buffer to all zero first...
+ new Color(0, 0, 0, 0));
+
+ // no distortion, no need to render
+ if (!anyDistortion)
+ {
+ // Return the draw handle to normal settings
+ args.WorldHandle.UseShader(null);
+ args.WorldHandle.SetTransform(Matrix3x2.Identity);
+ return false;
+ }
+
+ // Clear to draw
+ return true;
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ if (ScreenTexture is null || _heatTarget is null || _heatBlurTarget is null)
+ return;
+
+ // Blur to soften the edges of the distortion. the lower parts of the alpha channel need to get cut off in the
+ // distortion shader to keep them in tile bounds.
+ _clyde.BlurRenderTarget(args.Viewport, _heatTarget, _heatBlurTarget, args.Viewport.Eye!, 14f);
+
+ // Set up and render the distortion
+ _shader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
+ args.WorldHandle.UseShader(_shader);
+ args.WorldHandle.DrawTextureRect(_heatTarget.Texture, args.WorldBounds);
+
+ // Return the draw handle to normal settings
+ args.WorldHandle.UseShader(null);
+ args.WorldHandle.SetTransform(Matrix3x2.Identity);
+ }
+
+ protected override void DisposeBehavior()
+ {
+ _heatTarget = null;
+ _heatBlurTarget = null;
+ _configManager.UnsubValueChanged(CCVars.ReducedMotion, SetReducedMotion);
+ base.DisposeBehavior();
+ }
+}
diff --git a/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs b/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs
index e280523e43..63b4e6b0c6 100644
--- a/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs
+++ b/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs
@@ -208,7 +208,7 @@ namespace Content.Client.Atmos.UI
});
presBox.AddChild(new Label
{
- Text = Loc.GetString("gas-analyzer-window-pressure-val-text", ("pressure", $"{gasMix.Pressure:0.##}")),
+ Text = Loc.GetString("gas-analyzer-window-pressure-val-text", ("pressure", $"{gasMix.Pressure:0.00}")),
Align = Label.AlignMode.Right,
HorizontalExpand = true
});
@@ -232,8 +232,8 @@ namespace Content.Client.Atmos.UI
tempBox.AddChild(new Label
{
Text = Loc.GetString("gas-analyzer-window-temperature-val-text",
- ("tempK", $"{gasMix.Temperature:0.#}"),
- ("tempC", $"{TemperatureHelpers.KelvinToCelsius(gasMix.Temperature):0.#}")),
+ ("tempK", $"{gasMix.Temperature:0.0}"),
+ ("tempC", $"{TemperatureHelpers.KelvinToCelsius(gasMix.Temperature):0.0}")),
Align = Label.AlignMode.Right,
HorizontalExpand = true
});
diff --git a/Content.Client/Audio/Jukebox/JukeboxBoundUserInterface.cs b/Content.Client/Audio/Jukebox/JukeboxBoundUserInterface.cs
index 865dfc478d..510b9d3def 100644
--- a/Content.Client/Audio/Jukebox/JukeboxBoundUserInterface.cs
+++ b/Content.Client/Audio/Jukebox/JukeboxBoundUserInterface.cs
@@ -58,7 +58,7 @@ public sealed class JukeboxBoundUserInterface : BoundUserInterface
_menu.SetAudioStream(jukebox.AudioStream);
- if (_protoManager.TryIndex(jukebox.SelectedSongId, out var songProto))
+ if (_protoManager.Resolve(jukebox.SelectedSongId, out var songProto))
{
var length = EntMan.System().GetAudioLength(songProto.Path.Path.ToString());
_menu.SetSelectedSong(songProto.Name, (float) length.TotalSeconds);
diff --git a/Content.Client/BarSign/BarSignSystem.cs b/Content.Client/BarSign/BarSignSystem.cs
index 02e33861b7..1ea99864a1 100644
--- a/Content.Client/BarSign/BarSignSystem.cs
+++ b/Content.Client/BarSign/BarSignSystem.cs
@@ -39,7 +39,7 @@ public sealed class BarSignSystem : VisualizerSystem
if (powered
&& sign.Current != null
- && _prototypeManager.TryIndex(sign.Current, out var proto))
+ && _prototypeManager.Resolve(sign.Current, out var proto))
{
SpriteSystem.LayerSetSprite((id, sprite), 0, proto.Icon);
sprite.LayerSetShader(0, "unshaded");
diff --git a/Content.Client/BarSign/Ui/BarSignBoundUserInterface.cs b/Content.Client/BarSign/Ui/BarSignBoundUserInterface.cs
index 1d1280b2f3..fe07f0f1d1 100644
--- a/Content.Client/BarSign/Ui/BarSignBoundUserInterface.cs
+++ b/Content.Client/BarSign/Ui/BarSignBoundUserInterface.cs
@@ -35,7 +35,7 @@ public sealed class BarSignBoundUserInterface(EntityUid owner, Enum uiKey) : Bou
public void Update(ProtoId? sign)
{
- if (_prototype.TryIndex(sign, out var signPrototype))
+ if (_prototype.Resolve(sign, out var signPrototype))
_menu?.UpdateState(signPrototype);
}
diff --git a/Content.Client/Cargo/UI/BountyEntry.xaml.cs b/Content.Client/Cargo/UI/BountyEntry.xaml.cs
index 027d7b3e80..d813f70ff4 100644
--- a/Content.Client/Cargo/UI/BountyEntry.xaml.cs
+++ b/Content.Client/Cargo/UI/BountyEntry.xaml.cs
@@ -29,7 +29,7 @@ public sealed partial class BountyEntry : BoxContainer
UntilNextSkip = untilNextSkip;
- if (!_prototype.TryIndex(bounty.Bounty, out var bountyPrototype))
+ if (!_prototype.Resolve(bounty.Bounty, out var bountyPrototype))
return;
var items = new List();
diff --git a/Content.Client/Cargo/UI/BountyHistoryEntry.xaml.cs b/Content.Client/Cargo/UI/BountyHistoryEntry.xaml.cs
index 54804be641..98658e5f0a 100644
--- a/Content.Client/Cargo/UI/BountyHistoryEntry.xaml.cs
+++ b/Content.Client/Cargo/UI/BountyHistoryEntry.xaml.cs
@@ -19,7 +19,7 @@ public sealed partial class BountyHistoryEntry : BoxContainer
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
- if (!_prototype.TryIndex(bounty.Bounty, out var bountyPrototype))
+ if (!_prototype.Resolve(bounty.Bounty, out var bountyPrototype))
return;
var items = new List();
diff --git a/Content.Client/Changeling/UI/ChangelingTransformBoundUserInterface.cs b/Content.Client/Changeling/UI/ChangelingTransformBoundUserInterface.cs
index 8220e18708..97c07dd8c9 100644
--- a/Content.Client/Changeling/UI/ChangelingTransformBoundUserInterface.cs
+++ b/Content.Client/Changeling/UI/ChangelingTransformBoundUserInterface.cs
@@ -1,4 +1,7 @@
-using Content.Shared.Changeling.Systems;
+using Content.Client.Stylesheets;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Changeling.Components;
+using Content.Shared.Changeling.Systems;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
@@ -7,28 +10,58 @@ namespace Content.Client.Changeling.UI;
[UsedImplicitly]
public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
- private ChangelingTransformMenu? _window;
+ private SimpleRadialMenu? _menu;
+ private static readonly Color SelectedOptionBackground = StyleNano.ButtonColorGoodDefault.WithAlpha(128);
+ private static readonly Color SelectedOptionHoverBackground = StyleNano.ButtonColorGoodHovered.WithAlpha(128);
protected override void Open()
{
base.Open();
- _window = this.CreateWindow();
-
- _window.OnIdentitySelect += SendIdentitySelect;
-
- _window.Update(Owner);
+ _menu = this.CreateWindow();
+ Update();
+ _menu.OpenOverMouseScreenPosition();
}
+
public override void Update()
{
- if (_window == null)
+ if (_menu == null)
return;
- _window.Update(Owner);
+ if (!EntMan.TryGetComponent(Owner, out var lingIdentity))
+ return;
+
+ var models = ConvertToButtons(lingIdentity.ConsumedIdentities, lingIdentity?.CurrentIdentity);
+
+ _menu.SetButtons(models);
}
- public void SendIdentitySelect(NetEntity identityId)
+ private IEnumerable ConvertToButtons(
+ IEnumerable identities,
+ EntityUid? currentIdentity
+ )
+ {
+ var buttons = new List();
+ foreach (var identity in identities)
+ {
+ if (!EntMan.TryGetComponent(identity, out var metadata))
+ continue;
+
+ var option = new RadialMenuActionOption(SendIdentitySelect, EntMan.GetNetEntity(identity))
+ {
+ IconSpecifier = RadialMenuIconSpecifier.With(identity),
+ ToolTip = metadata.EntityName,
+ BackgroundColor = (currentIdentity == identity) ? SelectedOptionBackground : null,
+ HoverBackgroundColor = (currentIdentity == identity) ? SelectedOptionHoverBackground : null
+ };
+ buttons.Add(option);
+ }
+
+ return buttons;
+ }
+
+ private void SendIdentitySelect(NetEntity identityId)
{
SendPredictedMessage(new ChangelingTransformIdentitySelectMessage(identityId));
}
diff --git a/Content.Client/Changeling/UI/ChangelingTransformMenu.xaml b/Content.Client/Changeling/UI/ChangelingTransformMenu.xaml
deleted file mode 100644
index 38ae0ec715..0000000000
--- a/Content.Client/Changeling/UI/ChangelingTransformMenu.xaml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
diff --git a/Content.Client/Changeling/UI/ChangelingTransformMenu.xaml.cs b/Content.Client/Changeling/UI/ChangelingTransformMenu.xaml.cs
deleted file mode 100644
index ebd4e90440..0000000000
--- a/Content.Client/Changeling/UI/ChangelingTransformMenu.xaml.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-using System.Numerics;
-using Content.Client.UserInterface.Controls;
-using Content.Shared.Changeling.Components;
-using Robust.Client.AutoGenerated;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.XAML;
-
-namespace Content.Client.Changeling.UI;
-
-[GenerateTypedNameReferences]
-public sealed partial class ChangelingTransformMenu : RadialMenu
-{
- [Dependency] private readonly IEntityManager _entity = default!;
- public event Action? OnIdentitySelect;
-
- public ChangelingTransformMenu()
- {
- RobustXamlLoader.Load(this);
- IoCManager.InjectDependencies(this);
- }
-
- public void Update(EntityUid uid)
- {
- Main.DisposeAllChildren();
-
- if (!_entity.TryGetComponent(uid, out var identityComp))
- return;
-
- foreach (var identityUid in identityComp.ConsumedIdentities)
- {
- if (!_entity.TryGetComponent(identityUid, out var metadata))
- continue;
-
- var identityName = metadata.EntityName;
-
- var button = new ChangelingTransformMenuButton()
- {
- StyleClasses = { "RadialMenuButton" },
- SetSize = new Vector2(64, 64),
- ToolTip = identityName,
- };
-
- var entView = new SpriteView()
- {
- SetSize = new Vector2(48, 48),
- VerticalAlignment = VAlignment.Center,
- HorizontalAlignment = HAlignment.Center,
- Stretch = SpriteView.StretchMode.Fill,
- };
- entView.SetEntity(identityUid);
- button.OnButtonUp += _ =>
- {
- OnIdentitySelect?.Invoke(_entity.GetNetEntity(identityUid));
- Close();
- };
- button.AddChild(entView);
- Main.AddChild(button);
- }
- }
-}
-
-public sealed class ChangelingTransformMenuButton : RadialMenuTextureButtonWithSector;
diff --git a/Content.Client/Chat/TypingIndicator/TypingIndicatorVisualizerSystem.cs b/Content.Client/Chat/TypingIndicator/TypingIndicatorVisualizerSystem.cs
index c4b0c8f282..e1197d4ac4 100644
--- a/Content.Client/Chat/TypingIndicator/TypingIndicatorVisualizerSystem.cs
+++ b/Content.Client/Chat/TypingIndicator/TypingIndicatorVisualizerSystem.cs
@@ -27,7 +27,7 @@ public sealed class TypingIndicatorVisualizerSystem : VisualizerSystem(ent => new InjectorStatusControl(ent, SolutionContainers));
+
+ Subs.ItemStatus(ent => new InjectorStatusControl(ent, SolutionContainer));
}
}
diff --git a/Content.Client/Chemistry/UI/InjectorStatusControl.cs b/Content.Client/Chemistry/UI/InjectorStatusControl.cs
index f9b0d90e20..0358876b76 100644
--- a/Content.Client/Chemistry/UI/InjectorStatusControl.cs
+++ b/Content.Client/Chemistry/UI/InjectorStatusControl.cs
@@ -38,13 +38,13 @@ public sealed class InjectorStatusControl : Control
// only updates the UI if any of the details are different than they previously were
if (PrevVolume == solution.Volume
&& PrevMaxVolume == solution.MaxVolume
- && PrevTransferAmount == _parent.Comp.TransferAmount
+ && PrevTransferAmount == _parent.Comp.CurrentTransferAmount
&& PrevToggleState == _parent.Comp.ToggleState)
return;
PrevVolume = solution.Volume;
PrevMaxVolume = solution.MaxVolume;
- PrevTransferAmount = _parent.Comp.TransferAmount;
+ PrevTransferAmount = _parent.Comp.CurrentTransferAmount;
PrevToggleState = _parent.Comp.ToggleState;
// Update current volume and injector state
@@ -59,6 +59,6 @@ public sealed class InjectorStatusControl : Control
("currentVolume", solution.Volume),
("totalVolume", solution.MaxVolume),
("modeString", modeStringLocalized),
- ("transferVolume", _parent.Comp.TransferAmount)));
+ ("transferVolume", _parent.Comp.CurrentTransferAmount)));
}
}
diff --git a/Content.Client/Clothing/ClientClothingSystem.cs b/Content.Client/Clothing/ClientClothingSystem.cs
index 8d53e90e34..417e540d4a 100644
--- a/Content.Client/Clothing/ClientClothingSystem.cs
+++ b/Content.Client/Clothing/ClientClothingSystem.cs
@@ -1,12 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
-using System.Numerics;
using Content.Client.DisplacementMap;
using Content.Client.Inventory;
using Content.Shared.Clothing;
using Content.Shared.Clothing.Components;
using Content.Shared.Clothing.EntitySystems;
-using Content.Shared.DisplacementMap;
using Content.Shared.Humanoid;
using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;
@@ -14,7 +12,6 @@ using Content.Shared.Item;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
-using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
using Robust.Shared.Utility;
using static Robust.Client.GameObjects.SpriteComponent;
@@ -177,6 +174,7 @@ public sealed class ClientClothingSystem : ClothingSystem
var layer = new PrototypeLayerData();
layer.RsiPath = rsi.Path.ToString();
layer.State = state;
+ layer.Scale = clothing.Scale;
layers = new() { layer };
return true;
diff --git a/Content.Client/Clothing/UI/ChameleonBoundUserInterface.cs b/Content.Client/Clothing/UI/ChameleonBoundUserInterface.cs
index 876f300e50..6595426d48 100644
--- a/Content.Client/Clothing/UI/ChameleonBoundUserInterface.cs
+++ b/Content.Client/Clothing/UI/ChameleonBoundUserInterface.cs
@@ -45,7 +45,7 @@ public sealed class ChameleonBoundUserInterface : BoundUserInterface
var newTargets = new List();
foreach (var target in targets)
{
- if (string.IsNullOrEmpty(target) || !_proto.TryIndex(target, out EntityPrototype? proto))
+ if (string.IsNullOrEmpty(target) || !_proto.Resolve(target, out EntityPrototype? proto))
continue;
if (!proto.TryGetComponent(out TagComponent? tag, EntMan.ComponentFactory) || !_tag.HasTag(tag, st.RequiredTag))
diff --git a/Content.Client/Clothing/UI/ChameleonMenu.xaml.cs b/Content.Client/Clothing/UI/ChameleonMenu.xaml.cs
index c6dce10776..fb4447bdf9 100644
--- a/Content.Client/Clothing/UI/ChameleonMenu.xaml.cs
+++ b/Content.Client/Clothing/UI/ChameleonMenu.xaml.cs
@@ -54,7 +54,7 @@ public sealed partial class ChameleonMenu : DefaultWindow
foreach (var id in _possibleIds)
{
- if (!_prototypeManager.TryIndex(id, out EntityPrototype? proto))
+ if (!_prototypeManager.Resolve(id, out EntityPrototype? proto))
continue;
var lowId = id.Id.ToLowerInvariant();
diff --git a/Content.Client/Construction/ConstructionSystem.cs b/Content.Client/Construction/ConstructionSystem.cs
index 0e7557724f..d693f4ac47 100644
--- a/Content.Client/Construction/ConstructionSystem.cs
+++ b/Content.Client/Construction/ConstructionSystem.cs
@@ -80,7 +80,7 @@ namespace Content.Client.Construction
{
foreach (var constructionProto in PrototypeManager.EnumeratePrototypes())
{
- if (!PrototypeManager.TryIndex(constructionProto.Graph, out var graphProto))
+ if (!PrototypeManager.Resolve(constructionProto.Graph, out var graphProto))
continue;
if (constructionProto.TargetNode is not { } targetNodeId)
@@ -121,17 +121,14 @@ namespace Content.Client.Construction
// If we got the id of the prototype, we exit the “recursion” by clearing the stack.
stack.Clear();
- if (!PrototypeManager.TryIndex(constructionProto.ID, out ConstructionPrototype? recipe))
+ if (!PrototypeManager.Resolve(entityId, out var proto))
continue;
- if (!PrototypeManager.TryIndex(entityId, out var proto))
- continue;
+ var name = constructionProto.SetName.HasValue ? Loc.GetString(constructionProto.SetName) : proto.Name;
+ var desc = constructionProto.SetDescription.HasValue ? Loc.GetString(constructionProto.SetDescription) : proto.Description;
- var name = recipe.SetName.HasValue ? Loc.GetString(recipe.SetName) : proto.Name;
- var desc = recipe.SetDescription.HasValue ? Loc.GetString(recipe.SetDescription) : proto.Description;
-
- recipe.Name = name;
- recipe.Description = desc;
+ constructionProto.Name = name;
+ constructionProto.Description = desc;
_recipesMetadataCache.Add(constructionProto.ID, entityId);
} while (stack.Count > 0);
@@ -172,7 +169,7 @@ namespace Content.Client.Construction
"construction-ghost-examine-message",
("name", component.Prototype.Name)));
- if (!PrototypeManager.TryIndex(component.Prototype.Graph, out var graph))
+ if (!PrototypeManager.Resolve(component.Prototype.Graph, out var graph))
return;
var startNode = graph.Nodes[component.Prototype.StartNode];
diff --git a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs
index 119e92fc6f..d5fee2bdda 100644
--- a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs
+++ b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs
@@ -510,7 +510,7 @@ namespace Content.Client.Construction.UI
foreach (var id in favorites)
{
- if (_prototypeManager.TryIndex(id, out ConstructionPrototype? recipe, logError: false))
+ if (_prototypeManager.TryIndex(id, out ConstructionPrototype? recipe))
_favoritedRecipes.Add(recipe);
}
diff --git a/Content.Client/Damage/DamageVisualsSystem.cs b/Content.Client/Damage/DamageVisualsSystem.cs
index de866ca9a4..065bf628bc 100644
--- a/Content.Client/Damage/DamageVisualsSystem.cs
+++ b/Content.Client/Damage/DamageVisualsSystem.cs
@@ -150,7 +150,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem(damageComponent.DamageContainerID, out var damageContainer))
+ && _prototypeManager.Resolve(damageComponent.DamageContainerID, out var damageContainer))
{
// Are we using damage overlay sprites by group?
// Check if the container matches the supported groups,
diff --git a/Content.Client/DisplacementMap/DisplacementMapSystem.cs b/Content.Client/DisplacementMap/DisplacementMapSystem.cs
index 94dbc7f00c..6986e1c868 100644
--- a/Content.Client/DisplacementMap/DisplacementMapSystem.cs
+++ b/Content.Client/DisplacementMap/DisplacementMapSystem.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using Content.Shared.DisplacementMap;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
@@ -10,6 +11,11 @@ public sealed class DisplacementMapSystem : EntitySystem
[Dependency] private readonly ISerializationManager _serialization = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
+ private static string? BuildDisplacementLayerKey(object key)
+ {
+ return key.ToString() is null ? null : $"{key}-displacement";
+ }
+
///
/// Attempting to apply a displacement map to a specific layer of SpriteComponent
///
@@ -19,21 +25,22 @@ public sealed class DisplacementMapSystem : EntitySystem
/// Unique layer key, which will determine which layer to apply displacement map to
/// The key of the new displacement map layer added by this function.
///
- public bool TryAddDisplacement(DisplacementData data,
+ public bool TryAddDisplacement(
+ DisplacementData data,
Entity sprite,
int index,
object key,
- out string displacementKey)
+ [NotNullWhen(true)] out string? displacementKey
+ )
{
- displacementKey = $"{key}-displacement";
-
- if (key.ToString() is null)
+ displacementKey = BuildDisplacementLayerKey(key);
+ if (displacementKey is null)
return false;
- if (data.ShaderOverride != null)
- sprite.Comp.LayerSetShader(index, data.ShaderOverride);
+ EnsureDisplacementIsNotOnSprite(sprite, key);
- _sprite.RemoveLayer(sprite.AsNullable(), displacementKey, false);
+ if (data.ShaderOverride is not null)
+ sprite.Comp.LayerSetShader(index, data.ShaderOverride);
//allows you not to write it every time in the YML
foreach (var pair in data.SizeMaps)
@@ -70,7 +77,11 @@ public sealed class DisplacementMapSystem : EntitySystem
}
var displacementLayer = _serialization.CreateCopy(displacementDataLayer, notNullableOverride: true);
- displacementLayer.CopyToShaderParameters!.LayerKey = key.ToString() ?? "this is impossible";
+
+ // This previously assigned a string reading "this is impossible" if key.ToString eval'd to false.
+ // However, for the sake of sanity, we've changed this to assert non-null - !.
+ // If this throws an error, we're not sorry. Nanotrasen thanks you for your service fixing this bug.
+ displacementLayer.CopyToShaderParameters!.LayerKey = key.ToString()!;
_sprite.AddLayer(sprite.AsNullable(), displacementLayer, index);
_sprite.LayerMapSet(sprite.AsNullable(), displacementKey, index);
@@ -78,14 +89,18 @@ public sealed class DisplacementMapSystem : EntitySystem
return true;
}
- ///
- [Obsolete("Use the Entity overload")]
- public bool TryAddDisplacement(DisplacementData data,
- SpriteComponent sprite,
- int index,
- object key,
- out string displacementKey)
+ ///
+ /// Ensures that the displacement map associated with the given layer key is not in the Sprite's LayerMap.
+ ///
+ /// The sprite to remove the displacement layer from.
+ /// The key of the layer that is referenced by the displacement layer we want to remove.
+ /// Whether to report an error if the displacement map isn't on the sprite.
+ public void EnsureDisplacementIsNotOnSprite(Entity sprite, object key)
{
- return TryAddDisplacement(data, (sprite.Owner, sprite), index, key, out displacementKey);
+ var displacementLayerKey = BuildDisplacementLayerKey(key);
+ if (displacementLayerKey is null)
+ return;
+
+ _sprite.RemoveLayer(sprite.AsNullable(), displacementLayerKey, false);
}
}
diff --git a/Content.Client/Doors/DoorSystem.cs b/Content.Client/Doors/DoorSystem.cs
index 3d9a3e2a9a..f1697b820d 100644
--- a/Content.Client/Doors/DoorSystem.cs
+++ b/Content.Client/Doors/DoorSystem.cs
@@ -142,7 +142,7 @@ public sealed class DoorSystem : SharedDoorSystem
private void UpdateSpriteLayers(Entity sprite, string targetProto)
{
- if (!_prototypeManager.TryIndex(targetProto, out var target))
+ if (!_prototypeManager.Resolve(targetProto, out var target))
return;
if (!target.TryGetComponent(out SpriteComponent? targetSprite, _componentFactory))
diff --git a/Content.Client/Ghost/GhostRoleRadioBoundUserInterface.cs b/Content.Client/Ghost/GhostRoleRadioBoundUserInterface.cs
index 52ea835f4a..9334c85536 100644
--- a/Content.Client/Ghost/GhostRoleRadioBoundUserInterface.cs
+++ b/Content.Client/Ghost/GhostRoleRadioBoundUserInterface.cs
@@ -1,25 +1,58 @@
+using Content.Client.UserInterface.Controls;
using Content.Shared.Ghost.Roles;
+using Content.Shared.Ghost.Roles.Components;
using Robust.Client.UserInterface;
using Robust.Shared.Prototypes;
namespace Content.Client.Ghost;
-public sealed class GhostRoleRadioBoundUserInterface : BoundUserInterface
+public sealed class GhostRoleRadioBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
- private GhostRoleRadioMenu? _ghostRoleRadioMenu;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
- public GhostRoleRadioBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
- {
- IoCManager.InjectDependencies(this);
- }
+ private SimpleRadialMenu? _ghostRoleRadioMenu;
protected override void Open()
{
base.Open();
- _ghostRoleRadioMenu = this.CreateWindow();
- _ghostRoleRadioMenu.SetEntity(Owner);
- _ghostRoleRadioMenu.SendGhostRoleRadioMessageAction += SendGhostRoleRadioMessage;
+ _ghostRoleRadioMenu = this.CreateWindow();
+
+ // The purpose of this radial UI is for ghost role radios that allow you to select
+ // more than one potential option, such as with kobolds/lizards.
+ // This means that it won't show anything if SelectablePrototypes is empty.
+ if (!EntMan.TryGetComponent(Owner, out var comp))
+ return;
+
+ var list = ConvertToButtons(comp.SelectablePrototypes);
+
+ _ghostRoleRadioMenu.SetButtons(list);
+ }
+
+ private IEnumerable ConvertToButtons(List> protoIds)
+ {
+ var list = new List();
+ foreach (var ghostRoleProtoId in protoIds)
+ {
+ // For each prototype we find we want to create a button that uses the name of the ghost role
+ // as the hover tooltip, and the icon is taken from either the ghost role entityprototype
+ // or the indicated icon entityprototype.
+ if (!_prototypeManager.Resolve(ghostRoleProtoId, out var ghostRoleProto))
+ continue;
+
+ var option = new RadialMenuActionOption>(SendGhostRoleRadioMessage, ghostRoleProtoId)
+ {
+ ToolTip = Loc.GetString(ghostRoleProto.Name),
+ // pick the icon if it exists, otherwise fallback to the ghost role's entity
+ IconSpecifier = ghostRoleProto.IconPrototype != null
+ && _prototypeManager.Resolve(ghostRoleProto.IconPrototype, out var iconProto)
+ ? RadialMenuIconSpecifier.With(iconProto)
+ : RadialMenuIconSpecifier.With(ghostRoleProto.EntityPrototype)
+ };
+ list.Add(option);
+ }
+
+ return list;
}
private void SendGhostRoleRadioMessage(ProtoId protoId)
diff --git a/Content.Client/Ghost/GhostRoleRadioMenu.xaml b/Content.Client/Ghost/GhostRoleRadioMenu.xaml
deleted file mode 100644
index c35ee128c5..0000000000
--- a/Content.Client/Ghost/GhostRoleRadioMenu.xaml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
diff --git a/Content.Client/Ghost/GhostRoleRadioMenu.xaml.cs b/Content.Client/Ghost/GhostRoleRadioMenu.xaml.cs
deleted file mode 100644
index 1b65eac6ed..0000000000
--- a/Content.Client/Ghost/GhostRoleRadioMenu.xaml.cs
+++ /dev/null
@@ -1,105 +0,0 @@
-using Content.Client.UserInterface.Controls;
-using Content.Shared.Ghost.Roles;
-using Content.Shared.Ghost.Roles.Components;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Prototypes;
-using System.Numerics;
-
-namespace Content.Client.Ghost;
-
-public sealed partial class GhostRoleRadioMenu : RadialMenu
-{
- [Dependency] private readonly EntityManager _entityManager = default!;
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
-
- public event Action>? SendGhostRoleRadioMessageAction;
-
- public EntityUid Entity { get; set; }
-
- public GhostRoleRadioMenu()
- {
- IoCManager.InjectDependencies(this);
- RobustXamlLoader.Load(this);
- }
-
- public void SetEntity(EntityUid uid)
- {
- Entity = uid;
- RefreshUI();
- }
-
- private void RefreshUI()
- {
- // The main control that will contain all the clickable options
- var main = FindControl("Main");
-
- // The purpose of this radial UI is for ghost role radios that allow you to select
- // more than one potential option, such as with kobolds/lizards.
- // This means that it won't show anything if SelectablePrototypes is empty.
- if (!_entityManager.TryGetComponent(Entity, out var comp))
- return;
-
- foreach (var ghostRoleProtoString in comp.SelectablePrototypes)
- {
- // For each prototype we find we want to create a button that uses the name of the ghost role
- // as the hover tooltip, and the icon is taken from either the ghost role entityprototype
- // or the indicated icon entityprototype.
- if (!_prototypeManager.TryIndex(ghostRoleProtoString, out var ghostRoleProto))
- continue;
-
- var button = new GhostRoleRadioMenuButton()
- {
- SetSize = new Vector2(64, 64),
- ToolTip = Loc.GetString(ghostRoleProto.Name),
- ProtoId = ghostRoleProto.ID,
- };
-
- var entProtoView = new EntityPrototypeView()
- {
- SetSize = new Vector2(48, 48),
- VerticalAlignment = VAlignment.Center,
- HorizontalAlignment = HAlignment.Center,
- Stretch = SpriteView.StretchMode.Fill
- };
-
- // pick the icon if it exists, otherwise fallback to the ghost role's entity
- if (_prototypeManager.TryIndex(ghostRoleProto.IconPrototype, out var iconProto))
- entProtoView.SetPrototype(iconProto);
- else
- entProtoView.SetPrototype(ghostRoleProto.EntityPrototype);
-
- button.AddChild(entProtoView);
- main.AddChild(button);
- AddGhostRoleRadioMenuButtonOnClickActions(main);
- }
- }
-
- private void AddGhostRoleRadioMenuButtonOnClickActions(Control control)
- {
- var mainControl = control as RadialContainer;
-
- if (mainControl == null)
- return;
-
- foreach (var child in mainControl.Children)
- {
- var castChild = child as GhostRoleRadioMenuButton;
-
- if (castChild == null)
- continue;
-
- castChild.OnButtonUp += _ =>
- {
- SendGhostRoleRadioMessageAction?.Invoke(castChild.ProtoId);
- Close();
- };
- }
- }
-}
-
-public sealed class GhostRoleRadioMenuButton : RadialMenuTextureButtonWithSector
-{
- public ProtoId ProtoId { get; set; }
-}
diff --git a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs
index 29569e40e6..dbfd36daea 100644
--- a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs
+++ b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs
@@ -5,14 +5,17 @@ using Content.Client.Guidebook.Richtext;
using Content.Client.Message;
using Content.Client.UserInterface.ControlExtensions;
using Content.Shared.Body.Prototypes;
+using Content.Shared.CCVar;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;
+using Content.Shared.Contraband;
using JetBrains.Annotations;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -27,8 +30,10 @@ public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISea
[Dependency] private readonly IEntitySystemManager _systemManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] private readonly IConfigurationManager _config = default!;
private readonly ChemistryGuideDataSystem _chemistryGuideData;
+ private readonly ContrabandSystem _contraband;
private readonly ISawmill _sawmill;
public IPrototype? RepresentedPrototype { get; private set; }
@@ -39,6 +44,7 @@ public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISea
IoCManager.InjectDependencies(this);
_sawmill = _logManager.GetSawmill("guidebook.reagent");
_chemistryGuideData = _systemManager.GetEntitySystem();
+ _contraband = _systemManager.GetEntitySystem();
MouseFilter = MouseFilterMode.Stop;
}
@@ -204,6 +210,25 @@ public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISea
description.PushNewline();
description.AddMarkupOrThrow(Loc.GetString("guidebook-reagent-physical-description",
("description", reagent.LocalizedPhysicalDescription)));
+
+ if (_config.GetCVar(CCVars.ContrabandExamine))
+ {
+ // Department-restricted text
+ if (reagent.AllowedJobs.Count > 0 || reagent.AllowedDepartments.Count > 0)
+ {
+ description.PushNewline();
+ description.AddMarkupPermissive(
+ _contraband.GenerateDepartmentExamineMessage(reagent.AllowedDepartments, reagent.AllowedJobs, ContrabandItemType.Reagent));
+ }
+ // Other contraband text
+ else if (reagent.ContrabandSeverity != null &&
+ _prototype.Resolve(reagent.ContrabandSeverity.Value, out var severity))
+ {
+ description.PushNewline();
+ description.AddMarkupPermissive(Loc.GetString(severity.ExamineText, ("type", ContrabandItemType.Reagent)));
+ }
+ }
+
ReagentDescription.SetMessage(description);
}
diff --git a/Content.Client/Guidebook/DocumentParsingManager.cs b/Content.Client/Guidebook/DocumentParsingManager.cs
index ecf11d4725..8bc1a834fc 100644
--- a/Content.Client/Guidebook/DocumentParsingManager.cs
+++ b/Content.Client/Guidebook/DocumentParsingManager.cs
@@ -53,7 +53,7 @@ public sealed partial class DocumentParsingManager
public bool TryAddMarkup(Control control, ProtoId entryId, bool log = true)
{
- if (!_prototype.TryIndex(entryId, out var entry))
+ if (!_prototype.Resolve(entryId, out var entry))
return false;
using var file = _resourceManager.ContentFileReadText(entry.Text);
diff --git a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs
index 6700cf2a18..54c2801e33 100644
--- a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs
+++ b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs
@@ -289,25 +289,26 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
private void RemoveMarking(Marking marking, Entity spriteEnt)
{
if (!_markingManager.TryGetMarking(marking, out var prototype))
- {
return;
- }
foreach (var sprite in prototype.Sprites)
{
if (sprite is not SpriteSpecifier.Rsi rsi)
- {
continue;
- }
var layerId = $"{marking.MarkingId}-{rsi.RsiState}";
if (!_sprite.LayerMapTryGet(spriteEnt.AsNullable(), layerId, out var index, false))
- {
continue;
- }
_sprite.LayerMapRemove(spriteEnt.AsNullable(), layerId);
_sprite.RemoveLayer(spriteEnt.AsNullable(), index);
+
+ // If this marking is one that can be displaced, we need to remove the displacement as well; otherwise
+ // altering a marking at runtime can lead to the renderer falling over.
+ // The Vulps must be shaved.
+ // (https://github.com/space-wizards/space-station-14/issues/40135).
+ if (prototype.CanBeDisplaced)
+ _displacement.EnsureDisplacementIsNotOnSprite(spriteEnt, layerId);
}
}
@@ -346,9 +347,7 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
var sprite = entity.Comp2;
if (!_sprite.LayerMapTryGet((entity.Owner, sprite), markingPrototype.BodyPart, out var targetLayer, false))
- {
return;
- }
visible &= !IsHidden(humanoid, markingPrototype.BodyPart);
visible &= humanoid.BaseLayers.TryGetValue(markingPrototype.BodyPart, out var setting)
@@ -359,9 +358,7 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
var markingSprite = markingPrototype.Sprites[j];
if (markingSprite is not SpriteSpecifier.Rsi rsi)
- {
- continue;
- }
+ return;
var layerId = $"{markingPrototype.ID}-{rsi.RsiState}";
@@ -375,26 +372,18 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
_sprite.LayerSetVisible((entity.Owner, sprite), layerId, visible);
if (!visible || setting == null) // this is kinda implied
- {
continue;
- }
// Okay so if the marking prototype is modified but we load old marking data this may no longer be valid
// and we need to check the index is correct.
// So if that happens just default to white?
if (colors != null && j < colors.Count)
- {
_sprite.LayerSetColor((entity.Owner, sprite), layerId, colors[j]);
- }
else
- {
_sprite.LayerSetColor((entity.Owner, sprite), layerId, Color.White);
- }
if (humanoid.MarkingsDisplacement.TryGetValue(markingPrototype.BodyPart, out var displacementData) && markingPrototype.CanBeDisplaced)
- {
_displacement.TryAddDisplacement(displacementData, (entity.Owner, sprite), targetLayer + j + 1, layerId, out _);
- }
}
}
diff --git a/Content.Client/Implants/ImplanterSystem.cs b/Content.Client/Implants/ImplanterSystem.cs
index 4ba4d015ca..a8c501daf1 100644
--- a/Content.Client/Implants/ImplanterSystem.cs
+++ b/Content.Client/Implants/ImplanterSystem.cs
@@ -28,7 +28,7 @@ public sealed class ImplanterSystem : SharedImplanterSystem
Dictionary implants = new();
foreach (var implant in component.DeimplantWhitelist)
{
- if (_proto.TryIndex(implant, out var proto))
+ if (_proto.Resolve(implant, out var proto))
implants.Add(proto.ID, proto.Name);
}
diff --git a/Content.Client/Implants/UI/ChameleonControllerMenu.xaml.cs b/Content.Client/Implants/UI/ChameleonControllerMenu.xaml.cs
index a41e2e9293..c12ddb9319 100644
--- a/Content.Client/Implants/UI/ChameleonControllerMenu.xaml.cs
+++ b/Content.Client/Implants/UI/ChameleonControllerMenu.xaml.cs
@@ -62,7 +62,7 @@ public sealed partial class ChameleonControllerMenu : FancyWindow
// Go through every outfit and add them to the correct department.
foreach (var outfit in _outfits)
{
- _prototypeManager.TryIndex(outfit.Job, out var jobProto);
+ _prototypeManager.Resolve(outfit.Job, out var jobProto);
var name = outfit.LoadoutName ?? outfit.Name ?? jobProto?.Name ?? "Prototype has no name or job.";
diff --git a/Content.Client/Implants/UI/ImplanterStatusControl.cs b/Content.Client/Implants/UI/ImplanterStatusControl.cs
index 569dd785d7..24445eeecf 100644
--- a/Content.Client/Implants/UI/ImplanterStatusControl.cs
+++ b/Content.Client/Implants/UI/ImplanterStatusControl.cs
@@ -49,7 +49,7 @@ public sealed class ImplanterStatusControl : Control
if (_parent.CurrentMode == ImplanterToggleMode.Draw)
{
string implantName = _parent.DeimplantChosen != null
- ? (_prototype.TryIndex(_parent.DeimplantChosen.Value, out EntityPrototype? implantProto) ? implantProto.Name : Loc.GetString("implanter-empty-text"))
+ ? (_prototype.Resolve(_parent.DeimplantChosen.Value, out EntityPrototype? implantProto) ? implantProto.Name : Loc.GetString("implanter-empty-text"))
: Loc.GetString("implanter-empty-text");
_label.SetMarkup(Loc.GetString("implanter-label-draw",
diff --git a/Content.Client/Lathe/UI/LatheMenu.xaml.cs b/Content.Client/Lathe/UI/LatheMenu.xaml.cs
index ce190464d2..f6688a63af 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;
@@ -96,7 +97,7 @@ public sealed partial class LatheMenu : DefaultWindow
var recipesToShow = new List();
foreach (var recipe in Recipes)
{
- if (!_prototypeManager.TryIndex(recipe, out var proto))
+ if (!_prototypeManager.Resolve(recipe, out var proto))
continue;
// Category filtering
@@ -128,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);
}
}
@@ -153,7 +183,7 @@ public sealed partial class LatheMenu : DefaultWindow
foreach (var (id, amount) in prototype.Materials)
{
- if (!_prototypeManager.TryIndex(id, out var proto))
+ if (!_prototypeManager.Resolve(id, out var proto))
continue;
var adjustedAmount = SharedLatheSystem.AdjustMaterial(amount, prototype.ApplyMaterialDiscount, multiplier);
@@ -238,9 +268,10 @@ public sealed partial class LatheMenu : DefaultWindow
///
public void PopulateQueueList(IReadOnlyCollection queue)
{
- QueueList.DisposeAllChildren();
+ // Get the existing list of queue controls
+ var oldChildCount = QueueList.ChildCount;
- var idx = 1;
+ var idx = 0;
foreach (var batch in queue)
{
var recipe = _prototypeManager.Index(batch.Recipe);
@@ -248,18 +279,40 @@ public sealed partial class LatheMenu : DefaultWindow
var itemName = _lathe.GetRecipeName(batch.Recipe);
string displayText;
if (batch.ItemsRequested > 1)
- displayText = Loc.GetString("lathe-menu-item-batch", ("index", idx), ("name", itemName), ("printed", batch.ItemsPrinted), ("total", batch.ItemsRequested));
+ 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), ("name", itemName));
+ displayText = Loc.GetString("lathe-menu-item-single", ("index", idx + 1), ("name", itemName));
- var queuedRecipeBox = new QueuedRecipeControl(displayText, idx - 1, GetRecipeDisplayControl(recipe));
- queuedRecipeBox.OnDeletePressed += s => QueueDeleteAction?.Invoke(s);
- queuedRecipeBox.OnMoveUpPressed += s => QueueMoveUpAction?.Invoke(s);
- queuedRecipeBox.OnMoveDownPressed += s => QueueMoveDownAction?.Invoke(s);
+ 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;
- QueueList.AddChild(queuedRecipeBox);
+ 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.cs b/Content.Client/Lathe/UI/QueuedRecipeControl.xaml.cs
index c4ba9803b0..69c8da6d7b 100644
--- a/Content.Client/Lathe/UI/QueuedRecipeControl.xaml.cs
+++ b/Content.Client/Lathe/UI/QueuedRecipeControl.xaml.cs
@@ -11,26 +11,46 @@ public sealed partial class QueuedRecipeControl : Control
public Action? OnMoveUpPressed;
public Action? OnMoveDownPressed;
+ private int _index;
+
public QueuedRecipeControl(string displayText, int index, Control displayControl)
{
RobustXamlLoader.Load(this);
- RecipeName.Text = displayText;
- RecipeDisplayContainer.AddChild(displayControl);
+ SetDisplayText(displayText);
+ SetDisplayControl(displayControl);
+ SetIndex(index);
+ _index = index;
MoveUp.OnPressed += (_) =>
{
- OnMoveUpPressed?.Invoke(index);
+ OnMoveUpPressed?.Invoke(_index);
};
MoveDown.OnPressed += (_) =>
{
- OnMoveDownPressed?.Invoke(index);
+ OnMoveDownPressed?.Invoke(_index);
};
Delete.OnPressed += (_) =>
{
- OnDeletePressed?.Invoke(index);
+ 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/Lobby/LobbyUIController.cs b/Content.Client/Lobby/LobbyUIController.cs
index 121e8dbe71..e36a2cd174 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());
}
@@ -361,7 +362,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered(species))
page = new ProtoId(species.Id); // Gross. See above todo comment.
- if (_prototypeManager.TryIndex(DefaultSpeciesGuidebook, out var guideRoot))
+ if (_prototypeManager.Resolve(DefaultSpeciesGuidebook, out var guideRoot))
{
var dict = new Dictionary, GuideEntry>();
dict.Add(DefaultSpeciesGuidebook, guideRoot);
@@ -1291,7 +1291,7 @@ namespace Content.Client.Lobby.UI
var sexes = new List();
// add species sex options, default to just none if we are in bizzaro world and have no species
- if (_prototypeManager.TryIndex(Profile.Species, out var speciesProto))
+ if (_prototypeManager.Resolve(Profile.Species, out var speciesProto))
{
foreach (var sex in speciesProto.Sexes)
{
@@ -1384,7 +1384,7 @@ namespace Content.Client.Lobby.UI
if (species is null)
return;
- if (!_prototypeManager.TryIndex(species, out var speciesProto))
+ if (!_prototypeManager.Resolve(species, out var speciesProto))
return;
// Don't display the info button if no guide entry is found
diff --git a/Content.Client/Lobby/UI/Loadouts/LoadoutContainer.xaml.cs b/Content.Client/Lobby/UI/Loadouts/LoadoutContainer.xaml.cs
index 2264cecd23..035f4a3c1a 100644
--- a/Content.Client/Lobby/UI/Loadouts/LoadoutContainer.xaml.cs
+++ b/Content.Client/Lobby/UI/Loadouts/LoadoutContainer.xaml.cs
@@ -40,7 +40,7 @@ public sealed partial class LoadoutContainer : BoxContainer
SelectButton.TooltipSupplier = _ => tooltip;
}
- if (_protoManager.TryIndex(proto, out var loadProto))
+ if (_protoManager.Resolve(proto, out var loadProto))
{
var ent = loadProto.DummyEntity ?? _entManager.System().GetFirstOrNull(loadProto);
diff --git a/Content.Client/Lobby/UI/Loadouts/LoadoutGroupContainer.xaml.cs b/Content.Client/Lobby/UI/Loadouts/LoadoutGroupContainer.xaml.cs
index b06e7e41d1..644910db60 100644
--- a/Content.Client/Lobby/UI/Loadouts/LoadoutGroupContainer.xaml.cs
+++ b/Content.Client/Lobby/UI/Loadouts/LoadoutGroupContainer.xaml.cs
@@ -62,7 +62,7 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
});
}
- if (protoMan.TryIndex(loadout.Role, out var roleProto) && roleProto.Points != null && loadout.Points != null)
+ if (protoMan.Resolve(loadout.Role, out var roleProto) && roleProto.Points != null && loadout.Points != null)
{
RestrictionsContainer.AddChild(new Label()
{
@@ -112,14 +112,14 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
})
.ToList();
- /*
- * Determine which element should be displayed first:
- * - If any element is currently selected (its button is pressed), use it.
- * - Otherwise, fallback to the first element in the list.
- *
- * This moves the selected item outside of the sublist for better usability,
- * making it easier for players to quickly toggle loadout options (e.g. clothing, accessories)
- * without having to search inside expanded subgroups.
+ /*
+ * Determine which element should be displayed first:
+ * - If any element is currently selected (its button is pressed), use it.
+ * - Otherwise, fallback to the first element in the list.
+ *
+ * This moves the selected item outside of the sublist for better usability,
+ * making it easier for players to quickly toggle loadout options (e.g. clothing, accessories)
+ * without having to search inside expanded subgroups.
*/
var firstElement = uiElements.FirstOrDefault(e => e.Select.Pressed) ?? uiElements[0];
@@ -195,8 +195,8 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
///
/// Creates a UI container for a single Loadout item.
///
- /// This method was extracted from RefreshLoadouts because the logic for creating
- /// individual loadout items is used multiple times inside that method, and duplicating
+ /// This method was extracted from RefreshLoadouts because the logic for creating
+ /// individual loadout items is used multiple times inside that method, and duplicating
/// the code made it harder to maintain.
///
/// Logic:
diff --git a/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs b/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs
index 68e1ecbeae..50860b349a 100644
--- a/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs
+++ b/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs
@@ -68,7 +68,7 @@ public sealed partial class LoadoutWindow : FancyWindow
{
foreach (var group in proto.Groups)
{
- if (!protoManager.TryIndex(group, out var groupProto))
+ if (!protoManager.Resolve(group, out var groupProto))
continue;
if (groupProto.Hidden)
diff --git a/Content.Client/Medical/CrewMonitoring/CrewMonitoringNavMapControl.cs b/Content.Client/Medical/CrewMonitoring/CrewMonitoringNavMapControl.cs
index 340cc9af89..651c76e61f 100644
--- a/Content.Client/Medical/CrewMonitoring/CrewMonitoringNavMapControl.cs
+++ b/Content.Client/Medical/CrewMonitoring/CrewMonitoringNavMapControl.cs
@@ -64,7 +64,9 @@ public sealed partial class CrewMonitoringNavMapControl : NavMapControl
if (!LocalizedNames.TryGetValue(netEntity, out var name))
name = "Unknown";
- var message = name + "\nLocation: [x = " + MathF.Round(blip.Coordinates.X) + ", y = " + MathF.Round(blip.Coordinates.Y) + "]";
+ var message = name + "\n" + Loc.GetString("navmap-location",
+ ("x", MathF.Round(blip.Coordinates.X)),
+ ("y", MathF.Round(blip.Coordinates.Y)));
_trackedEntityLabel.Text = message;
_trackedEntityPanel.Visible = true;
diff --git a/Content.Client/Overlays/EntityHealthBarOverlay.cs b/Content.Client/Overlays/EntityHealthBarOverlay.cs
index 9ff0422aba..cf9d879844 100644
--- a/Content.Client/Overlays/EntityHealthBarOverlay.cs
+++ b/Content.Client/Overlays/EntityHealthBarOverlay.cs
@@ -57,7 +57,7 @@ public sealed class EntityHealthBarOverlay : Overlay
const float scale = 1f;
var scaleMatrix = Matrix3Helpers.CreateScale(new Vector2(scale, scale));
var rotationMatrix = Matrix3Helpers.CreateRotation(-rotation);
- _prototype.TryIndex(StatusIcon, out var statusIcon);
+ _prototype.Resolve(StatusIcon, out var statusIcon);
var query = _entManager.AllEntityQueryEnumerator();
while (query.MoveNext(out var uid,
diff --git a/Content.Client/Overlays/ShowCriminalRecordIconsSystem.cs b/Content.Client/Overlays/ShowCriminalRecordIconsSystem.cs
index c353b17272..9a84defba0 100644
--- a/Content.Client/Overlays/ShowCriminalRecordIconsSystem.cs
+++ b/Content.Client/Overlays/ShowCriminalRecordIconsSystem.cs
@@ -22,7 +22,7 @@ public sealed class ShowCriminalRecordIconsSystem : EquipmentHudSystem(entity, out var state))
{
// Since there is no MobState for a rotting mob, we have to deal with this case first.
- if (HasComp(entity) && _prototypeMan.TryIndex(damageableComponent.RottingIcon, out var rottingIcon))
+ if (HasComp(entity) && _prototypeMan.Resolve(damageableComponent.RottingIcon, out var rottingIcon))
result.Add(rottingIcon);
- else if (damageableComponent.HealthIcons.TryGetValue(state.CurrentState, out var value) && _prototypeMan.TryIndex(value, out var icon))
+ else if (damageableComponent.HealthIcons.TryGetValue(state.CurrentState, out var value) && _prototypeMan.Resolve(value, out var icon))
result.Add(icon);
}
}
diff --git a/Content.Client/Overlays/ShowJobIconsSystem.cs b/Content.Client/Overlays/ShowJobIconsSystem.cs
index d0d14449f6..faf4024c2f 100644
--- a/Content.Client/Overlays/ShowJobIconsSystem.cs
+++ b/Content.Client/Overlays/ShowJobIconsSystem.cs
@@ -51,7 +51,7 @@ public sealed class ShowJobIconsSystem : EquipmentHudSystem(proto, out var weatherProto))
+ if (!_protoManager.Resolve(proto, out var weatherProto))
continue;
var alpha = _weather.GetPercent(weather, mapUid);
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/RCD/RCDMenuBoundUserInterface.cs b/Content.Client/RCD/RCDMenuBoundUserInterface.cs
index c001b7ec70..6aa32892cf 100644
--- a/Content.Client/RCD/RCDMenuBoundUserInterface.cs
+++ b/Content.Client/RCD/RCDMenuBoundUserInterface.cs
@@ -51,10 +51,10 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface
_menu.OpenOverMouseScreenPosition();
}
- private IEnumerable ConvertToButtons(HashSet> prototypes)
+ private IEnumerable ConvertToButtons(HashSet> prototypes)
{
- Dictionary> buttonsByCategory = new();
- ValueList topLevelActions = new();
+ Dictionary> buttonsByCategory = new();
+ ValueList topLevelActions = new();
foreach (var protoId in prototypes)
{
var prototype = _prototypeManager.Index(protoId);
@@ -62,7 +62,7 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface
{
var topLevelActionOption = new RadialMenuActionOption(HandleMenuOptionClick, prototype)
{
- Sprite = prototype.Sprite,
+ IconSpecifier = RadialMenuIconSpecifier.With(prototype.Sprite),
ToolTip = GetTooltip(prototype)
};
topLevelActions.Add(topLevelActionOption);
@@ -74,26 +74,26 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface
if (!buttonsByCategory.TryGetValue(prototype.Category, out var list))
{
- list = new List();
+ list = new List();
buttonsByCategory.Add(prototype.Category, list);
}
var actionOption = new RadialMenuActionOption(HandleMenuOptionClick, prototype)
{
- Sprite = prototype.Sprite,
+ IconSpecifier = RadialMenuIconSpecifier.With(prototype.Sprite),
ToolTip = GetTooltip(prototype)
};
list.Add(actionOption);
}
- var models = new RadialMenuOption[buttonsByCategory.Count + topLevelActions.Count];
+ var models = new RadialMenuOptionBase[buttonsByCategory.Count + topLevelActions.Count];
var i = 0;
foreach (var (key, list) in buttonsByCategory)
{
var groupInfo = PrototypesGroupingInfo[key];
models[i] = new RadialMenuNestedLayerOption(list)
{
- Sprite = groupInfo.Sprite,
+ IconSpecifier = RadialMenuIconSpecifier.With(groupInfo.Sprite),
ToolTip = Loc.GetString(groupInfo.Tooltip)
};
i++;
@@ -125,8 +125,10 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface
var name = Loc.GetString(proto.SetName);
if (proto.Prototype != null &&
- _prototypeManager.TryIndex(proto.Prototype, out var entProto, logError: false))
+ _prototypeManager.Resolve(proto.Prototype, out var entProto))
+ {
name = entProto.Name;
+ }
msg = Loc.GetString("rcd-component-change-build-mode", ("name", name));
}
@@ -142,7 +144,7 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface
if (proto.Mode is RcdMode.ConstructTile or RcdMode.ConstructObject
&& proto.Prototype != null
- && _prototypeManager.TryIndex(proto.Prototype, out var entProto, logError: false))
+ && _prototypeManager.Resolve(proto.Prototype, out var entProto))
{
tooltip = Loc.GetString(entProto.Name);
}
diff --git a/Content.Client/Radio/Ui/IntercomMenu.xaml.cs b/Content.Client/Radio/Ui/IntercomMenu.xaml.cs
index f66b3db000..887c6f6443 100644
--- a/Content.Client/Radio/Ui/IntercomMenu.xaml.cs
+++ b/Content.Client/Radio/Ui/IntercomMenu.xaml.cs
@@ -42,7 +42,7 @@ public sealed partial class IntercomMenu : FancyWindow
for (var i = 0; i < entity.Comp.SupportedChannels.Count; i++)
{
var channel = entity.Comp.SupportedChannels[i];
- if (!_prototype.TryIndex(channel, out var prototype))
+ if (!_prototype.Resolve(channel, out var prototype))
continue;
_channels.Add(channel);
diff --git a/Content.Client/Revolutionary/RevolutionarySystem.cs b/Content.Client/Revolutionary/RevolutionarySystem.cs
index 8e7e687fa8..2dc16d9c11 100644
--- a/Content.Client/Revolutionary/RevolutionarySystem.cs
+++ b/Content.Client/Revolutionary/RevolutionarySystem.cs
@@ -25,13 +25,13 @@ public sealed class RevolutionarySystem : SharedRevolutionarySystem
if (HasComp(ent))
return;
- if (_prototype.TryIndex(ent.Comp.StatusIcon, out var iconPrototype))
+ if (_prototype.Resolve(ent.Comp.StatusIcon, out var iconPrototype))
args.StatusIcons.Add(iconPrototype);
}
private void GetHeadRevIcon(Entity ent, ref GetStatusIconsEvent args)
{
- if (_prototype.TryIndex(ent.Comp.StatusIcon, out var iconPrototype))
+ if (_prototype.Resolve(ent.Comp.StatusIcon, out var iconPrototype))
args.StatusIcons.Add(iconPrototype);
}
}
diff --git a/Content.Client/Shuttles/Systems/EmergencyShuttleSystem.cs b/Content.Client/Shuttles/Systems/EmergencyShuttleSystem.cs
new file mode 100644
index 0000000000..c2b8dc8c8d
--- /dev/null
+++ b/Content.Client/Shuttles/Systems/EmergencyShuttleSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared.Shuttles.Systems;
+
+namespace Content.Client.Shuttles.Systems;
+
+public sealed partial class EmergencyShuttleSystem : SharedEmergencyShuttleSystem;
diff --git a/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs b/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs
index 77ac13c972..2ada6e4b01 100644
--- a/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs
+++ b/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs
@@ -23,15 +23,15 @@ public sealed class StationAiBoundUserInterface(EntityUid owner, Enum uiKey) : B
_menu.Open();
}
- private IEnumerable ConvertToButtons(IReadOnlyList actions)
+ private IEnumerable ConvertToButtons(IReadOnlyList actions)
{
- var models = new RadialMenuActionOption[actions.Count];
+ var models = new RadialMenuActionOptionBase[actions.Count];
for (int i = 0; i < actions.Count; i++)
{
var action = actions[i];
models[i] = new RadialMenuActionOption(HandleRadialMenuClick, action.Event)
{
- Sprite = action.Sprite,
+ IconSpecifier = RadialMenuIconSpecifier.With(action.Sprite),
ToolTip = action.Tooltip
};
}
diff --git a/Content.Client/Silicons/StationAi/StationAiCustomizationMenu.xaml.cs b/Content.Client/Silicons/StationAi/StationAiCustomizationMenu.xaml.cs
index 009969196b..3c5375a8e4 100644
--- a/Content.Client/Silicons/StationAi/StationAiCustomizationMenu.xaml.cs
+++ b/Content.Client/Silicons/StationAi/StationAiCustomizationMenu.xaml.cs
@@ -44,7 +44,7 @@ public sealed partial class StationAiCustomizationMenu : FancyWindow
StationAiCustomizationPrototype? selectedPrototype = null;
if (stationAiCustomization?.ProtoIds.TryGetValue(groupPrototype, out var selectedProtoId) == true)
- _protoManager.TryIndex(selectedProtoId, out selectedPrototype);
+ _protoManager.Resolve(selectedProtoId, out selectedPrototype);
_buttonGroups[groupPrototype] = new ButtonGroup();
_groupContainers[groupPrototype] = new StationAiCustomizationGroupContainer(groupPrototype, selectedPrototype, _buttonGroups[groupPrototype], this, _protoManager);
@@ -76,7 +76,7 @@ public sealed partial class StationAiCustomizationMenu : FancyWindow
// Create UI entries for all customization in the group
foreach (var protoId in groupPrototype.ProtoIds)
{
- if (!protoManager.TryIndex(protoId, out var prototype))
+ if (!protoManager.Resolve(protoId, out var prototype))
continue;
var entry = new StationAiCustomizationEntryContainer(groupPrototype, prototype, buttonGroup, menu);
diff --git a/Content.Client/SprayPainter/SprayPainterSystem.cs b/Content.Client/SprayPainter/SprayPainterSystem.cs
index 8f7d7f0362..788e97bef1 100644
--- a/Content.Client/SprayPainter/SprayPainterSystem.cs
+++ b/Content.Client/SprayPainter/SprayPainterSystem.cs
@@ -65,7 +65,7 @@ public sealed class SprayPainterSystem : SharedSprayPainterSystem
var groupList = new List();
foreach (var groupId in category.Groups)
{
- if (!Proto.TryIndex(groupId, out var group))
+ if (!Proto.Resolve(groupId, out var group))
continue;
groupList.Add(groupId);
diff --git a/Content.Client/Storage/Visualizers/EntityStorageVisualizerSystem.cs b/Content.Client/Storage/Visualizers/EntityStorageVisualizerSystem.cs
index c10a24ac32..ea0b800ccc 100644
--- a/Content.Client/Storage/Visualizers/EntityStorageVisualizerSystem.cs
+++ b/Content.Client/Storage/Visualizers/EntityStorageVisualizerSystem.cs
@@ -42,7 +42,7 @@ public sealed class EntityStorageVisualizerSystem : VisualizerSystem(uid, PaintableVisuals.Prototype, out var prototype, args.Component))
{
- if (_prototypeManager.TryIndex(prototype, out var proto))
+ if (_prototypeManager.Resolve(prototype, out var proto))
{
if (proto.TryGetComponent(out SpriteComponent? sprite, _componentFactory))
{
diff --git a/Content.Client/Store/Ui/StoreWithdrawWindow.xaml.cs b/Content.Client/Store/Ui/StoreWithdrawWindow.xaml.cs
index a8b93cd2b3..e706e74bc3 100644
--- a/Content.Client/Store/Ui/StoreWithdrawWindow.xaml.cs
+++ b/Content.Client/Store/Ui/StoreWithdrawWindow.xaml.cs
@@ -33,7 +33,7 @@ public sealed partial class StoreWithdrawWindow : DefaultWindow
_validCurrencies.Clear();
foreach (var currency in balance)
{
- if (!_prototypeManager.TryIndex(currency.Key, out var proto))
+ if (!_prototypeManager.Resolve(currency.Key, out var proto))
continue;
_validCurrencies.Add(proto, currency.Value);
diff --git a/Content.Client/UserInterface/Controls/RadialMenu.cs b/Content.Client/UserInterface/Controls/RadialMenu.cs
index 9734cf2960..959a60ef4f 100644
--- a/Content.Client/UserInterface/Controls/RadialMenu.cs
+++ b/Content.Client/UserInterface/Controls/RadialMenu.cs
@@ -229,10 +229,10 @@ public class RadialMenu : BaseWindow
/// from interactions.
///
[Virtual]
-public class RadialMenuTextureButtonBase : TextureButton
+public abstract class RadialMenuButtonBase : BaseButton
{
///
- protected RadialMenuTextureButtonBase()
+ protected RadialMenuButtonBase()
{
EnableAllKeybinds = true;
}
@@ -242,7 +242,9 @@ public class RadialMenuTextureButtonBase : TextureButton
{
if (args.Function == EngineKeyFunctions.UIClick
|| args.Function == ContentKeyFunctions.AltActivateItemInWorld)
+ {
base.KeyBindUp(args);
+ }
}
}
@@ -253,8 +255,14 @@ public class RadialMenuTextureButtonBase : TextureButton
/// works only if control have parent, and ActiveContainer property is set.
/// Also considers all space outside of radial menu buttons as itself for clicking.
///
-public sealed class RadialMenuContextualCentralTextureButton : RadialMenuTextureButtonBase
+public sealed class RadialMenuContextualCentralTextureButton : TextureButton
{
+ ///
+ public RadialMenuContextualCentralTextureButton()
+ {
+ EnableAllKeybinds = true;
+ }
+
public float InnerRadius { get; set; }
public Vector2? ParentCenter { get; set; }
@@ -271,15 +279,25 @@ public sealed class RadialMenuContextualCentralTextureButton : RadialMenuTexture
var innerRadiusSquared = InnerRadius * InnerRadius;
- // comparing to squared values is faster then making sqrt
+ // comparing to squared values is faster, then making sqrt
return distSquared < innerRadiusSquared;
}
+
+ ///
+ protected override void KeyBindUp(GUIBoundKeyEventArgs args)
+ {
+ if (args.Function == EngineKeyFunctions.UIClick
+ || args.Function == ContentKeyFunctions.AltActivateItemInWorld)
+ {
+ base.KeyBindUp(args);
+ }
+ }
}
///
/// Menu button for outer area of radial menu (covers everything 'outside').
///
-public sealed class RadialMenuOuterAreaButton : RadialMenuTextureButtonBase
+public sealed class RadialMenuOuterAreaButton : RadialMenuButtonBase
{
public float OuterRadius { get; set; }
@@ -303,7 +321,7 @@ public sealed class RadialMenuOuterAreaButton : RadialMenuTextureButtonBase
}
[Virtual]
-public class RadialMenuTextureButton : RadialMenuTextureButtonBase
+public class RadialMenuButton : RadialMenuButtonBase
{
///
/// Upon clicking this button the radial menu will be moved to the layer of this control.
@@ -319,9 +337,8 @@ public class RadialMenuTextureButton : RadialMenuTextureButtonBase
///
/// A simple texture button that can move the user to a different layer within a radial menu
///
- public RadialMenuTextureButton()
+ public RadialMenuButton()
{
- EnableAllKeybinds = true;
OnButtonUp += OnClicked;
}
@@ -391,7 +408,7 @@ public interface IRadialMenuItemWithSector
}
[Virtual]
-public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadialMenuItemWithSector
+public class RadialMenuButtonWithSector : RadialMenuButton, IRadialMenuItemWithSector
{
private Vector2[]? _sectorPointsForDrawing;
@@ -500,7 +517,7 @@ public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadia
///
/// A simple texture button that can move the user to a different layer within a radial menu
///
- public RadialMenuTextureButtonWithSector()
+ public RadialMenuButtonWithSector()
{
}
diff --git a/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs b/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs
index 31d7eab340..ec7dcbbb5a 100644
--- a/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs
+++ b/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs
@@ -7,6 +7,8 @@ using Robust.Client.GameObjects;
using Robust.Shared.Timing;
using Robust.Client.UserInterface.XAML;
using Robust.Client.Input;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Prototypes;
namespace Content.Client.UserInterface.Controls;
@@ -30,7 +32,7 @@ public sealed partial class SimpleRadialMenu : RadialMenu
_attachMenuToEntity = owner;
}
- public void SetButtons(IEnumerable models, SimpleRadialMenuSettings? settings = null)
+ public void SetButtons(IEnumerable models, SimpleRadialMenuSettings? settings = null)
{
ClearExistingChildrenRadialButtons();
@@ -45,7 +47,7 @@ public sealed partial class SimpleRadialMenu : RadialMenu
}
private void Fill(
- IEnumerable models,
+ IEnumerable models,
SpriteSystem sprites,
ICollection rootControlChildren,
SimpleRadialMenuSettings settings
@@ -77,7 +79,7 @@ public sealed partial class SimpleRadialMenu : RadialMenu
}
}
- private RadialMenuTextureButton RecursiveContainerExtraction(
+ private RadialMenuButton RecursiveContainerExtraction(
SpriteSystem sprites,
ICollection rootControlChildren,
RadialMenuNestedLayerOption model,
@@ -112,8 +114,8 @@ public sealed partial class SimpleRadialMenu : RadialMenu
return thisLayerLinkButton;
}
- private RadialMenuTextureButton ConvertToButton(
- RadialMenuOption model,
+ private RadialMenuButton ConvertToButton(
+ RadialMenuOptionBase model,
SpriteSystem sprites,
SimpleRadialMenuSettings settings,
bool haveNested
@@ -121,29 +123,26 @@ public sealed partial class SimpleRadialMenu : RadialMenu
{
var button = settings.UseSectors
? ConvertToButtonWithSector(model, settings)
- : new RadialMenuTextureButton();
+ : new RadialMenuButton();
button.SetSize = new Vector2(64f, 64f);
button.ToolTip = model.ToolTip;
- if (model.Sprite != null)
+ var imageControl = model.IconSpecifier switch
{
- var scale = Vector2.One;
+ RadialMenuTextureIconSpecifier textureSpecifier => CreateTexture(textureSpecifier.Sprite, sprites),
+ RadialMenuEntityIconSpecifier entitySpecifier => CreateSpriteView(entitySpecifier.Entity),
+ RadialMenuEntityPrototypeIconSpecifier entProtoSpecifier => CreateEntityPrototypeView(entProtoSpecifier.ProtoId),
+ _ => null
+ };
- var texture = sprites.Frame0(model.Sprite);
- if (texture.Width <= 32)
- {
- scale *= 2;
- }
+ if(imageControl != null)
+ button.AddChild(imageControl);
- button.TextureNormal = texture;
- button.Scale = scale;
- }
-
- if (model is RadialMenuActionOption actionOption)
+ if (model is RadialMenuActionOptionBase actionOption)
{
button.OnPressed += _ =>
{
actionOption.OnPressed?.Invoke();
- if(!haveNested)
+ if (!haveNested)
Close();
};
}
@@ -151,9 +150,53 @@ public sealed partial class SimpleRadialMenu : RadialMenu
return button;
}
- private static RadialMenuTextureButtonWithSector ConvertToButtonWithSector(RadialMenuOption model, SimpleRadialMenuSettings settings)
+ private Control CreateEntityPrototypeView(EntProtoId protoId)
{
- var button = new RadialMenuTextureButtonWithSector
+ var entProtoView = new EntityPrototypeView
+ {
+ SetSize = new Vector2(48, 48),
+ VerticalAlignment = VAlignment.Center,
+ HorizontalAlignment = HAlignment.Center,
+ Stretch = SpriteView.StretchMode.Fill,
+ };
+ entProtoView.SetPrototype(protoId);
+ return entProtoView;
+ }
+
+ private static Control CreateSpriteView(EntityUid entityForSpriteView)
+ {
+ var entView = new SpriteView
+ {
+ SetSize = new Vector2(48, 48),
+ VerticalAlignment = VAlignment.Center,
+ HorizontalAlignment = HAlignment.Center,
+ Stretch = SpriteView.StretchMode.Fill,
+ };
+ entView.SetEntity(entityForSpriteView);
+ return entView;
+ }
+
+ private static Control CreateTexture(SpriteSpecifier spriteSpecifier, SpriteSystem sprites)
+ {
+ var scale = Vector2.One;
+
+ var texture = sprites.Frame0(spriteSpecifier);
+ if (texture.Width <= 32)
+ {
+ scale *= 2;
+ }
+
+ var imageControl = new TextureRect()
+ {
+ Texture = texture,
+ TextureScale = scale
+ };
+ return imageControl;
+ }
+
+ private static RadialMenuButtonWithSector ConvertToButtonWithSector(RadialMenuOptionBase model, SimpleRadialMenuSettings settings)
+ {
+ var button = new RadialMenuButtonWithSector
{
DrawBorder = settings.DisplayBorders,
DrawBackground = !settings.NoBackground
@@ -228,32 +271,99 @@ public sealed partial class SimpleRadialMenu : RadialMenu
}
-
-public abstract class RadialMenuOption
+///
+/// Abstract representation of a way to specify icon in radial menu.
+///
+public abstract record RadialMenuIconSpecifier
{
- public string? ToolTip { get; init; }
+ /// Use entity prototype viewer.
+ public static RadialMenuIconSpecifier? With(EntProtoId? protoId)
+ {
+ if (protoId is null)
+ return null;
- public SpriteSpecifier? Sprite { get; init; }
- public Color? BackgroundColor { get; set; }
- public Color? HoverBackgroundColor { get; set; }
+ return new RadialMenuEntityPrototypeIconSpecifier(protoId.Value);
+ }
+
+ /// Use simple texture icon.
+ public static RadialMenuIconSpecifier? With(SpriteSpecifier? sprite)
+ {
+ if (sprite == null)
+ return null;
+
+ return new RadialMenuTextureIconSpecifier(sprite);
+ }
+
+ /// Use entity sprite viewer.
+ public static RadialMenuIconSpecifier? With(EntityUid? entity)
+ {
+ if (entity == null)
+ return null;
+
+ return new RadialMenuEntityIconSpecifier(entity.Value);
+ }
}
-public abstract class RadialMenuActionOption(Action onPressed) : RadialMenuOption
+/// Marker that should be used to display radial menu icon.
+public sealed record RadialMenuEntityIconSpecifier(EntityUid Entity) : RadialMenuIconSpecifier;
+
+/// Marker that should be used to display radial menu icon.
+public sealed record RadialMenuTextureIconSpecifier(SpriteSpecifier Sprite) : RadialMenuIconSpecifier;
+
+/// Marker that should be used to display radial menu icon.
+public sealed record RadialMenuEntityPrototypeIconSpecifier(EntProtoId ProtoId) : RadialMenuIconSpecifier;
+
+/// Container for common options for radial menu button.
+public abstract class RadialMenuOptionBase
{
+ /// Tooltip to be displayed when button is hovered.
+ public string? ToolTip { get; init; }
+
+ ///
+ /// Color for button background.
+ /// Is used only with sector radial ().
+ ///
+ public Color? BackgroundColor { get; set; }
+ ///
+ /// Color for button background when it is hovered.
+ /// Is used only with sector radial ().
+ ///
+ public Color? HoverBackgroundColor { get; set; }
+
+ ///
+ /// Specifier that describes icon to be used for radial menu button.
+ ///
+ public RadialMenuIconSpecifier? IconSpecifier { get; set; }
+}
+
+/// Base type for model of radial menu button with some action on button pressed.
+///
+public abstract class RadialMenuActionOptionBase(Action onPressed) : RadialMenuOptionBase
+{
+ /// Action to be executed on button press.
public Action OnPressed { get; } = onPressed;
}
-public sealed class RadialMenuActionOption(Action onPressed, T data)
- : RadialMenuActionOption(onPressed: () => onPressed(data));
+/// Strong-typed model for radial menu button with action, stores provided data to be used upon button press.
+public sealed class RadialMenuActionOption(Action onPressed, T data) : RadialMenuActionOptionBase(onPressed: () => onPressed(data));
-public sealed class RadialMenuNestedLayerOption(IReadOnlyCollection nested, float containerRadius = 100)
- : RadialMenuOption
+///
+/// Model for radial menu button that represents reference for next layer of radial buttons.
+///
+/// List of button models for next layer of menu.
+/// Radius for radial menu buttons of next layer.
+public sealed class RadialMenuNestedLayerOption(IReadOnlyCollection nested, float containerRadius = 100) : RadialMenuOptionBase
{
+ /// Radius for radial menu buttons of next layer.
public float? ContainerRadius { get; } = containerRadius;
- public IReadOnlyCollection Nested { get; } = nested;
+ /// List of button models for next layer of menu.
+ public IReadOnlyCollection Nested { get; } = nested;
}
+///
+/// Additional settings for radial menu render.
+///
public sealed class SimpleRadialMenuSettings
{
///
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
{
- [Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
@@ -133,12 +132,12 @@ public sealed class EmotesUIController : UIController, IOnStateChanged ConvertToButtons(IEnumerable emotePrototypes)
+ private IEnumerable ConvertToButtons(IEnumerable emotePrototypes)
{
var whitelistSystem = EntitySystemManager.GetEntitySystem();
var player = _playerManager.LocalSession?.AttachedEntity;
- Dictionary> emotesByCategory = new();
+ Dictionary> emotesByCategory = new();
foreach (var emote in emotePrototypes)
{
if(emote.Category == EmoteCategory.Invalid)
@@ -158,19 +157,19 @@ public sealed class EmotesUIController : UIController, IOnStateChanged();
+ list = new List();
emotesByCategory.Add(emote.Category, list);
}
var actionOption = new RadialMenuActionOption(HandleRadialButtonClick, emote)
{
- Sprite = emote.Icon,
+ IconSpecifier = RadialMenuIconSpecifier.With(emote.Icon),
ToolTip = Loc.GetString(emote.Name)
};
list.Add(actionOption);
}
- var models = new RadialMenuOption[emotesByCategory.Count];
+ var models = new RadialMenuOptionBase[emotesByCategory.Count];
var i = 0;
foreach (var (key, list) in emotesByCategory)
{
@@ -178,7 +177,7 @@ public sealed class EmotesUIController : UIController, IOnStateChanged
+/// 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/Commands/SuicideCommandTests.cs b/Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs
index 61b8d54448..c232ccf415 100644
--- a/Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs
+++ b/Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs
@@ -146,8 +146,8 @@ public sealed class SuicideCommandTests
mobThresholdsComp = entManager.GetComponent(player);
damageableComp = entManager.GetComponent(player);
- if (protoMan.TryIndex(DamageType, out var slashProto))
- damageableSystem.TryChangeDamage(player, new DamageSpecifier(slashProto, FixedPoint2.New(46.5)));
+ var slashProto = protoMan.Index(DamageType);
+ damageableSystem.TryChangeDamage(player, new DamageSpecifier(slashProto, FixedPoint2.New(46.5)));
});
// Check that running the suicide command kills the player
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/ContrabandTest.cs b/Content.IntegrationTests/Tests/ContrabandTest.cs
index a33e7c2067..c52ef293e1 100644
--- a/Content.IntegrationTests/Tests/ContrabandTest.cs
+++ b/Content.IntegrationTests/Tests/ContrabandTest.cs
@@ -27,8 +27,11 @@ public sealed class ContrabandTest
if (!proto.TryGetComponent(out var contraband, componentFactory))
continue;
- Assert.That(protoMan.TryIndex(contraband.Severity, out var severity, false),
- @$"{proto.ID} has a ContrabandComponent with a unknown severity.");
+ if (!protoMan.TryIndex(contraband.Severity, out var severity))
+ {
+ Assert.Fail($"{proto.ID} has a ContrabandComponent with a unknown severity.");
+ continue;
+ }
if (!severity.ShowDepartmentsAndJobs)
continue;
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/Lathe/LatheTest.cs b/Content.IntegrationTests/Tests/Lathe/LatheTest.cs
index 2fe347f636..c335f8d6c8 100644
--- a/Content.IntegrationTests/Tests/Lathe/LatheTest.cs
+++ b/Content.IntegrationTests/Tests/Lathe/LatheTest.cs
@@ -88,14 +88,18 @@ public sealed class LatheTest
// Check each recipe assigned to this lathe
foreach (var recipeId in recipes)
{
- Assert.That(protoMan.TryIndex(recipeId, out var recipeProto));
+ if (!protoMan.TryIndex(recipeId, out var recipeProto))
+ {
+ Assert.Fail($"Lathe recipe '{recipeId}' does not exist");
+ continue;
+ }
// Track the total material volume of the recipe
var totalQuantity = 0;
// Check each material called for by the recipe
foreach (var (materialId, quantity) in recipeProto.Materials)
{
- Assert.That(protoMan.TryIndex(materialId, out var materialProto));
+ Assert.That(protoMan.HasIndex(materialId), $"Material '{materialId}' does not exist");
// Make sure the material is accepted by the lathe
Assert.That(acceptedMaterials, Does.Contain(materialId), $"Lathe {latheProto.ID} has recipe {recipeId} but does not accept any materials containing {materialId}");
totalQuantity += quantity;
diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.cs b/Content.IntegrationTests/Tests/Minds/MindTests.cs
index 2f77519829..1bda6fd4db 100644
--- a/Content.IntegrationTests/Tests/Minds/MindTests.cs
+++ b/Content.IntegrationTests/Tests/Minds/MindTests.cs
@@ -145,10 +145,7 @@ public sealed partial class MindTests
await server.WaitAssertion(() =>
{
var damageable = entMan.GetComponent(entity);
- if (!protoMan.TryIndex(BluntDamageType, out var prototype))
- {
- return;
- }
+ var prototype = protoMan.Index(BluntDamageType);
damageableSystem.SetDamage(entity, damageable, new DamageSpecifier(prototype, FixedPoint2.New(401)));
Assert.That(mindSystem.GetMind(entity, mindContainerComp), Is.EqualTo(mindId));
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.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs
index a7a50a5270..22db3ca31f 100644
--- a/Content.IntegrationTests/Tests/PostMapInitTest.cs
+++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs
@@ -256,8 +256,7 @@ namespace Content.IntegrationTests.Tests
return;
var yamlEntities = node["entities"];
- if (!protoManager.TryIndex(DoNotMapCategory, out var dnmCategory))
- return;
+ var dnmCategory = protoManager.Index(DoNotMapCategory);
Assert.Multiple(() =>
{
@@ -266,7 +265,7 @@ namespace Content.IntegrationTests.Tests
var protoId = yamlEntity["proto"].AsString();
// This doesn't properly handle prototype migrations, but thats not a significant issue.
- if (!protoManager.TryIndex(protoId, out var proto, false))
+ if (!protoManager.TryIndex(protoId, out var proto))
continue;
Assert.That(!proto.Categories.Contains(dnmCategory),
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 0df760baef..1706908e72 100644
--- a/Content.Server/Access/Systems/AgentIDCardSystem.cs
+++ b/Content.Server/Access/Systems/AgentIDCardSystem.cs
@@ -45,7 +45,7 @@ namespace Content.Server.Access.Systems
if (!TryComp(ent, out var idCardComp))
return;
- _prototypeManager.TryIndex(args.Args.ChameleonOutfit.Job, out var jobProto);
+ _prototypeManager.Resolve(args.Args.ChameleonOutfit.Job, out var jobProto);
var jobIcon = args.Args.ChameleonOutfit.Icon ?? jobProto?.Icon;
var jobName = args.Args.ChameleonOutfit.Name ?? jobProto?.Name ?? "";
@@ -130,7 +130,7 @@ namespace Content.Server.Access.Systems
if (!TryComp(uid, out var idCard))
return;
- if (!_prototypeManager.TryIndex(args.JobIconId, out var jobIcon))
+ if (!_prototypeManager.Resolve(args.JobIconId, out var jobIcon))
return;
_cardSystem.TryChangeJobIcon(uid, jobIcon, idCard);
diff --git a/Content.Server/Access/Systems/IdCardConsoleSystem.cs b/Content.Server/Access/Systems/IdCardConsoleSystem.cs
index 62dfddbb58..13f2430b3d 100644
--- a/Content.Server/Access/Systems/IdCardConsoleSystem.cs
+++ b/Content.Server/Access/Systems/IdCardConsoleSystem.cs
@@ -98,7 +98,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
var targetIdComponent = Comp(targetId);
var targetAccessComponent = Comp(targetId);
- var jobProto = targetIdComponent.JobPrototype ?? new ProtoId(string.Empty);
+ var jobProto = targetIdComponent.JobPrototype ?? new ProtoId(string.Empty);
if (TryComp(targetId, out var keyStorage)
&& keyStorage.Key is { } key
&& _record.TryGetRecord(key, out var record))
@@ -130,7 +130,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
string newFullName,
string newJobTitle,
List> newAccessList,
- ProtoId newJobProto,
+ ProtoId newJobProto,
EntityUid player,
IdCardConsoleComponent? component = null)
{
@@ -144,7 +144,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
_idCard.TryChangeJobTitle(targetId, newJobTitle, player: player);
if (_prototype.TryIndex(newJobProto, out var job)
- && _prototype.TryIndex(job.Icon, out var jobIcon))
+ && _prototype.Resolve(job.Icon, out var jobIcon))
{
_idCard.TryChangeJobIcon(targetId, jobIcon, player: player);
_idCard.TryChangeJobDepartment(targetId, job);
diff --git a/Content.Server/Access/Systems/PresetIdCardSystem.cs b/Content.Server/Access/Systems/PresetIdCardSystem.cs
index 426e523243..6d9f61d941 100644
--- a/Content.Server/Access/Systems/PresetIdCardSystem.cs
+++ b/Content.Server/Access/Systems/PresetIdCardSystem.cs
@@ -82,7 +82,7 @@ public sealed class PresetIdCardSystem : EntitySystem
_cardSystem.TryChangeJobTitle(uid, job.LocalizedName);
_cardSystem.TryChangeJobDepartment(uid, job);
- if (_prototypeManager.TryIndex(job.Icon, out var jobIcon))
+ if (_prototypeManager.Resolve(job.Icon, out var jobIcon))
_cardSystem.TryChangeJobIcon(uid, jobIcon);
}
}
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/ExplosionCommand.cs b/Content.Server/Administration/Commands/ExplosionCommand.cs
index 787886f164..6f9e89243d 100644
--- a/Content.Server/Administration/Commands/ExplosionCommand.cs
+++ b/Content.Server/Administration/Commands/ExplosionCommand.cs
@@ -118,7 +118,7 @@ public sealed class ExplosionCommand : LocalizedEntityCommands
return;
}
}
- else if (!_prototypeManager.TryIndex(ExplosionSystem.DefaultExplosionPrototypeId, out type))
+ else if (!_prototypeManager.Resolve(ExplosionSystem.DefaultExplosionPrototypeId, out type))
{
// no prototype was specified, so lets default to whichever one was defined first
type = _prototypeManager.EnumeratePrototypes().FirstOrDefault();
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/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs b/Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs
index 3fca640d4a..94b20c7b77 100644
--- a/Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs
+++ b/Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs
@@ -38,7 +38,7 @@ public sealed partial class SpeakOnUIClosedSystem : SharedSpeakOnUIClosedSystem
if (!entity.Comp.Enabled)
return false;
- if (!_prototypeManager.TryIndex(entity.Comp.Pack, out var messagePack))
+ if (!_prototypeManager.Resolve(entity.Comp.Pack, out var messagePack))
return false;
var message = Loc.GetString(_random.Pick(messagePack.Values), ("name", Name(entity)));
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