diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 8acc40d5c2..acc65241a2 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -5,14 +5,20 @@
* @JerryImMouse
* @tau27
-# Ping for all PRs that include translations/editing fluent strings
-*.ftl @ficcialfaint
+# Translations
+*.ftl @Morb0 @DIMMoon1 @ficcialfaint
+<<<<<<< HEAD
# Map files
/Resources/Prototypes/Maps/** @Ko4ergaPunk
/Resources/Maps/** @Ko4ergaPunk
/Resources/Prototypes/_WL/Maps/** @0leshe
/Resources/Maps/_WL/** @0leshe
+=======
+# Maps
+/Resources/Prototypes/Maps/** @Morb0 @DIMMoon1 @Ko4ergaPunk
+/Resources/Maps/** @Morb0 @DIMMoon1 @Ko4ergaPunk
+>>>>>>> corvax/master
# Sprites
-/Resources/Textures/** @SonicHDC
+/Resources/Textures/** @Morb0 @DIMMoon1 @SonicHDC
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 0aac69d4e2..85acfdefd0 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -63,7 +63,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 4555b521ee..f8ae6092e6 100644
--- a/.github/workflows/test-packaging.yml
+++ b/.github/workflows/test-packaging.yml
@@ -74,7 +74,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/GlobalUsings.cs b/Content.Benchmarks/GlobalUsings.cs
new file mode 100644
index 0000000000..120b7f39b5
--- /dev/null
+++ b/Content.Benchmarks/GlobalUsings.cs
@@ -0,0 +1,3 @@
+// Global usings for Content.Benchmarks
+
+global using Robust.UnitTesting.Pool;
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..a783dd368f 100644
--- a/Content.Client/Access/UI/AccessOverriderWindow.xaml.cs
+++ b/Content.Client/Access/UI/AccessOverriderWindow.xaml.cs
@@ -25,11 +25,11 @@ namespace Content.Client.Access.UI
public void SetAccessLevels(IPrototypeManager protoManager, List> accessLevels)
{
_accessButtons.Clear();
- AccessLevelGrid.DisposeAllChildren();
+ AccessLevelGrid.RemoveAllChildren();
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/AgentIDCardWindow.xaml.cs b/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
index 320bb88a67..209c58c950 100644
--- a/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
+++ b/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
@@ -41,7 +41,7 @@ namespace Content.Client.Access.UI
public void SetAllowedIcons(string currentJobIconId)
{
- IconGrid.DisposeAllChildren();
+ IconGrid.RemoveAllChildren();
var jobIconButtonGroup = new ButtonGroup();
var i = 0;
diff --git a/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs b/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs
index 4f07c31009..7af78d9e5f 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;
@@ -99,8 +99,8 @@ public sealed partial class GroupedAccessLevelChecklist : BoxContainer
private bool TryRebuildAccessGroupControls()
{
- AccessGroupList.DisposeAllChildren();
- AccessLevelChecklist.DisposeAllChildren();
+ AccessGroupList.RemoveAllChildren();
+ AccessLevelChecklist.RemoveAllChildren();
// No access level prototypes were assigned to any of the access level groups.
// Either the turret controller has no assigned access levels or their names were invalid.
@@ -165,7 +165,7 @@ public sealed partial class GroupedAccessLevelChecklist : BoxContainer
///
public void RebuildAccessLevelsControls()
{
- AccessLevelChecklist.DisposeAllChildren();
+ AccessLevelChecklist.RemoveAllChildren();
_accessLevelEntries.Clear();
// No access level prototypes were assigned to any of the access level groups
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/Actions/ActionsSystem.cs b/Content.Client/Actions/ActionsSystem.cs
index 8efe0b2367..49d90dedaf 100644
--- a/Content.Client/Actions/ActionsSystem.cs
+++ b/Content.Client/Actions/ActionsSystem.cs
@@ -33,6 +33,7 @@ namespace Content.Client.Actions
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IResourceManager _resources = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
+ [Dependency] private readonly ISerializationManager _serialization = default!;
public event Action? OnActionAdded;
public event Action? OnActionRemoved;
@@ -286,8 +287,27 @@ namespace Content.Client.Actions
continue;
}
+ if (assignmentNode is SequenceDataNode sequenceAssignments)
+ {
+ try
+ {
+ var nodeAssignments = _serialization.Read>(sequenceAssignments, notNullableOverride: true);
+
+ foreach (var index in nodeAssignments)
+ {
+ assignments.Add(new SlotAssignment(index.Hotbar, index.Slot, actionId));
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"Failed to parse action assignments: {ex}");
+ }
+ }
+
AddActionDirect((user, actions), actionId);
}
+
+ AssignSlot?.Invoke(assignments);
}
private void OnWorldTargetAttempt(Entity ent, ref ActionTargetAttemptEvent args)
@@ -309,10 +329,10 @@ namespace Content.Client.Actions
// this is the actual entity-world targeting magic
EntityUid? targetEnt = null;
if (TryComp(ent, out var entity) &&
- args.Input.EntityUid != null &&
- ValidateEntityTarget(user, args.Input.EntityUid, (uid, entity)))
+ args.Input.EntityUid is { Valid: true } entityUid &&
+ ValidateEntityTarget(user, entityUid, (uid, entity)))
{
- targetEnt = args.Input.EntityUid;
+ targetEnt = entityUid;
}
if (action.ClientExclusive)
diff --git a/Content.Client/Administration/UI/AdminCamera/AdminCameraWindow.xaml b/Content.Client/Administration/UI/AdminCamera/AdminCameraWindow.xaml
index 87583cef97..a6ac34bb29 100644
--- a/Content.Client/Administration/UI/AdminCamera/AdminCameraWindow.xaml
+++ b/Content.Client/Administration/UI/AdminCamera/AdminCameraWindow.xaml
@@ -1,6 +1,5 @@
+ MinSize="200 225">
diff --git a/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs b/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs
index 46090a6f3d..d20c741673 100644
--- a/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs
+++ b/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs
@@ -24,7 +24,7 @@ namespace Content.Client.Administration.UI.BanPanel;
[GenerateTypedNameReferences]
public sealed partial class BanPanel : DefaultWindow
{
- public event Action? BanSubmitted;
+ public event Action? BanSubmitted;
public event Action? PlayerChanged;
private string? PlayerUsername { get; set; }
private (IPAddress, int)? IpAddress { get; set; }
@@ -37,8 +37,8 @@ public sealed partial class BanPanel : DefaultWindow
// This is less efficient than just holding a reference to the root control and enumerating children, but you
// have to know how the controls are nested, which makes the code more complicated.
// Role group name -> the role buttons themselves.
- private readonly Dictionary> _roleCheckboxes = new();
- private readonly ISawmill _banpanelSawmill;
+ private readonly Dictionary> _roleCheckboxes = new();
+ private readonly ISawmill _banPanelSawmill;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
@@ -79,7 +79,7 @@ public sealed partial class BanPanel : DefaultWindow
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
- _banpanelSawmill = _logManager.GetSawmill("admin.banpanel");
+ _banPanelSawmill = _logManager.GetSawmill("admin.banpanel");
PlayerList.OnSelectionChanged += OnPlayerSelectionChanged;
PlayerNameLine.OnFocusExit += _ => OnPlayerNameChanged();
PlayerCheckbox.OnPressed += _ =>
@@ -110,7 +110,7 @@ public sealed partial class BanPanel : DefaultWindow
TypeOption.SelectId(args.Id);
OnTypeChanged();
};
- LastConnCheckbox.OnPressed += args =>
+ LastConnCheckbox.OnPressed += _ =>
{
IpLine.ModulateSelfOverride = null;
HwidLine.ModulateSelfOverride = null;
@@ -164,7 +164,7 @@ public sealed partial class BanPanel : DefaultWindow
var antagRoles = _protoMan.EnumeratePrototypes()
.OrderBy(x => x.ID);
- CreateRoleGroup("Antagonist", Color.Red, antagRoles);
+ CreateRoleGroup(AntagPrototype.GroupName, AntagPrototype.GroupColor, antagRoles);
}
///
@@ -236,14 +236,14 @@ public sealed partial class BanPanel : DefaultWindow
{
foreach (var role in _roleCheckboxes[groupName])
{
- role.Pressed = args.Pressed;
+ role.Item1.Pressed = args.Pressed;
}
if (args.Pressed)
{
if (!Enum.TryParse(_cfg.GetCVar(CCVars.DepartmentBanDefaultSeverity), true, out NoteSeverity newSeverity))
{
- _banpanelSawmill
+ _banPanelSawmill
.Warning("Departmental role ban severity could not be parsed from config!");
return;
}
@@ -255,14 +255,14 @@ public sealed partial class BanPanel : DefaultWindow
{
foreach (var button in roleButtons)
{
- if (button.Pressed)
+ if (button.Item1.Pressed)
return;
}
}
if (!Enum.TryParse(_cfg.GetCVar(CCVars.RoleBanDefaultSeverity), true, out NoteSeverity newSeverity))
{
- _banpanelSawmill
+ _banPanelSawmill
.Warning("Role ban severity could not be parsed from config!");
return;
}
@@ -294,7 +294,7 @@ public sealed partial class BanPanel : DefaultWindow
}
///
- /// Adds a checkbutton specifically for one "role" in a "group"
+ /// Adds a check button specifically for one "role" in a "group"
/// E.g. it would add the Chief Medical Officer "role" into the "Medical" group.
///
private void AddRoleCheckbox(string group, string role, GridContainer roleGroupInnerContainer, Button roleGroupCheckbox)
@@ -302,22 +302,36 @@ public sealed partial class BanPanel : DefaultWindow
var roleCheckboxContainer = new BoxContainer();
var roleCheckButton = new Button
{
- Name = $"{role}RoleCheckbox",
+ Name = role,
Text = role,
ToggleMode = true,
};
roleCheckButton.OnToggled += args =>
{
// Checks the role group checkbox if all the children are pressed
- if (args.Pressed && _roleCheckboxes[group].All(e => e.Pressed))
+ if (args.Pressed && _roleCheckboxes[group].All(e => e.Item1.Pressed))
roleGroupCheckbox.Pressed = args.Pressed;
else
roleGroupCheckbox.Pressed = false;
};
+ IPrototype rolePrototype;
+
+ if (_protoMan.TryIndex(role, out var jobPrototype))
+ rolePrototype = jobPrototype;
+ else if (_protoMan.TryIndex(role, out var antagPrototype))
+ rolePrototype = antagPrototype;
+ else
+ {
+ _banPanelSawmill.Error($"Adding a role checkbox for role {role}: role is not a JobPrototype or AntagPrototype.");
+
+ return;
+ }
+
// 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 (jobPrototype is not null && _protoMan.TryIndex(jobPrototype.Icon, out var iconProto))
{
var jobIconTexture = new TextureRect
{
@@ -334,7 +348,7 @@ public sealed partial class BanPanel : DefaultWindow
roleGroupInnerContainer.AddChild(roleCheckboxContainer);
_roleCheckboxes.TryAdd(group, []);
- _roleCheckboxes[group].Add(roleCheckButton);
+ _roleCheckboxes[group].Add((roleCheckButton, rolePrototype));
}
public void UpdateBanFlag(bool newFlag)
@@ -487,7 +501,7 @@ public sealed partial class BanPanel : DefaultWindow
newSeverity = serverSeverity;
else
{
- _banpanelSawmill
+ _banPanelSawmill
.Warning("Server ban severity could not be parsed from config!");
}
@@ -500,7 +514,7 @@ public sealed partial class BanPanel : DefaultWindow
}
else
{
- _banpanelSawmill
+ _banPanelSawmill
.Warning("Role ban severity could not be parsed from config!");
}
break;
@@ -545,34 +559,51 @@ public sealed partial class BanPanel : DefaultWindow
private void SubmitButtonOnOnPressed(BaseButton.ButtonEventArgs obj)
{
- string[]? roles = null;
+ ProtoId[]? jobs = null;
+ ProtoId[]? antags = null;
+
if (TypeOption.SelectedId == (int) Types.Role)
{
- var rolesList = new List();
+ var jobList = new List>();
+ var antagList = new List>();
+
if (_roleCheckboxes.Count == 0)
throw new DebugAssertException("RoleCheckboxes was empty");
foreach (var button in _roleCheckboxes.Values.SelectMany(departmentButtons => departmentButtons))
{
- if (button is { Pressed: true, Text: not null })
+ if (button.Item1 is { Pressed: true, Name: not null })
{
- rolesList.Add(button.Text);
+ switch (button.Item2)
+ {
+ case JobPrototype:
+ jobList.Add(button.Item2.ID);
+
+ break;
+ case AntagPrototype:
+ antagList.Add(button.Item2.ID);
+
+ break;
+ }
}
}
- if (rolesList.Count == 0)
+ if (jobList.Count + antagList.Count == 0)
{
Tabs.CurrentTab = (int) TabNumbers.Roles;
+
return;
}
- roles = rolesList.ToArray();
+ jobs = jobList.ToArray();
+ antags = antagList.ToArray();
}
if (TypeOption.SelectedId == (int) Types.None)
{
TypeOption.ModulateSelfOverride = Color.Red;
Tabs.CurrentTab = (int) TabNumbers.BasicInfo;
+
return;
}
@@ -584,6 +615,7 @@ public sealed partial class BanPanel : DefaultWindow
ReasonTextEdit.GrabKeyboardFocus();
ReasonTextEdit.ModulateSelfOverride = Color.Red;
ReasonTextEdit.OnKeyBindDown += ResetTextEditor;
+
return;
}
@@ -592,6 +624,7 @@ public sealed partial class BanPanel : DefaultWindow
ButtonResetOn = _gameTiming.CurTime.Add(TimeSpan.FromSeconds(3));
SubmitButton.ModulateSelfOverride = Color.Red;
SubmitButton.Text = Loc.GetString("ban-panel-confirm");
+
return;
}
@@ -600,7 +633,22 @@ public sealed partial class BanPanel : DefaultWindow
var useLastHwid = HwidCheckbox.Pressed && LastConnCheckbox.Pressed && Hwid is null;
var severity = (NoteSeverity) SeverityOption.SelectedId;
var erase = EraseCheckbox.Pressed;
- BanSubmitted?.Invoke(player, IpAddress, useLastIp, Hwid, useLastHwid, (uint) (TimeEntered * Multiplier), reason, severity, roles, erase);
+
+ var ban = new Ban(
+ player,
+ IpAddress,
+ useLastIp,
+ Hwid,
+ useLastHwid,
+ (uint)(TimeEntered * Multiplier),
+ reason,
+ severity,
+ jobs,
+ antags,
+ erase
+ );
+
+ BanSubmitted?.Invoke(ban);
}
protected override void FrameUpdate(FrameEventArgs args)
diff --git a/Content.Client/Administration/UI/BanPanel/BanPanelEui.cs b/Content.Client/Administration/UI/BanPanel/BanPanelEui.cs
index 940a55e010..ac17576361 100644
--- a/Content.Client/Administration/UI/BanPanel/BanPanelEui.cs
+++ b/Content.Client/Administration/UI/BanPanel/BanPanelEui.cs
@@ -14,8 +14,7 @@ public sealed class BanPanelEui : BaseEui
{
BanPanel = new BanPanel();
BanPanel.OnClose += () => SendMessage(new CloseEuiMessage());
- BanPanel.BanSubmitted += (player, ip, useLastIp, hwid, useLastHwid, minutes, reason, severity, roles, erase)
- => SendMessage(new BanPanelEuiStateMsg.CreateBanRequest(player, ip, useLastIp, hwid, useLastHwid, minutes, reason, severity, roles, erase));
+ BanPanel.BanSubmitted += ban => SendMessage(new BanPanelEuiStateMsg.CreateBanRequest(ban));
BanPanel.PlayerChanged += player => SendMessage(new BanPanelEuiStateMsg.GetPlayerInfoRequest(player));
}
diff --git a/Content.Client/Administration/UI/ManageSolutions/EditSolutionsWindow.xaml.cs b/Content.Client/Administration/UI/ManageSolutions/EditSolutionsWindow.xaml.cs
index a6b61a4393..89016fdf41 100644
--- a/Content.Client/Administration/UI/ManageSolutions/EditSolutionsWindow.xaml.cs
+++ b/Content.Client/Administration/UI/ManageSolutions/EditSolutionsWindow.xaml.cs
@@ -67,7 +67,7 @@ namespace Content.Client.Administration.UI.ManageSolutions
///
public void UpdateReagents()
{
- ReagentList.DisposeAllChildren();
+ ReagentList.RemoveAllChildren();
if (_selectedSolution == null || _solutions == null)
return;
@@ -92,7 +92,7 @@ namespace Content.Client.Administration.UI.ManageSolutions
/// The selected solution.
private void UpdateVolumeBox(Solution solution)
{
- VolumeBox.DisposeAllChildren();
+ VolumeBox.RemoveAllChildren();
var volumeLabel = new Label();
volumeLabel.HorizontalExpand = true;
@@ -131,7 +131,7 @@ namespace Content.Client.Administration.UI.ManageSolutions
/// The selected solution.
private void UpdateThermalBox(Solution solution)
{
- ThermalBox.DisposeAllChildren();
+ ThermalBox.RemoveAllChildren();
var heatCap = solution.GetHeatCapacity(null);
var specificHeatLabel = new Label();
specificHeatLabel.HorizontalExpand = true;
diff --git a/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs b/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs
index ead1d8b00e..97ddc15000 100644
--- a/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs
+++ b/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs
@@ -82,7 +82,11 @@ public sealed partial class AdminNotesLine : BoxContainer
if (Note.UnbannedTime is not null)
{
- ExtraLabel.Text = Loc.GetString("admin-notes-unbanned", ("admin", Note.UnbannedByName ?? "[error]"), ("date", Note.UnbannedTime));
+ ExtraLabel.Text = Loc.GetString(
+ "admin-notes-unbanned",
+ ("admin", Note.UnbannedByName ?? "[error]"),
+ ("date", Note.UnbannedTime.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"))
+ );
ExtraLabel.Visible = true;
}
else if (Note.ExpiryTime is not null)
@@ -139,7 +143,7 @@ public sealed partial class AdminNotesLine : BoxContainer
private string FormatRoleBanMessage()
{
- var banMessage = new StringBuilder($"{Loc.GetString("admin-notes-banned-from")} {string.Join(", ", Note.BannedRoles ?? new []{"unknown"})} ");
+ var banMessage = new StringBuilder($"{Loc.GetString("admin-notes-banned-from")} {string.Join(", ", Note.BannedRoles ?? new[] { "unknown" })} ");
return FormatBanMessageCommon(banMessage);
}
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/Monitor/UI/Widgets/ThresholdBoundControl.xaml.cs b/Content.Client/Atmos/Monitor/UI/Widgets/ThresholdBoundControl.xaml.cs
index 55f7c00898..38c631e630 100644
--- a/Content.Client/Atmos/Monitor/UI/Widgets/ThresholdBoundControl.xaml.cs
+++ b/Content.Client/Atmos/Monitor/UI/Widgets/ThresholdBoundControl.xaml.cs
@@ -30,7 +30,10 @@ public sealed partial class ThresholdBoundControl : BoxContainer
public void SetValue(float value)
{
_value = value;
- CSpinner.Value = ScaledValue;
+ if (!CSpinner.HasKeyboardFocus())
+ {
+ CSpinner.Value = ScaledValue;
+ }
}
public void SetEnabled(bool enabled)
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/Cargo/UI/CargoConsoleMenu.xaml.cs b/Content.Client/Cargo/UI/CargoConsoleMenu.xaml.cs
index 03246cfdfe..624ab36125 100644
--- a/Content.Client/Cargo/UI/CargoConsoleMenu.xaml.cs
+++ b/Content.Client/Cargo/UI/CargoConsoleMenu.xaml.cs
@@ -206,7 +206,7 @@ namespace Content.Client.Cargo.UI
if (!_orderConsoleQuery.TryComp(_owner, out var orderConsole))
return;
- Requests.DisposeAllChildren();
+ Requests.RemoveAllChildren();
foreach (var order in orders)
{
diff --git a/Content.Client/Cargo/UI/CargoShuttleMenu.xaml.cs b/Content.Client/Cargo/UI/CargoShuttleMenu.xaml.cs
index c1ffed0783..970051432b 100644
--- a/Content.Client/Cargo/UI/CargoShuttleMenu.xaml.cs
+++ b/Content.Client/Cargo/UI/CargoShuttleMenu.xaml.cs
@@ -30,7 +30,7 @@ namespace Content.Client.Cargo.UI
public void SetOrders(SpriteSystem sprites, IPrototypeManager protoManager, List orders)
{
- Orders.DisposeAllChildren();
+ Orders.RemoveAllChildren();
foreach (var order in orders)
{
diff --git a/Content.Client/CartridgeLoader/Cartridges/CrewManifestUiFragment.xaml.cs b/Content.Client/CartridgeLoader/Cartridges/CrewManifestUiFragment.xaml.cs
index 27ddd51815..4daab3d27c 100644
--- a/Content.Client/CartridgeLoader/Cartridges/CrewManifestUiFragment.xaml.cs
+++ b/Content.Client/CartridgeLoader/Cartridges/CrewManifestUiFragment.xaml.cs
@@ -1,4 +1,4 @@
-using Content.Client.CrewManifest.UI;
+using Content.Client.CrewManifest.UI;
using Content.Shared.CrewManifest;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
@@ -21,7 +21,6 @@ public sealed partial class CrewManifestUiFragment : BoxContainer
public void UpdateState(string stationName, CrewManifestEntries? entries)
{
- CrewManifestListing.DisposeAllChildren();
CrewManifestListing.RemoveAllChildren();
StationNameContainer.Visible = entries != null;
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/Changelog/ChangelogWindow.xaml.cs b/Content.Client/Changelog/ChangelogWindow.xaml.cs
index d82c34254c..d8f560f151 100644
--- a/Content.Client/Changelog/ChangelogWindow.xaml.cs
+++ b/Content.Client/Changelog/ChangelogWindow.xaml.cs
@@ -55,7 +55,7 @@ namespace Content.Client.Changelog
// Changelog is not kept in memory so load it again.
var changelogs = await _changelog.LoadChangelog();
- Tabs.DisposeAllChildren();
+ Tabs.RemoveAllChildren();
var i = 0;
foreach (var changelog in changelogs)
diff --git a/Content.Client/Chat/TypingIndicator/TypingIndicatorVisualizerSystem.cs b/Content.Client/Chat/TypingIndicator/TypingIndicatorVisualizerSystem.cs
index 5e9cf91f59..1c7a378c95 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/ContextMenu/UI/ContextMenuUIController.cs b/Content.Client/ContextMenu/UI/ContextMenuUIController.cs
index 1b83f5ed03..ca173ff1e1 100644
--- a/Content.Client/ContextMenu/UI/ContextMenuUIController.cs
+++ b/Content.Client/ContextMenu/UI/ContextMenuUIController.cs
@@ -95,7 +95,7 @@ namespace Content.Client.ContextMenu.UI
///
public void Close()
{
- RootMenu.MenuBody.DisposeAllChildren();
+ RootMenu.MenuBody.RemoveAllChildren();
CancelOpen?.Cancel();
CancelClose?.Cancel();
OnContextClosed?.Invoke();
diff --git a/Content.Client/ContextMenu/UI/EntityMenuUIController.cs b/Content.Client/ContextMenu/UI/EntityMenuUIController.cs
index e0a88300db..1855911ca4 100644
--- a/Content.Client/ContextMenu/UI/EntityMenuUIController.cs
+++ b/Content.Client/ContextMenu/UI/EntityMenuUIController.cs
@@ -293,7 +293,7 @@ namespace Content.Client.ContextMenu.UI
var element = new EntityMenuElement(entity);
element.SubMenu = new ContextMenuPopup(_context, element);
element.SubMenu.OnPopupOpen += () => _verb.OpenVerbMenu(entity, popup: element.SubMenu);
- element.SubMenu.OnPopupHide += element.SubMenu.MenuBody.DisposeAllChildren;
+ element.SubMenu.OnPopupHide += element.SubMenu.MenuBody.RemoveAllChildren;
_context.AddElement(menu, element);
Elements.TryAdd(entity, element);
}
diff --git a/Content.Client/Corvax/TTS/HumanoidProfileEditor.TTS.cs b/Content.Client/Corvax/TTS/HumanoidProfileEditor.TTS.cs
deleted file mode 100644
index 1d73b08e18..0000000000
--- a/Content.Client/Corvax/TTS/HumanoidProfileEditor.TTS.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-using System.Linq;
-using Content.Client.Corvax.TTS;
-using Content.Client.Lobby;
-using Content.Corvax.Interfaces.Shared;
-using Content.Shared.Corvax.TTS;
-using Content.Shared.Preferences;
-
-namespace Content.Client.Lobby.UI;
-
-public sealed partial class HumanoidProfileEditor
-{
- private ISharedSponsorsManager? _sponsorsMgr;
- private List _voiceList = new();
-
- private void InitializeVoice()
- {
- _voiceList = _prototypeManager
- .EnumeratePrototypes()
- .Where(o => o.RoundStart)
- .OrderBy(o => Loc.GetString(o.Name))
- .ToList();
-
- VoiceButton.OnItemSelected += args =>
- {
- VoiceButton.SelectId(args.Id);
- SetVoice(_voiceList[args.Id].ID);
- };
-
- VoicePlayButton.OnPressed += _ => PlayPreviewTTS();
-
- IoCManager.Instance!.TryResolveType(out _sponsorsMgr);
- }
-
- private void UpdateTTSVoicesControls()
- {
- if (Profile is null)
- return;
-
- VoiceButton.Clear();
-
- var firstVoiceChoiceId = 1;
- for (var i = 0; i < _voiceList.Count; i++)
- {
- var voice = _voiceList[i];
- if (!HumanoidCharacterProfile.CanHaveVoice(voice, Profile.Sex))
- continue;
-
- var name = Loc.GetString(voice.Name);
- VoiceButton.AddItem(name, i);
-
- if (firstVoiceChoiceId == 1)
- firstVoiceChoiceId = i;
-
- if (_sponsorsMgr is null)
- continue;
- if (voice.SponsorOnly && _sponsorsMgr != null &&
- !_sponsorsMgr.GetClientPrototypes().Contains(voice.ID))
- {
- VoiceButton.SetItemDisabled(VoiceButton.GetIdx(i), true);
- }
- }
-
- var voiceChoiceId = _voiceList.FindIndex(x => x.ID == Profile.Voice);
- if (!VoiceButton.TrySelectId(voiceChoiceId) &&
- VoiceButton.TrySelectId(firstVoiceChoiceId))
- {
- SetVoice(_voiceList[firstVoiceChoiceId].ID);
- }
- }
-
- private void PlayPreviewTTS()
- {
- if (Profile is null)
- return;
-
- _entManager.System().RequestPreviewTTS(Profile.Voice, CTTSPreview?.Text ?? String.Empty); //WL-PreviewTTSEdit
- }
-}
diff --git a/Content.Client/Corvax/TTS/TTSTab.xaml b/Content.Client/Corvax/TTS/TTSTab.xaml
new file mode 100644
index 0000000000..2a308ef253
--- /dev/null
+++ b/Content.Client/Corvax/TTS/TTSTab.xaml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Corvax/TTS/TTSTab.xaml.cs b/Content.Client/Corvax/TTS/TTSTab.xaml.cs
new file mode 100644
index 0000000000..21b4d81de8
--- /dev/null
+++ b/Content.Client/Corvax/TTS/TTSTab.xaml.cs
@@ -0,0 +1,194 @@
+using System.Linq;
+using Content.Corvax.Interfaces.Shared;
+using Content.Shared.Corvax.TTS;
+using Content.Shared.Preferences;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Prototypes;
+using Content.Client.Stylesheets;
+using Content.Shared.Humanoid;
+using System.Text.RegularExpressions;
+
+namespace Content.Client.Corvax.TTS;
+
+[GenerateTypedNameReferences]
+public sealed partial class TTSTab : Control
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+
+ public event Action? OnVoiceSelected;
+ public event Action? OnPreviewRequested;
+
+ private List _allVoices = new();
+ private List _filteredVoices = new();
+ private Dictionary> _categorizedVoices = new();
+ private string? _selectedVoiceId;
+
+ private static readonly Regex CategoryRegex = new Regex(@"^(.*?)\s*\(([^)]+)\)\s*$", RegexOptions.Compiled);
+
+ public TTSTab()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ LoadVoices();
+ SearchEdit.OnTextChanged += OnSearchChanged;
+ }
+
+ private void LoadVoices()
+ {
+ foreach (var voice in _allVoices)
+ {
+ var name = Loc.GetString(voice.Name);
+ var category = Loc.GetString("humanoid-profile-editor-voice-other");
+
+ var match = CategoryRegex.Match(name);
+ if (match.Success)
+ {
+ category = match.Groups[2].Value.Trim();
+ }
+
+ if (!_categorizedVoices.ContainsKey(category))
+ _categorizedVoices[category] = new List();
+
+ _categorizedVoices[category].Add(voice);
+ }
+
+ CategoriesContainer.RemoveAllChildren();
+
+ foreach (var category in _categorizedVoices.Keys.OrderBy(k => k))
+ {
+ var button = new Button
+ {
+ Text = category,
+ ToolTip = Loc.GetString("humanoid-profile-editor-voice-category-tooltip", ("category", category)),
+ HorizontalExpand = true,
+ };
+
+ button.OnPressed += _ =>
+ {
+ SearchEdit.Text = category;
+ UpdateResults();
+ };
+
+ CategoriesContainer.AddChild(button);
+ }
+
+ UpdateResults();
+ }
+
+ private void OnSearchChanged(LineEdit.LineEditEventArgs args)
+ {
+ UpdateResults();
+ }
+
+ private void UpdateResults()
+ {
+ VoicesGrid.RemoveAllChildren();
+ _filteredVoices.Clear();
+
+ var searchText = SearchEdit.Text.ToLowerInvariant();
+
+ foreach (var voice in _allVoices)
+ {
+ var name = Loc.GetString(voice.Name).ToLowerInvariant();
+
+ if (string.IsNullOrEmpty(searchText) ||
+ name.Contains(searchText) ||
+ voice.ID.ToLowerInvariant().Contains(searchText))
+ {
+ _filteredVoices.Add(voice);
+ }
+ }
+
+ foreach (var voice in _filteredVoices)
+ {
+ var displayName = Loc.GetString(voice.Name);
+ var canSelectVoice = CanUseVoice(voice);
+
+ var voiceContainer = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ HorizontalExpand = true,
+ VerticalAlignment = VAlignment.Center
+ };
+
+ var selectButton = new Button
+ {
+ Text = displayName,
+ ToolTip = canSelectVoice ? voice.ID : Loc.GetString("humanoid-profile-editor-voice-tooltip-sponsoronly"),
+ HorizontalExpand = true,
+ Disabled = !canSelectVoice,
+ StyleClasses = { StyleNano.ButtonOpenRight }
+ };
+
+ if (voice.ID == _selectedVoiceId)
+ {
+ selectButton.AddStyleClass(StyleBase.ButtonCaution);
+ }
+
+ selectButton.OnPressed += _ =>
+ {
+ if (canSelectVoice)
+ {
+ OnVoiceSelected?.Invoke(voice.ID);
+ }
+ };
+
+ var previewButton = new Button
+ {
+ Text = Loc.GetString("humanoid-profile-editor-voice-play"),
+ MinWidth = 30,
+ ToolTip = Loc.GetString("humanoid-profile-editor-voice-tooltip-play"),
+ StyleClasses = { StyleNano.ButtonOpenLeft }
+ };
+
+ previewButton.OnPressed += _ =>
+ {
+ OnPreviewRequested?.Invoke(voice.ID);
+ };
+
+ voiceContainer.AddChild(selectButton);
+ voiceContainer.AddChild(previewButton);
+
+ VoicesGrid.AddChild(voiceContainer);
+ }
+
+ ResultsLabel.Text = Loc.GetString("humanoid-profile-editor-voice-match",
+ ("filtered", _filteredVoices.Count), ("all", _allVoices.Count));
+ }
+
+ private bool CanUseVoice(TTSVoicePrototype voice)
+ {
+ if (!voice.SponsorOnly)
+ return true;
+
+ var sponsorsManager = IoCManager.Resolve();
+ return sponsorsManager?.GetClientPrototypes().Contains(voice.ID) == true;
+ }
+
+ public void UpdateControls(HumanoidCharacterProfile? profile, Sex sex)
+ {
+ if (profile == null)
+ return;
+
+ _selectedVoiceId = profile.Voice;
+
+ _allVoices = _prototypeManager
+ .EnumeratePrototypes()
+ .Where(o => o.RoundStart && HumanoidCharacterProfile.CanHaveVoice(o, sex))
+ .OrderBy(o => Loc.GetString(o.Name))
+ .ToList();
+
+ _categorizedVoices.Clear();
+ LoadVoices();
+ }
+
+ public void SetSelectedVoice(string voiceId)
+ {
+ _selectedVoiceId = voiceId;
+ UpdateResults();
+ }
+}
diff --git a/Content.Client/Crayon/UI/CrayonWindow.xaml.cs b/Content.Client/Crayon/UI/CrayonWindow.xaml.cs
index 88475562c6..f1ac5a79cb 100644
--- a/Content.Client/Crayon/UI/CrayonWindow.xaml.cs
+++ b/Content.Client/Crayon/UI/CrayonWindow.xaml.cs
@@ -53,7 +53,7 @@ namespace Content.Client.Crayon.UI
private void RefreshList()
{
// Clear
- Grids.DisposeAllChildren();
+ Grids.RemoveAllChildren();
if (_decals == null || _allDecals == null)
return;
diff --git a/Content.Client/CrewManifest/CrewManifestUi.xaml.cs b/Content.Client/CrewManifest/CrewManifestUi.xaml.cs
index f07e54eb65..3c13681b97 100644
--- a/Content.Client/CrewManifest/CrewManifestUi.xaml.cs
+++ b/Content.Client/CrewManifest/CrewManifestUi.xaml.cs
@@ -18,7 +18,6 @@ public sealed partial class CrewManifestUi : DefaultWindow
public void Populate(string name, CrewManifestEntries? entries)
{
- CrewManifestListing.DisposeAllChildren();
CrewManifestListing.RemoveAllChildren();
StationNameContainer.Visible = entries != null;
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..ae9c7eda78 100644
--- a/Content.Client/Doors/DoorSystem.cs
+++ b/Content.Client/Doors/DoorSystem.cs
@@ -31,7 +31,7 @@ public sealed class DoorSystem : SharedDoorSystem
comp.OpeningAnimation = new Animation
{
- Length = TimeSpan.FromSeconds(comp.OpeningAnimationTime),
+ Length = comp.OpeningAnimationTime,
AnimationTracks =
{
new AnimationTrackSpriteFlick
@@ -47,7 +47,7 @@ public sealed class DoorSystem : SharedDoorSystem
comp.ClosingAnimation = new Animation
{
- Length = TimeSpan.FromSeconds(comp.ClosingAnimationTime),
+ Length = comp.ClosingAnimationTime,
AnimationTracks =
{
new AnimationTrackSpriteFlick
@@ -63,7 +63,7 @@ public sealed class DoorSystem : SharedDoorSystem
comp.EmaggingAnimation = new Animation
{
- Length = TimeSpan.FromSeconds(comp.EmaggingAnimationTime),
+ Length = comp.EmaggingAnimationTime,
AnimationTracks =
{
new AnimationTrackSpriteFlick
@@ -116,14 +116,14 @@ public sealed class DoorSystem : SharedDoorSystem
return;
case DoorState.Opening:
- if (entity.Comp.OpeningAnimationTime == 0.0)
+ if (entity.Comp.OpeningAnimationTime == TimeSpan.Zero)
return;
_animationSystem.Play(entity, (Animation)entity.Comp.OpeningAnimation, DoorComponent.AnimationKey);
return;
case DoorState.Closing:
- if (entity.Comp.ClosingAnimationTime == 0.0 || entity.Comp.CurrentlyCrushing.Count != 0)
+ if (entity.Comp.ClosingAnimationTime == TimeSpan.Zero || entity.Comp.CurrentlyCrushing.Count != 0)
return;
_animationSystem.Play(entity, (Animation)entity.Comp.ClosingAnimation, DoorComponent.AnimationKey);
@@ -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/Gateway/UI/GatewayWindow.xaml.cs b/Content.Client/Gateway/UI/GatewayWindow.xaml.cs
index 1c779b2b35..9fb7c339d3 100644
--- a/Content.Client/Gateway/UI/GatewayWindow.xaml.cs
+++ b/Content.Client/Gateway/UI/GatewayWindow.xaml.cs
@@ -72,7 +72,7 @@ public sealed partial class GatewayWindow : FancyWindow,
_isUnlockPending = _nextUnlock >= _timing.CurTime;
_isCooldownPending = _nextReady >= _timing.CurTime;
- Container.DisposeAllChildren();
+ Container.RemoveAllChildren();
if (_destinations.Count == 0)
{
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/Graphics/OverlayResourceCache.cs b/Content.Client/Graphics/OverlayResourceCache.cs
new file mode 100644
index 0000000000..ef7ebfd2b7
--- /dev/null
+++ b/Content.Client/Graphics/OverlayResourceCache.cs
@@ -0,0 +1,90 @@
+using Robust.Client.Graphics;
+
+namespace Content.Client.Graphics;
+
+///
+/// A cache for s to store per-viewport render resources, such as render targets.
+///
+/// The type of data stored in the cache.
+public sealed class OverlayResourceCache : IDisposable where T : class, IDisposable
+{
+ private readonly Dictionary _cache = new();
+
+ ///
+ /// Get the data for a specific viewport, creating a new entry if necessary.
+ ///
+ ///
+ /// The cached data may be cleared at any time if gets invoked.
+ ///
+ /// The viewport for which to retrieve cached data.
+ /// A delegate used to create the cached data, if necessary.
+ public T GetForViewport(IClydeViewport viewport, Func factory)
+ {
+ return GetForViewport(viewport, out _, factory);
+ }
+
+ ///
+ /// Get the data for a specific viewport, creating a new entry if necessary.
+ ///
+ ///
+ /// The cached data may be cleared at any time if gets invoked.
+ ///
+ /// The viewport for which to retrieve cached data.
+ /// True if the data was pulled from cache, false if it was created anew.
+ /// A delegate used to create the cached data, if necessary.
+ public T GetForViewport(IClydeViewport viewport, out bool wasCached, Func factory)
+ {
+ if (_cache.TryGetValue(viewport.Id, out var entry))
+ {
+ wasCached = true;
+ return entry.Data;
+ }
+
+ wasCached = false;
+
+ entry = new CacheEntry
+ {
+ Data = factory(viewport),
+ Viewport = new WeakReference(viewport),
+ };
+ _cache.Add(viewport.Id, entry);
+
+ viewport.ClearCachedResources += ViewportOnClearCachedResources;
+
+ return entry.Data;
+ }
+
+ private void ViewportOnClearCachedResources(ClearCachedViewportResourcesEvent ev)
+ {
+ if (!_cache.Remove(ev.ViewportId, out var entry))
+ {
+ // I think this could theoretically happen if you manually dispose the cache *after* a leaked viewport got
+ // GC'd, but before its ClearCachedResources got invoked.
+ return;
+ }
+
+ entry.Data.Dispose();
+
+ if (ev.Viewport != null)
+ ev.Viewport.ClearCachedResources -= ViewportOnClearCachedResources;
+ }
+
+ public void Dispose()
+ {
+ foreach (var entry in _cache)
+ {
+ if (entry.Value.Viewport.TryGetTarget(out var viewport))
+ viewport.ClearCachedResources -= ViewportOnClearCachedResources;
+
+ entry.Value.Data.Dispose();
+ }
+
+ _cache.Clear();
+ }
+
+ private struct CacheEntry
+ {
+ public T Data;
+ public WeakReference Viewport;
+ }
+}
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/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
index fd3615d59f..225619b031 100644
--- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
+++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
@@ -116,7 +116,7 @@ namespace Content.Client.HealthAnalyzer.UI
AlertsContainer.Visible = showAlerts;
if (showAlerts)
- AlertsContainer.DisposeAllChildren();
+ AlertsContainer.RemoveAllChildren();
if (msg.Unrevivable == true)
AlertsContainer.AddChild(new RichTextLabel
diff --git a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs
index 115670fdf4..4793c8e76e 100644
--- a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs
+++ b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs
@@ -291,25 +291,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);
}
}
@@ -348,9 +349,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)
@@ -361,9 +360,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}";
@@ -377,26 +374,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/Humanoid/MarkingPicker.xaml.cs b/Content.Client/Humanoid/MarkingPicker.xaml.cs
index ad6671511a..7a591a46aa 100644
--- a/Content.Client/Humanoid/MarkingPicker.xaml.cs
+++ b/Content.Client/Humanoid/MarkingPicker.xaml.cs
@@ -416,7 +416,7 @@ public sealed partial class MarkingPicker : Control
var stateNames = GetMarkingStateNames(prototype);
_currentMarkingColors.Clear();
- CMarkingColors.DisposeAllChildren();
+ CMarkingColors.RemoveAllChildren();
List colorSliders = new();
for (int i = 0; i < prototype.Sprites.Count; i++)
{
diff --git a/Content.Client/Humanoid/SingleMarkingPicker.xaml.cs b/Content.Client/Humanoid/SingleMarkingPicker.xaml.cs
index ff4dfb973b..ae1cf3db6f 100644
--- a/Content.Client/Humanoid/SingleMarkingPicker.xaml.cs
+++ b/Content.Client/Humanoid/SingleMarkingPicker.xaml.cs
@@ -228,7 +228,6 @@ public sealed partial class SingleMarkingPicker : BoxContainer
var marking = _markings[Slot];
- ColorSelectorContainer.DisposeAllChildren();
ColorSelectorContainer.RemoveAllChildren();
if (marking.MarkingColors.Count != proto.Sprites.Count)
diff --git a/Content.Client/IdentityManagement/IdentitySystem.cs b/Content.Client/IdentityManagement/IdentitySystem.cs
deleted file mode 100644
index 15d4ee20e9..0000000000
--- a/Content.Client/IdentityManagement/IdentitySystem.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-using Content.Shared.IdentityManagement;
-
-namespace Content.Client.IdentityManagement;
-
-public sealed class IdentitySystem : SharedIdentitySystem
-{
-}
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/Light/AfterLightTargetOverlay.cs b/Content.Client/Light/AfterLightTargetOverlay.cs
index 7856fd4ded..8f19ce922d 100644
--- a/Content.Client/Light/AfterLightTargetOverlay.cs
+++ b/Content.Client/Light/AfterLightTargetOverlay.cs
@@ -30,6 +30,7 @@ public sealed class AfterLightTargetOverlay : Overlay
return;
var lightOverlay = _overlay.GetOverlay();
+ var lightRes = lightOverlay.GetCachedForViewport(args.Viewport);
var bounds = args.WorldBounds;
// at 1-1 render scale it's mostly fine but at 4x4 it's way too fkn big
@@ -38,7 +39,7 @@ public sealed class AfterLightTargetOverlay : Overlay
var localMatrix =
viewport.LightRenderTarget.GetWorldToLocalMatrix(viewport.Eye, newScale);
- var diff = (lightOverlay.EnlargedLightTarget.Size - viewport.LightRenderTarget.Size);
+ var diff = (lightRes.EnlargedLightTarget.Size - viewport.LightRenderTarget.Size);
var halfDiff = diff / 2;
// Pixels -> Metres -> Half distance.
@@ -53,7 +54,7 @@ public sealed class AfterLightTargetOverlay : Overlay
viewport.LightRenderTarget.Size.Y + halfDiff.Y);
worldHandle.SetTransform(localMatrix);
- worldHandle.DrawTextureRectRegion(lightOverlay.EnlargedLightTarget.Texture, bounds, subRegion: subRegion);
+ worldHandle.DrawTextureRectRegion(lightRes.EnlargedLightTarget.Texture, bounds, subRegion: subRegion);
}, Color.Transparent);
}
}
diff --git a/Content.Client/Light/AmbientOcclusionOverlay.cs b/Content.Client/Light/AmbientOcclusionOverlay.cs
index 4caf654494..aa8c3b52a1 100644
--- a/Content.Client/Light/AmbientOcclusionOverlay.cs
+++ b/Content.Client/Light/AmbientOcclusionOverlay.cs
@@ -1,4 +1,5 @@
using System.Numerics;
+using Content.Client.Graphics;
using Content.Shared.CCVar;
using Content.Shared.Maps;
using Robust.Client.Graphics;
@@ -27,11 +28,7 @@ public sealed class AmbientOcclusionOverlay : Overlay
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowEntities;
- private IRenderTexture? _aoTarget;
- private IRenderTexture? _aoBlurBuffer;
-
- // Couldn't figure out a way to avoid this so if you can then please do.
- private IRenderTexture? _aoStencilTarget;
+ private readonly OverlayResourceCache _resources = new ();
public AmbientOcclusionOverlay()
{
@@ -69,30 +66,32 @@ public sealed class AmbientOcclusionOverlay : Overlay
var turfSystem = _entManager.System();
var invMatrix = args.Viewport.GetWorldToLocalMatrix();
- if (_aoTarget?.Texture.Size != target.Size)
+ var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
+
+ if (res.AOTarget?.Texture.Size != target.Size)
{
- _aoTarget?.Dispose();
- _aoTarget = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-target");
+ res.AOTarget?.Dispose();
+ res.AOTarget = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-target");
}
- if (_aoBlurBuffer?.Texture.Size != target.Size)
+ if (res.AOBlurBuffer?.Texture.Size != target.Size)
{
- _aoBlurBuffer?.Dispose();
- _aoBlurBuffer = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-blur-target");
+ res.AOBlurBuffer?.Dispose();
+ res.AOBlurBuffer = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-blur-target");
}
- if (_aoStencilTarget?.Texture.Size != target.Size)
+ if (res.AOStencilTarget?.Texture.Size != target.Size)
{
- _aoStencilTarget?.Dispose();
- _aoStencilTarget = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-stencil-target");
+ res.AOStencilTarget?.Dispose();
+ res.AOStencilTarget = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-stencil-target");
}
// Draw the texture data to the texture.
- args.WorldHandle.RenderInRenderTarget(_aoTarget,
+ args.WorldHandle.RenderInRenderTarget(res.AOTarget,
() =>
{
worldHandle.UseShader(_proto.Index(UnshadedShader).Instance());
- var invMatrix = _aoTarget.GetWorldToLocalMatrix(viewport.Eye!, scale);
+ var invMatrix = res.AOTarget.GetWorldToLocalMatrix(viewport.Eye!, scale);
foreach (var entry in query.QueryAabb(mapId, worldBounds))
{
@@ -106,11 +105,11 @@ public sealed class AmbientOcclusionOverlay : Overlay
}
}, Color.Transparent);
- _clyde.BlurRenderTarget(viewport, _aoTarget, _aoBlurBuffer, viewport.Eye!, 14f);
+ _clyde.BlurRenderTarget(viewport, res.AOTarget, res.AOBlurBuffer, viewport.Eye!, 14f);
// Need to do stencilling after blur as it will nuke it.
// Draw stencil for the grid so we don't draw in space.
- args.WorldHandle.RenderInRenderTarget(_aoStencilTarget,
+ args.WorldHandle.RenderInRenderTarget(res.AOStencilTarget,
() =>
{
// Don't want lighting affecting it.
@@ -136,13 +135,36 @@ public sealed class AmbientOcclusionOverlay : Overlay
// Draw the stencil texture to depth buffer.
worldHandle.UseShader(_proto.Index(StencilMaskShader).Instance());
- worldHandle.DrawTextureRect(_aoStencilTarget!.Texture, worldBounds);
+ worldHandle.DrawTextureRect(res.AOStencilTarget!.Texture, worldBounds);
// Draw the Blurred AO texture finally.
worldHandle.UseShader(_proto.Index(StencilEqualDrawShader).Instance());
- worldHandle.DrawTextureRect(_aoTarget!.Texture, worldBounds, color);
+ worldHandle.DrawTextureRect(res.AOTarget!.Texture, worldBounds, color);
args.WorldHandle.SetTransform(Matrix3x2.Identity);
args.WorldHandle.UseShader(null);
}
+
+ protected override void DisposeBehavior()
+ {
+ _resources.Dispose();
+
+ base.DisposeBehavior();
+ }
+
+ private sealed class CachedResources : IDisposable
+ {
+ public IRenderTexture? AOTarget;
+ public IRenderTexture? AOBlurBuffer;
+
+ // Couldn't figure out a way to avoid this so if you can then please do.
+ public IRenderTexture? AOStencilTarget;
+
+ public void Dispose()
+ {
+ AOTarget?.Dispose();
+ AOBlurBuffer?.Dispose();
+ AOStencilTarget?.Dispose();
+ }
+ }
}
diff --git a/Content.Client/Light/BeforeLightTargetOverlay.cs b/Content.Client/Light/BeforeLightTargetOverlay.cs
index 8f1bd0e527..6afaebc146 100644
--- a/Content.Client/Light/BeforeLightTargetOverlay.cs
+++ b/Content.Client/Light/BeforeLightTargetOverlay.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using Content.Client.Graphics;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
@@ -13,7 +13,8 @@ public sealed class BeforeLightTargetOverlay : Overlay
[Dependency] private readonly IClyde _clyde = default!;
- public IRenderTexture EnlargedLightTarget = default!;
+ private readonly OverlayResourceCache _resources = new();
+
public Box2Rotated EnlargedBounds;
///
@@ -36,16 +37,42 @@ public sealed class BeforeLightTargetOverlay : Overlay
var size = args.Viewport.LightRenderTarget.Size + (int) (_skirting * EyeManager.PixelsPerMeter);
EnlargedBounds = args.WorldBounds.Enlarged(_skirting / 2f);
+ var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
+
// This just exists to copy the lightrendertarget and write back to it.
- if (EnlargedLightTarget?.Size != size)
+ if (res.EnlargedLightTarget?.Size != size)
{
- EnlargedLightTarget = _clyde
+ res.EnlargedLightTarget = _clyde
.CreateRenderTarget(size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "enlarged-light-copy");
}
- args.WorldHandle.RenderInRenderTarget(EnlargedLightTarget,
+ args.WorldHandle.RenderInRenderTarget(res.EnlargedLightTarget,
() =>
{
}, _clyde.GetClearColor(args.MapUid));
}
+
+ internal CachedResources GetCachedForViewport(IClydeViewport viewport)
+ {
+ return _resources.GetForViewport(viewport,
+ static _ => throw new InvalidOperationException(
+ "Expected BeforeLightTargetOverlay to have created its resources"));
+ }
+
+ protected override void DisposeBehavior()
+ {
+ _resources.Dispose();
+
+ base.DisposeBehavior();
+ }
+
+ internal sealed class CachedResources : IDisposable
+ {
+ public IRenderTexture EnlargedLightTarget = default!;
+
+ public void Dispose()
+ {
+ EnlargedLightTarget?.Dispose();
+ }
+ }
}
diff --git a/Content.Client/Light/LightBlurOverlay.cs b/Content.Client/Light/LightBlurOverlay.cs
index 4ce80946aa..eab4a95c07 100644
--- a/Content.Client/Light/LightBlurOverlay.cs
+++ b/Content.Client/Light/LightBlurOverlay.cs
@@ -1,3 +1,4 @@
+using Content.Client.Graphics;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
@@ -15,7 +16,7 @@ public sealed class LightBlurOverlay : Overlay
public const int ContentZIndex = TileEmissionOverlay.ContentZIndex + 1;
- private IRenderTarget? _blurTarget;
+ private readonly OverlayResourceCache _resources = new();
public LightBlurOverlay()
{
@@ -29,16 +30,36 @@ public sealed class LightBlurOverlay : Overlay
return;
var beforeOverlay = _overlay.GetOverlay();
- var size = beforeOverlay.EnlargedLightTarget.Size;
+ var beforeLightRes = beforeOverlay.GetCachedForViewport(args.Viewport);
+ var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
- if (_blurTarget?.Size != size)
+ var size = beforeLightRes.EnlargedLightTarget.Size;
+
+ if (res.BlurTarget?.Size != size)
{
- _blurTarget = _clyde
+ res.BlurTarget = _clyde
.CreateRenderTarget(size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "enlarged-light-blur");
}
- var target = beforeOverlay.EnlargedLightTarget;
+ var target = beforeLightRes.EnlargedLightTarget;
// Yeah that's all this does keep walkin.
- _clyde.BlurRenderTarget(args.Viewport, target, _blurTarget, args.Viewport.Eye, 14f * 5f);
+ _clyde.BlurRenderTarget(args.Viewport, target, res.BlurTarget, args.Viewport.Eye, 14f * 5f);
+ }
+
+ protected override void DisposeBehavior()
+ {
+ _resources.Dispose();
+
+ base.DisposeBehavior();
+ }
+
+ private sealed class CachedResources : IDisposable
+ {
+ public IRenderTarget? BlurTarget;
+
+ public void Dispose()
+ {
+ BlurTarget?.Dispose();
+ }
}
}
diff --git a/Content.Client/Light/RoofOverlay.cs b/Content.Client/Light/RoofOverlay.cs
index 9be4bfe4c4..01e9bf0961 100644
--- a/Content.Client/Light/RoofOverlay.cs
+++ b/Content.Client/Light/RoofOverlay.cs
@@ -51,8 +51,9 @@ public sealed class RoofOverlay : Overlay
var worldHandle = args.WorldHandle;
var lightoverlay = _overlay.GetOverlay();
+ var lightRes = lightoverlay.GetCachedForViewport(args.Viewport);
var bounds = lightoverlay.EnlargedBounds;
- var target = lightoverlay.EnlargedLightTarget;
+ var target = lightRes.EnlargedLightTarget;
_grids.Clear();
_mapManager.FindGridsIntersecting(args.MapId, bounds, ref _grids, approx: true, includeMap: true);
diff --git a/Content.Client/Light/SunShadowOverlay.cs b/Content.Client/Light/SunShadowOverlay.cs
index f30f4c0409..59ac0a5efb 100644
--- a/Content.Client/Light/SunShadowOverlay.cs
+++ b/Content.Client/Light/SunShadowOverlay.cs
@@ -1,4 +1,5 @@
using System.Numerics;
+using Content.Client.Graphics;
using Content.Shared.Light.Components;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
@@ -24,8 +25,7 @@ public sealed class SunShadowOverlay : Overlay
private readonly HashSet> _shadows = new();
- private IRenderTexture? _blurTarget;
- private IRenderTexture? _target;
+ private readonly OverlayResourceCache _resources = new();
public SunShadowOverlay()
{
@@ -55,16 +55,18 @@ public sealed class SunShadowOverlay : Overlay
var worldBounds = args.WorldBounds;
var targetSize = viewport.LightRenderTarget.Size;
- if (_target?.Size != targetSize)
+ var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
+
+ if (res.Target?.Size != targetSize)
{
- _target = _clyde
+ res.Target = _clyde
.CreateRenderTarget(targetSize,
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
name: "sun-shadow-target");
- if (_blurTarget?.Size != targetSize)
+ if (res.BlurTarget?.Size != targetSize)
{
- _blurTarget = _clyde
+ res.BlurTarget = _clyde
.CreateRenderTarget(targetSize, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "sun-shadow-blur");
}
}
@@ -93,11 +95,11 @@ public sealed class SunShadowOverlay : Overlay
_shadows.Clear();
// Draw shadow polys to stencil
- args.WorldHandle.RenderInRenderTarget(_target,
+ args.WorldHandle.RenderInRenderTarget(res.Target,
() =>
{
var invMatrix =
- _target.GetWorldToLocalMatrix(eye, scale);
+ res.Target.GetWorldToLocalMatrix(eye, scale);
var indices = new Vector2[PhysicsConstants.MaxPolygonVertices * 2];
// Go through shadows in range.
@@ -142,7 +144,7 @@ public sealed class SunShadowOverlay : Overlay
Color.Transparent);
// Slightly blur it just to avoid aliasing issues on the later viewport-wide blur.
- _clyde.BlurRenderTarget(viewport, _target, _blurTarget!, eye, 1f);
+ _clyde.BlurRenderTarget(viewport, res.Target, res.BlurTarget!, eye, 1f);
// Draw stencil (see roofoverlay).
args.WorldHandle.RenderInRenderTarget(viewport.LightRenderTarget,
@@ -155,8 +157,27 @@ public sealed class SunShadowOverlay : Overlay
var maskShader = _protoManager.Index(MixShader).Instance();
worldHandle.UseShader(maskShader);
- worldHandle.DrawTextureRect(_target.Texture, worldBounds, Color.Black.WithAlpha(alpha));
+ worldHandle.DrawTextureRect(res.Target.Texture, worldBounds, Color.Black.WithAlpha(alpha));
}, null);
}
}
+
+ protected override void DisposeBehavior()
+ {
+ _resources.Dispose();
+
+ base.DisposeBehavior();
+ }
+
+ private sealed class CachedResources : IDisposable
+ {
+ public IRenderTexture? BlurTarget;
+ public IRenderTexture? Target;
+
+ public void Dispose()
+ {
+ BlurTarget?.Dispose();
+ Target?.Dispose();
+ }
+ }
}
diff --git a/Content.Client/Light/TileEmissionOverlay.cs b/Content.Client/Light/TileEmissionOverlay.cs
index 2f4a1390ff..2acb0ee609 100644
--- a/Content.Client/Light/TileEmissionOverlay.cs
+++ b/Content.Client/Light/TileEmissionOverlay.cs
@@ -47,7 +47,7 @@ public sealed class TileEmissionOverlay : Overlay
var worldHandle = args.WorldHandle;
var lightoverlay = _overlay.GetOverlay();
var bounds = lightoverlay.EnlargedBounds;
- var target = lightoverlay.EnlargedLightTarget;
+ var target = lightoverlay.GetCachedForViewport(args.Viewport).EnlargedLightTarget;
var viewport = args.Viewport;
_grids.Clear();
_mapManager.FindGridsIntersecting(mapId, bounds, ref _grids, approx: true);
diff --git a/Content.Client/Lobby/LobbyState.cs b/Content.Client/Lobby/LobbyState.cs
index 649fd98eb5..e59821acfa 100644
--- a/Content.Client/Lobby/LobbyState.cs
+++ b/Content.Client/Lobby/LobbyState.cs
@@ -207,10 +207,10 @@ namespace Content.Client.Lobby
else
{
Lobby!.StartTime.Text = string.Empty;
+ Lobby!.ReadyButton.Pressed = _gameTicker.AreWeReady;
Lobby!.ReadyButton.Text = Loc.GetString(Lobby!.ReadyButton.Pressed ? "lobby-state-player-status-ready": "lobby-state-player-status-not-ready");
Lobby!.ReadyButton.ToggleMode = true;
Lobby!.ReadyButton.Disabled = false;
- Lobby!.ReadyButton.Pressed = _gameTicker.AreWeReady;
Lobby!.ObserveButton.Disabled = true;
}
diff --git a/Content.Client/Lobby/LobbyUIController.cs b/Content.Client/Lobby/LobbyUIController.cs
index a2b62fb36a..f9ff546f91 100644
--- a/Content.Client/Lobby/LobbyUIController.cs
+++ b/Content.Client/Lobby/LobbyUIController.cs
@@ -73,6 +73,7 @@ public sealed partial class LobbyUIController : UIController, IOnStateEntered RefreshProfileEditor());
+ _configurationManager.OnValueChanged(CCVars.GameRoleLoadoutTimers, _ => RefreshProfileEditor());
_configurationManager.OnValueChanged(CCVars.GameRoleWhitelist, _ => RefreshProfileEditor());
}
@@ -362,7 +363,7 @@ public sealed partial class LobbyUIController : UIController, IOnStateEntered
@@ -732,6 +723,8 @@ namespace Content.Client.Lobby.UI
RefreshTraits();
+ TabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab")); // Corvax-TTS-Edit
+
#region Markings
TabContainer.SetTabTitle(4, Loc.GetString("humanoid-profile-editor-markings-tab"));
@@ -746,6 +739,7 @@ namespace Content.Client.Lobby.UI
RefreshFlavorText();
RefreshRecords(); // WL-Records
+ RefreshVoiceTab(); // Corvax-TTS
#region Dummy
@@ -856,16 +850,65 @@ namespace Content.Client.Lobby.UI
SetDirty();
}
// WL-Records-End
+ // Corvax-TTS-Start
+ #region Voice
+
+ private void RefreshVoiceTab()
+ {
+ if (!_cfgManager.GetCVar(CCCVars.TTSEnabled))
+ return;
+
+ _ttsTab = new TTSTab();
+ var children = new List();
+ foreach (var child in TabContainer.Children)
+ children.Add(child);
+
+ TabContainer.RemoveAllChildren();
+
+ for (int i = 0; i < children.Count; i++)
+ {
+ if (i == 1) // Set the tab to the 2nd place.
+ {
+ TabContainer.AddChild(_ttsTab);
+ }
+ TabContainer.AddChild(children[i]);
+ }
+
+ TabContainer.SetTabTitle(1, Loc.GetString("humanoid-profile-editor-voice-tab"));
+
+ _ttsTab.OnVoiceSelected += voiceId =>
+ {
+ SetVoice(voiceId);
+ _ttsTab.SetSelectedVoice(voiceId);
+ };
+
+ /*_ttsTab.OnPreviewRequested += voiceId =>
+ {
+ _entManager.System().RequestPreviewTTS(voiceId);
+ };*/
+ }
+
+ private void UpdateTTSVoicesControls()
+ {
+ if (Profile is null || _ttsTab is null)
+ return;
+
+ _ttsTab.UpdateControls(Profile, Profile.Sex);
+ _ttsTab.SetSelectedVoice(Profile.Voice);
+ }
+
+ #endregion
+ // Corvax-TTS-End
///
/// Refreshes traits selector
///
public void RefreshTraits()
{
- TraitsList.DisposeAllChildren();
+ TraitsList.RemoveAllChildren();
var traits = _prototypeManager.EnumeratePrototypes().OrderBy(t => Loc.GetString(t.Name)).ToList();
- TabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab"));
+ // TabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab")); // Corvax-TTS-Edit
if (traits.Count < 1)
{
@@ -1007,7 +1050,7 @@ namespace Content.Client.Lobby.UI
public void RefreshAntags()
{
- AntagList.DisposeAllChildren();
+ AntagList.RemoveAllChildren();
var items = new[]
{
("humanoid-profile-editor-antag-preference-yes-button", 0),
@@ -1037,8 +1080,10 @@ namespace Content.Client.Lobby.UI
selector.Select(Profile?.AntagPreferences.Contains(antag.ID) == true ? 0 : 1);
- var requirements = _entManager.System().GetAntagRequirement(antag);
- if (!_requirements.CheckRoleRequirements(requirements, (HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter, out var reason))
+ if (!_requirements.IsAllowed(
+ antag,
+ (HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter,
+ out var reason))
{
selector.LockRequirements(reason);
Profile = Profile?.WithAntagPreference(antag.ID, false);
@@ -1192,7 +1237,7 @@ namespace Content.Client.Lobby.UI
if (_prototypeManager.HasIndex(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);
@@ -1208,7 +1253,7 @@ namespace Content.Client.Lobby.UI
public void RefreshJobs()
{
JobList.DisposeAllChildren();
-
+ JobList.RemoveAllChildren();
_jobCategories.Clear();
_jobPriorities.Clear();
var firstCategory = true;
@@ -1512,10 +1557,11 @@ namespace Content.Client.Lobby.UI
if (Profile is null) return;
var skin = _prototypeManager.Index(Profile.Species).SkinColoration;
+ var strategy = _prototypeManager.Index(skin).Strategy;
- switch (skin)
+ switch (strategy.InputType)
{
- case HumanoidSkinColor.HumanToned:
+ case SkinColorationStrategyInput.Unary:
{
if (!Skin.Visible)
{
@@ -1523,39 +1569,14 @@ namespace Content.Client.Lobby.UI
RgbSkinColorContainer.Visible = false;
}
- var color = SkinColor.HumanSkinTone((int) Skin.Value);
-
- Markings.CurrentSkinColor = color;
- Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));//
- break;
- }
- case HumanoidSkinColor.Hues:
- {
- if (!RgbSkinColorContainer.Visible)
- {
- Skin.Visible = false;
- RgbSkinColorContainer.Visible = true;
- }
-
- Markings.CurrentSkinColor = _rgbSkinColorSelector.Color;
- Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(_rgbSkinColorSelector.Color));
- break;
- }
- case HumanoidSkinColor.TintedHues:
- {
- if (!RgbSkinColorContainer.Visible)
- {
- Skin.Visible = false;
- RgbSkinColorContainer.Visible = true;
- }
-
- var color = SkinColor.TintedHues(_rgbSkinColorSelector.Color);
+ var color = strategy.FromUnary(Skin.Value);
Markings.CurrentSkinColor = color;
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
+
break;
}
- case HumanoidSkinColor.VoxFeathers:
+ case SkinColorationStrategyInput.Color:
{
if (!RgbSkinColorContainer.Visible)
{
@@ -1563,10 +1584,11 @@ namespace Content.Client.Lobby.UI
RgbSkinColorContainer.Visible = true;
}
- var color = SkinColor.ClosestVoxColor(_rgbSkinColorSelector.Color);
+ var color = strategy.ClosestSkinColor(_rgbSkinColorSelector.Color);
Markings.CurrentSkinColor = color;
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
+
break;
}
}
@@ -1763,7 +1785,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)
{
@@ -1793,10 +1815,11 @@ namespace Content.Client.Lobby.UI
return;
var skin = _prototypeManager.Index(Profile.Species).SkinColoration;
+ var strategy = _prototypeManager.Index(skin).Strategy;
- switch (skin)
+ switch (strategy.InputType)
{
- case HumanoidSkinColor.HumanToned:
+ case SkinColorationStrategyInput.Unary:
{
if (!Skin.Visible)
{
@@ -1804,11 +1827,11 @@ namespace Content.Client.Lobby.UI
RgbSkinColorContainer.Visible = false;
}
- Skin.Value = SkinColor.HumanSkinToneFromColor(Profile.Appearance.SkinColor);
+ Skin.Value = strategy.ToUnary(Profile.Appearance.SkinColor);
break;
}
- case HumanoidSkinColor.Hues:
+ case SkinColorationStrategyInput.Color:
{
if (!RgbSkinColorContainer.Visible)
{
@@ -1816,36 +1839,11 @@ namespace Content.Client.Lobby.UI
RgbSkinColorContainer.Visible = true;
}
- // set the RGB values to the direct values otherwise
- _rgbSkinColorSelector.Color = Profile.Appearance.SkinColor;
- break;
- }
- case HumanoidSkinColor.TintedHues:
- {
- if (!RgbSkinColorContainer.Visible)
- {
- Skin.Visible = false;
- RgbSkinColorContainer.Visible = true;
- }
-
- // set the RGB values to the direct values otherwise
- _rgbSkinColorSelector.Color = Profile.Appearance.SkinColor;
- break;
- }
- case HumanoidSkinColor.VoxFeathers:
- {
- if (!RgbSkinColorContainer.Visible)
- {
- Skin.Visible = false;
- RgbSkinColorContainer.Visible = true;
- }
-
- _rgbSkinColorSelector.Color = SkinColor.ClosestVoxColor(Profile.Appearance.SkinColor);
+ _rgbSkinColorSelector.Color = strategy.ClosestSkinColor(Profile.Appearance.SkinColor);
break;
}
}
-
}
public void UpdateSpeciesGuidebookIcon()
@@ -1856,7 +1854,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 dee169b0ef..f000dd6fb1 100644
--- a/Content.Client/Lobby/UI/Loadouts/LoadoutGroupContainer.xaml.cs
+++ b/Content.Client/Lobby/UI/Loadouts/LoadoutGroupContainer.xaml.cs
@@ -43,7 +43,7 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
{
var protoMan = collection.Resolve();
var loadoutSystem = collection.Resolve().System();
- RestrictionsContainer.DisposeAllChildren();
+ RestrictionsContainer.RemoveAllChildren();
if (_groupProto.MinLimit > 0)
{
@@ -63,7 +63,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()
{
@@ -72,7 +72,7 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
});
}
- LoadoutsContainer.DisposeAllChildren();
+ LoadoutsContainer.RemoveAllChildren();
// Corvax-Loadouts-Start
var groupLoadouts = _groupProto.Loadouts;
@@ -124,14 +124,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];
@@ -207,8 +207,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/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs
index 619cac6839..9b1e7d50f8 100644
--- a/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs
+++ b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs
@@ -43,7 +43,7 @@ public sealed partial class LobbyCharacterPreviewPanel : Control
_previewDummy = uid;
- ViewBox.DisposeAllChildren();
+ ViewBox.RemoveAllChildren();
var spriteView = new SpriteView
{
OverrideDirection = Direction.South,
diff --git a/Content.Client/Mapping/MappingPrototypeList.xaml.cs b/Content.Client/Mapping/MappingPrototypeList.xaml.cs
index 8b59e6eb6f..13c92c4516 100644
--- a/Content.Client/Mapping/MappingPrototypeList.xaml.cs
+++ b/Content.Client/Mapping/MappingPrototypeList.xaml.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
@@ -37,7 +37,7 @@ public sealed partial class MappingPrototypeList : Control
{
_prototypes.Clear();
- PrototypeList.DisposeAllChildren();
+ PrototypeList.RemoveAllChildren();
_prototypes.AddRange(prototypes);
@@ -99,7 +99,7 @@ public sealed partial class MappingPrototypeList : Control
public void Search(List prototypes)
{
_search.Clear();
- SearchList.DisposeAllChildren();
+ SearchList.RemoveAllChildren();
_lastIndices = (0, -1);
_search.AddRange(prototypes);
diff --git a/Content.Client/Mapping/MappingState.cs b/Content.Client/Mapping/MappingState.cs
index 97fbee70bc..27440607cb 100644
--- a/Content.Client/Mapping/MappingState.cs
+++ b/Content.Client/Mapping/MappingState.cs
@@ -861,7 +861,7 @@ public sealed class MappingState : GameplayStateBase
}
else
{
- button.ChildrenPrototypes.DisposeAllChildren();
+ button.ChildrenPrototypes.RemoveAllChildren();
button.CollapseButton.Label.Text = "▶";
}
}
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/Nutrition/EntitySystems/DrinkSystem.cs b/Content.Client/Nutrition/EntitySystems/DrinkSystem.cs
deleted file mode 100644
index 16dbecb793..0000000000
--- a/Content.Client/Nutrition/EntitySystems/DrinkSystem.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-using Content.Shared.Nutrition.EntitySystems;
-
-namespace Content.Client.Nutrition.EntitySystems;
-
-public sealed class DrinkSystem : SharedDrinkSystem
-{
-}
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
+ worldHandle.RenderInRenderTarget(res.Blep!, () =>
{
worldHandle.UseShader(_shader);
worldHandle.DrawRect(localAABB, Color.White);
@@ -46,7 +50,7 @@ public sealed partial class StencilOverlay
worldHandle.SetTransform(Matrix3x2.Identity);
worldHandle.UseShader(_protoManager.Index(StencilMask).Instance());
- worldHandle.DrawTextureRect(_blep!.Texture, worldBounds);
+ worldHandle.DrawTextureRect(res.Blep!.Texture, worldBounds);
var curTime = _timing.RealTime;
var sprite = _sprite.GetFrame(new SpriteSpecifier.Texture(new ResPath("/Textures/Parallaxes/noise.png")), curTime);
diff --git a/Content.Client/Overlays/StencilOverlay.Weather.cs b/Content.Client/Overlays/StencilOverlay.Weather.cs
index 509b946ad4..66a6a799a7 100644
--- a/Content.Client/Overlays/StencilOverlay.Weather.cs
+++ b/Content.Client/Overlays/StencilOverlay.Weather.cs
@@ -11,7 +11,12 @@ public sealed partial class StencilOverlay
{
private List> _grids = new();
- private void DrawWeather(in OverlayDrawArgs args, WeatherPrototype weatherProto, float alpha, Matrix3x2 invMatrix)
+ private void DrawWeather(
+ in OverlayDrawArgs args,
+ CachedResources res,
+ WeatherPrototype weatherProto,
+ float alpha,
+ Matrix3x2 invMatrix)
{
var worldHandle = args.WorldHandle;
var mapId = args.MapId;
@@ -22,7 +27,7 @@ public sealed partial class StencilOverlay
// Cut out the irrelevant bits via stencil
// This is why we don't just use parallax; we might want specific tiles to get drawn over
// particularly for planet maps or stations.
- worldHandle.RenderInRenderTarget(_blep!, () =>
+ worldHandle.RenderInRenderTarget(res.Blep!, () =>
{
var xformQuery = _entManager.GetEntityQuery();
_grids.Clear();
@@ -56,7 +61,7 @@ public sealed partial class StencilOverlay
worldHandle.SetTransform(Matrix3x2.Identity);
worldHandle.UseShader(_protoManager.Index(StencilMask).Instance());
- worldHandle.DrawTextureRect(_blep!.Texture, worldBounds);
+ worldHandle.DrawTextureRect(res.Blep!.Texture, worldBounds);
var curTime = _timing.RealTime;
var sprite = _sprite.GetFrame(weatherProto.Sprite, curTime);
diff --git a/Content.Client/Overlays/StencilOverlay.cs b/Content.Client/Overlays/StencilOverlay.cs
index 0796be08e1..276181468b 100644
--- a/Content.Client/Overlays/StencilOverlay.cs
+++ b/Content.Client/Overlays/StencilOverlay.cs
@@ -1,4 +1,5 @@
using System.Numerics;
+using Content.Client.Graphics;
using Content.Client.Parallax;
using Content.Client.Weather;
using Content.Shared.Salvage;
@@ -34,7 +35,7 @@ public sealed partial class StencilOverlay : Overlay
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
- private IRenderTexture? _blep;
+ private readonly OverlayResourceCache _resources = new();
private readonly ShaderInstance _shader;
@@ -55,30 +56,49 @@ public sealed partial class StencilOverlay : Overlay
var mapUid = _map.GetMapOrInvalid(args.MapId);
var invMatrix = args.Viewport.GetWorldToLocalMatrix();
- if (_blep?.Texture.Size != args.Viewport.Size)
+ var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
+
+ if (res.Blep?.Texture.Size != args.Viewport.Size)
{
- _blep?.Dispose();
- _blep = _clyde.CreateRenderTarget(args.Viewport.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "weather-stencil");
+ res.Blep?.Dispose();
+ res.Blep = _clyde.CreateRenderTarget(args.Viewport.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "weather-stencil");
}
if (_entManager.TryGetComponent(mapUid, out var comp))
{
foreach (var (proto, weather) in comp.Weather)
{
- if (!_protoManager.TryIndex(proto, out var weatherProto))
+ if (!_protoManager.Resolve(proto, out var weatherProto))
continue;
var alpha = _weather.GetPercent(weather, mapUid);
- DrawWeather(args, weatherProto, alpha, invMatrix);
+ DrawWeather(args, res, weatherProto, alpha, invMatrix);
}
}
if (_entManager.TryGetComponent(mapUid, out var restrictedRangeComponent))
{
- DrawRestrictedRange(args, restrictedRangeComponent, invMatrix);
+ DrawRestrictedRange(args, res, restrictedRangeComponent, invMatrix);
}
args.WorldHandle.UseShader(null);
args.WorldHandle.SetTransform(Matrix3x2.Identity);
}
+
+ protected override void DisposeBehavior()
+ {
+ _resources.Dispose();
+
+ base.DisposeBehavior();
+ }
+
+ private sealed class CachedResources : IDisposable
+ {
+ public IRenderTexture? Blep;
+
+ public void Dispose()
+ {
+ Blep?.Dispose();
+ }
+ }
}
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/Players/PlayTimeTracking/JobRequirementsManager.cs b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
index bf34bfc34c..b1fa092df0 100644
--- a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
+++ b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
@@ -1,6 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
-using Content.Client.Lobby;
using Content.Shared.CCVar;
using Content.Shared.Players;
using Content.Shared.Players.JobWhitelist;
@@ -28,7 +27,8 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
[Dependency] private readonly IPrototypeManager _prototypes = default!;
private readonly Dictionary _roles = new();
- private readonly List _roleBans = new();
+ private readonly List _jobBans = new();
+ private readonly List _antagBans = new();
private readonly List _jobWhitelists = new();
private ISawmill _sawmill = default!;
@@ -54,16 +54,19 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
// Reset on disconnect, just in case.
_roles.Clear();
_jobWhitelists.Clear();
- _roleBans.Clear();
+ _jobBans.Clear();
+ _antagBans.Clear();
}
}
private void RxRoleBans(MsgRoleBans message)
{
- _sawmill.Debug($"Received roleban info containing {message.Bans.Count} entries.");
+ _sawmill.Debug($"Received role ban info: {message.JobBans.Count} job ban entries and {message.AntagBans.Count} antag ban entries.");
- _roleBans.Clear();
- _roleBans.AddRange(message.Bans);
+ _jobBans.Clear();
+ _jobBans.AddRange(message.JobBans);
+ _antagBans.Clear();
+ _antagBans.AddRange(message.AntagBans);
Updated?.Invoke();
}
@@ -92,32 +95,96 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
Updated?.Invoke();
}
- public bool IsAllowed(JobPrototype job, HumanoidCharacterProfile? profile, [NotNullWhen(false)] out FormattedMessage? reason)
+ ///
+ /// Check a list of job- and antag prototypes against the current player, for requirements and bans.
+ ///
+ ///
+ /// False if any of the prototypes are banned or have unmet requirements.
+ /// >
+ public bool IsAllowed(
+ List>? jobs,
+ List>? antags,
+ HumanoidCharacterProfile? profile,
+ [NotNullWhen(false)] out FormattedMessage? reason)
{
reason = null;
- if (_roleBans.Contains($"Job:{job.ID}"))
+ if (antags is not null)
+ {
+ foreach (var proto in antags)
+ {
+ if (!IsAllowed(_prototypes.Index(proto), profile, out reason))
+ return false;
+ }
+ }
+
+ if (jobs is not null)
+ {
+ foreach (var proto in jobs)
+ {
+ if (!IsAllowed(_prototypes.Index(proto), profile, out reason))
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// Check the job prototype against the current player, for requirements and bans
+ ///
+ public bool IsAllowed(
+ JobPrototype job,
+ HumanoidCharacterProfile? profile,
+ [NotNullWhen(false)] out FormattedMessage? reason)
+ {
+ // Check the player's bans
+ if (_jobBans.Contains(job.ID))
{
reason = FormattedMessage.FromUnformatted(Loc.GetString("role-ban"));
return false;
}
+ // Check whitelist requirements
if (!CheckWhitelist(job, out reason))
return false;
- var player = _playerManager.LocalSession;
- if (player == null)
- return true;
+ // Check other role requirements
+ var reqs = _entManager.System().GetRoleRequirements(job);
+ if (!CheckRoleRequirements(reqs, profile, out reason, job))
+ return false;
- return CheckRoleRequirements(job, profile, out reason);
+ return true;
}
- public bool CheckRoleRequirements(JobPrototype job, HumanoidCharacterProfile? profile, [NotNullWhen(false)] out FormattedMessage? reason)
+ ///
+ /// Check the antag prototype against the current player, for requirements and bans
+ ///
+ public bool IsAllowed(
+ AntagPrototype antag,
+ HumanoidCharacterProfile? profile,
+ [NotNullWhen(false)] out FormattedMessage? reason)
{
- var reqs = _entManager.System().GetJobRequirement(job);
- return CheckRoleRequirements(reqs, profile, out reason, job);
+ // Check the player's bans
+ if (_antagBans.Contains(antag.ID))
+ {
+ reason = FormattedMessage.FromUnformatted(Loc.GetString("role-ban"));
+ return false;
+ }
+
+ // Check whitelist requirements
+ if (!CheckWhitelist(antag, out reason))
+ return false;
+
+ // Check other role requirements
+ var reqs = _entManager.System().GetRoleRequirements(antag);
+ if (!CheckRoleRequirements(reqs, profile, out reason))
+ return false;
+
+ return true;
}
+ // This must be private so code paths can't accidentally skip requirement overrides. Call this through IsAllowed()
public bool CheckRoleRequirements(
HashSet? requirements,
HumanoidCharacterProfile? profile,
@@ -170,6 +237,15 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
return true;
}
+ public bool CheckWhitelist(AntagPrototype antag, [NotNullWhen(false)] out FormattedMessage? reason)
+ {
+ reason = default;
+
+ // TODO: Implement antag whitelisting.
+
+ return true;
+ }
+
public TimeSpan FetchOverallPlaytime()
{
return _roles.TryGetValue("Overall", out var overallPlaytime) ? overallPlaytime : TimeSpan.Zero;
diff --git a/Content.Client/Power/APC/UI/ApcMenu.xaml b/Content.Client/Power/APC/UI/ApcMenu.xaml
index 0ce4a943da..6cb46a4360 100644
--- a/Content.Client/Power/APC/UI/ApcMenu.xaml
+++ b/Content.Client/Power/APC/UI/ApcMenu.xaml
@@ -20,7 +20,7 @@
-
+
diff --git a/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs b/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs
index e2b27c1b62..209c1cd32b 100644
--- a/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs
+++ b/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs
@@ -53,8 +53,8 @@ public sealed partial class PowerMonitoringWindow
// Selection action
windowEntry.Button.OnButtonUp += args =>
{
- windowEntry.SourcesContainer.DisposeAllChildren();
- windowEntry.LoadsContainer.DisposeAllChildren();
+ windowEntry.SourcesContainer.RemoveAllChildren();
+ windowEntry.LoadsContainer.RemoveAllChildren();
ButtonAction(windowEntry, masterContainer);
};
}
diff --git a/Content.Client/RCD/RCDMenuBoundUserInterface.cs b/Content.Client/RCD/RCDMenuBoundUserInterface.cs
index c001b7ec70..3f847c8beb 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.TryIndex(proto.Prototype, out var entProto)) // don't use Resolve because this can be a tile
+ {
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.TryIndex(proto.Prototype, out var entProto)) // don't use Resolve because this can be a tile
{
tooltip = Loc.GetString(entProto.Name);
}
diff --git a/Content.Client/Radio/EntitySystems/RadioDeviceSystem.cs b/Content.Client/Radio/EntitySystems/RadioDeviceSystem.cs
index 29d6c635eb..a2711a8257 100644
--- a/Content.Client/Radio/EntitySystems/RadioDeviceSystem.cs
+++ b/Content.Client/Radio/EntitySystems/RadioDeviceSystem.cs
@@ -1,11 +1,12 @@
using Content.Client.Radio.Ui;
using Content.Shared.Radio;
using Content.Shared.Radio.Components;
+using Content.Shared.Radio.EntitySystems;
using Robust.Client.GameObjects;
namespace Content.Client.Radio.EntitySystems;
-public sealed class RadioDeviceSystem : EntitySystem
+public sealed class RadioDeviceSystem : SharedRadioDeviceSystem
{
[Dependency] private readonly UserInterfaceSystem _ui = default!;
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/Salvage/UI/OfferingWindow.xaml.cs b/Content.Client/Salvage/UI/OfferingWindow.xaml.cs
index 2b607b8213..3b12a31c77 100644
--- a/Content.Client/Salvage/UI/OfferingWindow.xaml.cs
+++ b/Content.Client/Salvage/UI/OfferingWindow.xaml.cs
@@ -70,7 +70,7 @@ public sealed partial class OfferingWindow : FancyWindow,
public void ClearOptions()
{
- Container.DisposeAllChildren();
+ Container.RemoveAllChildren();
}
protected override void FrameUpdate(FrameEventArgs args)
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/Shuttles/UI/DockingScreen.xaml.cs b/Content.Client/Shuttles/UI/DockingScreen.xaml.cs
index 97943c6973..97125d3159 100644
--- a/Content.Client/Shuttles/UI/DockingScreen.xaml.cs
+++ b/Content.Client/Shuttles/UI/DockingScreen.xaml.cs
@@ -67,8 +67,8 @@ public sealed partial class DockingScreen : BoxContainer
{
DockingControl.BuildDocks(shuttle);
var currentDock = DockingControl.ViewedDock;
- // DockedWith.DisposeAllChildren();
- DockPorts.DisposeAllChildren();
+ // DockedWith.RemoveAllChildren();
+ DockPorts.RemoveAllChildren();
_ourDockButtons.Clear();
if (shuttle == null)
diff --git a/Content.Client/Shuttles/UI/EmergencyConsoleWindow.xaml.cs b/Content.Client/Shuttles/UI/EmergencyConsoleWindow.xaml.cs
index 87103084b4..f9a7f02d7e 100644
--- a/Content.Client/Shuttles/UI/EmergencyConsoleWindow.xaml.cs
+++ b/Content.Client/Shuttles/UI/EmergencyConsoleWindow.xaml.cs
@@ -59,7 +59,7 @@ public sealed partial class EmergencyConsoleWindow : FancyWindow,
// TODO: Loc and cvar for this.
_earlyLaunchTime = scc.EarlyLaunchTime;
- AuthorizationsContainer.DisposeAllChildren();
+ AuthorizationsContainer.RemoveAllChildren();
var remainingAuths = scc.AuthorizationsRequired - scc.Authorizations.Count;
AuthorizationCount.Text = Loc.GetString("emergency-shuttle-ui-remaining", ("remaining", remainingAuths));
diff --git a/Content.Client/Shuttles/UI/MapScreen.xaml.cs b/Content.Client/Shuttles/UI/MapScreen.xaml.cs
index 0d7df38b91..72ad3c28b1 100644
--- a/Content.Client/Shuttles/UI/MapScreen.xaml.cs
+++ b/Content.Client/Shuttles/UI/MapScreen.xaml.cs
@@ -237,7 +237,7 @@ public sealed partial class MapScreen : BoxContainer
private void ClearMapObjects()
{
_mapObjectControls.Clear();
- HyperspaceDestinations.DisposeAllChildren();
+ HyperspaceDestinations.RemoveAllChildren();
_pendingMapObjects.Clear();
_mapObjects.Clear();
_mapHeadings.Clear();
diff --git a/Content.Client/Silicons/Laws/Ui/LawDisplay.xaml.cs b/Content.Client/Silicons/Laws/Ui/LawDisplay.xaml.cs
index 245ea194f0..55fb99a526 100644
--- a/Content.Client/Silicons/Laws/Ui/LawDisplay.xaml.cs
+++ b/Content.Client/Silicons/Laws/Ui/LawDisplay.xaml.cs
@@ -26,7 +26,7 @@ public sealed partial class LawDisplay : Control
private readonly Dictionary
[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/Controls/SplitBar.xaml.cs b/Content.Client/UserInterface/Controls/SplitBar.xaml.cs
index 2c0b716448..2f69b15499 100644
--- a/Content.Client/UserInterface/Controls/SplitBar.xaml.cs
+++ b/Content.Client/UserInterface/Controls/SplitBar.xaml.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
@@ -19,7 +19,7 @@ namespace Content.Client.UserInterface.Controls
public void Clear()
{
- DisposeAllChildren();
+ RemoveAllChildren();
}
public void AddEntry(float amount, Color color, string? tooltip = null)
diff --git a/Content.Client/UserInterface/StatsWindow.xaml.cs b/Content.Client/UserInterface/StatsWindow.xaml.cs
index 29c48fff67..5684be9e5b 100644
--- a/Content.Client/UserInterface/StatsWindow.xaml.cs
+++ b/Content.Client/UserInterface/StatsWindow.xaml.cs
@@ -17,7 +17,7 @@ namespace Content.Client.UserInterface
public void UpdateValues(List headers, List values)
{
- Values.DisposeAllChildren();
+ Values.RemoveAllChildren();
Values.Columns = headers.Count;
for (var i = 0; i < headers.Count; i++)
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();
var requirementsManager = IoCManager.Resolve();
- // TODO: role.Requirements value doesn't work at all as an equality key, this must be fixed
// Grouping roles
var groupedRoles = ghostState.GhostRoles.GroupBy(
- role => (role.Name, role.Description, role.Requirements));
+ role => (
+ role.Name,
+ role.Description,
+ // Check the prototypes for role requirements and bans
+ requirementsManager.IsAllowed(role.RolePrototypes.Item1, role.RolePrototypes.Item2, null, out var reason),
+ reason));
// Add a new entry for each role group
foreach (var group in groupedRoles)
{
+ var reason = group.Key.reason;
var name = group.Key.Name;
var description = group.Key.Description;
- var hasAccess = requirementsManager.CheckRoleRequirements(
- group.Key.Requirements,
- null,
- out var reason);
+ var prototypesAllowed = group.Key.Item3;
// Adding a new role
- _window.AddEntry(name, description, hasAccess, reason, group, spriteSystem);
+ _window.AddEntry(name, description, prototypesAllowed, reason, group, spriteSystem);
}
// Restore the Collapsible box state if it is saved
diff --git a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesWindow.xaml.cs b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesWindow.xaml.cs
index 9e2ff816b3..dd5e7e6a9b 100644
--- a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesWindow.xaml.cs
+++ b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesWindow.xaml.cs
@@ -26,7 +26,7 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
public void ClearEntries()
{
NoRolesMessage.Visible = true;
- EntryContainer.DisposeAllChildren();
+ EntryContainer.RemoveAllChildren();
_collapsibleBoxes.Clear();
}
diff --git a/Content.Client/UserInterface/Systems/Hands/Controls/HandsContainer.cs b/Content.Client/UserInterface/Systems/Hands/Controls/HandsContainer.cs
index 1421e302b8..d2f24abd6c 100644
--- a/Content.Client/UserInterface/Systems/Hands/Controls/HandsContainer.cs
+++ b/Content.Client/UserInterface/Systems/Hands/Controls/HandsContainer.cs
@@ -1,4 +1,4 @@
-using System.Linq;
+using System.Linq;
using Content.Client.UserInterface.Systems.Inventory.Controls;
using Robust.Client.UserInterface.Controls;
@@ -74,7 +74,7 @@ public sealed class HandsContainer : ItemSlotUIContainer
public void Clear()
{
ClearButtons();
- _grid.DisposeAllChildren();
+ _grid.RemoveAllChildren();
}
public IEnumerable GetButtons()
diff --git a/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs b/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs
index b22fffe57f..a139d327b0 100644
--- a/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs
+++ b/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs
@@ -121,7 +121,7 @@ namespace Content.Client.VendingMachines.UI
{
var entry = inventory[i];
- if (!_prototypeManager.TryIndex(entry.ID, out var prototype))
+ if (!_prototypeManager.Resolve(entry.ID, out var prototype))
{
_amounts[entry.ID] = 0;
continue;
diff --git a/Content.Client/Verbs/UI/VerbMenuUIController.cs b/Content.Client/Verbs/UI/VerbMenuUIController.cs
index efacf877ad..a45dc90cb7 100644
--- a/Content.Client/Verbs/UI/VerbMenuUIController.cs
+++ b/Content.Client/Verbs/UI/VerbMenuUIController.cs
@@ -109,7 +109,7 @@ namespace Content.Client.Verbs.UI
Close();
var menu = popup ?? _context.RootMenu;
- menu.MenuBody.DisposeAllChildren();
+ menu.MenuBody.RemoveAllChildren();
CurrentTarget = target;
CurrentVerbs = _verbSystem.GetVerbs(target, user, Verb.VerbTypes, out ExtraCategories, force);
@@ -207,7 +207,7 @@ namespace Content.Client.Verbs.UI
///
public void AddServerVerbs(List? verbs, ContextMenuPopup popup)
{
- popup.MenuBody.DisposeAllChildren();
+ popup.MenuBody.RemoveAllChildren();
// Verbs may be null if the server does not think we can see the target entity. This **should** not happen.
if (verbs == null)
@@ -273,7 +273,7 @@ namespace Content.Client.Verbs.UI
if (verbElement.SubMenu == null)
{
- var popupElement = new ConfirmationMenuElement(verb, "Confirm");
+ var popupElement = new ConfirmationMenuElement(verb, Loc.GetString("generic-confirm"));
verbElement.SubMenu = new ContextMenuPopup(_context, verbElement);
_context.AddElement(verbElement.SubMenu, popupElement);
}
diff --git a/Content.IntegrationTests/ExternalTestContext.cs b/Content.IntegrationTests/ExternalTestContext.cs
deleted file mode 100644
index e23b2ee636..0000000000
--- a/Content.IntegrationTests/ExternalTestContext.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System.IO;
-
-namespace Content.IntegrationTests;
-
-///
-/// Generic implementation of for usage outside of actual tests.
-///
-public sealed class ExternalTestContext(string name, TextWriter writer) : ITestContextLike
-{
- public string FullName => name;
- public TextWriter Out => writer;
-}
diff --git a/Content.IntegrationTests/GlobalUsings.cs b/Content.IntegrationTests/GlobalUsings.cs
index 8422c5c3cd..1139d45dba 100644
--- a/Content.IntegrationTests/GlobalUsings.cs
+++ b/Content.IntegrationTests/GlobalUsings.cs
@@ -3,3 +3,4 @@
global using NUnit.Framework;
global using System;
global using System.Threading.Tasks;
+global using Robust.UnitTesting.Pool;
diff --git a/Content.IntegrationTests/ITestContextLike.cs b/Content.IntegrationTests/ITestContextLike.cs
deleted file mode 100644
index 47b6e08529..0000000000
--- a/Content.IntegrationTests/ITestContextLike.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using System.IO;
-
-namespace Content.IntegrationTests;
-
-///
-/// Something that looks like a , for passing to integration tests.
-///
-public interface ITestContextLike
-{
- string FullName { get; }
- TextWriter Out { get; }
-}
-
diff --git a/Content.IntegrationTests/NUnitTestContextWrap.cs b/Content.IntegrationTests/NUnitTestContextWrap.cs
deleted file mode 100644
index 849c1b0910..0000000000
--- a/Content.IntegrationTests/NUnitTestContextWrap.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System.IO;
-
-namespace Content.IntegrationTests;
-
-///
-/// Canonical implementation of for usage in actual NUnit tests.
-///
-public sealed class NUnitTestContextWrap(TestContext context, TextWriter writer) : ITestContextLike
-{
- public string FullName => context.Test.FullName;
- public TextWriter Out => writer;
-}
diff --git a/Content.IntegrationTests/Pair/TestMapData.cs b/Content.IntegrationTests/Pair/TestMapData.cs
deleted file mode 100644
index 343641e161..0000000000
--- a/Content.IntegrationTests/Pair/TestMapData.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using Robust.Shared.GameObjects;
-using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
-
-namespace Content.IntegrationTests.Pair;
-
-///
-/// Simple data class that stored information about a map being used by a test.
-///
-public sealed class TestMapData
-{
- public EntityUid MapUid { get; set; }
- public Entity Grid;
- public MapId MapId;
- public EntityCoordinates GridCoords { get; set; }
- public MapCoordinates MapCoords { get; set; }
- public TileRef Tile { get; set; }
-
- // Client-side uids
- public EntityUid CMapUid { get; set; }
- public EntityUid CGridUid { get; set; }
- public EntityCoordinates CGridCoords { get; set; }
-}
diff --git a/Content.IntegrationTests/Pair/TestPair.Cvars.cs b/Content.IntegrationTests/Pair/TestPair.Cvars.cs
deleted file mode 100644
index 81df31fc9a..0000000000
--- a/Content.IntegrationTests/Pair/TestPair.Cvars.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-#nullable enable
-using System.Collections.Generic;
-using Content.Shared.CCVar;
-using Robust.Shared.Configuration;
-using Robust.Shared.Utility;
-
-namespace Content.IntegrationTests.Pair;
-
-public sealed partial class TestPair
-{
- private readonly Dictionary _modifiedClientCvars = new();
- private readonly Dictionary _modifiedServerCvars = new();
-
- private void OnServerCvarChanged(CVarChangeInfo args)
- {
- _modifiedServerCvars.TryAdd(args.Name, args.OldValue);
- }
-
- private void OnClientCvarChanged(CVarChangeInfo args)
- {
- _modifiedClientCvars.TryAdd(args.Name, args.OldValue);
- }
-
- internal void ClearModifiedCvars()
- {
- _modifiedClientCvars.Clear();
- _modifiedServerCvars.Clear();
- }
-
- ///
- /// Reverts any cvars that were modified during a test back to their original values.
- ///
- public async Task RevertModifiedCvars()
- {
- await Server.WaitPost(() =>
- {
- foreach (var (name, value) in _modifiedServerCvars)
- {
- if (Server.CfgMan.GetCVar(name).Equals(value))
- continue;
- Server.Log.Info($"Resetting cvar {name} to {value}");
- Server.CfgMan.SetCVar(name, value);
- }
-
- // I just love order dependent cvars
- if (_modifiedServerCvars.TryGetValue(CCVars.PanicBunkerEnabled.Name, out var panik))
- Server.CfgMan.SetCVar(CCVars.PanicBunkerEnabled.Name, panik);
-
- });
-
- await Client.WaitPost(() =>
- {
- foreach (var (name, value) in _modifiedClientCvars)
- {
- if (Client.CfgMan.GetCVar(name).Equals(value))
- continue;
-
- var flags = Client.CfgMan.GetCVarFlags(name);
- if (flags.HasFlag(CVar.REPLICATED) && flags.HasFlag(CVar.SERVER))
- continue;
-
- Client.Log.Info($"Resetting cvar {name} to {value}");
- Client.CfgMan.SetCVar(name, value);
- }
- });
-
- ClearModifiedCvars();
- }
-}
diff --git a/Content.IntegrationTests/Pair/TestPair.Helpers.cs b/Content.IntegrationTests/Pair/TestPair.Helpers.cs
index 5e7ba0dcc8..1a3b38e829 100644
--- a/Content.IntegrationTests/Pair/TestPair.Helpers.cs
+++ b/Content.IntegrationTests/Pair/TestPair.Helpers.cs
@@ -1,172 +1,19 @@
#nullable enable
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Preferences.Managers;
using Content.Shared.Preferences;
using Content.Shared.Roles;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
-using Robust.UnitTesting;
namespace Content.IntegrationTests.Pair;
// Contains misc helper functions to make writing tests easier.
public sealed partial class TestPair
{
- ///
- /// Creates a map, a grid, and a tile, and gives back references to them.
- ///
- [MemberNotNull(nameof(TestMap))]
- public async Task CreateTestMap(bool initialized = true, string tile = "Plating")
- {
- var mapData = new TestMapData();
- TestMap = mapData;
- await Server.WaitIdleAsync();
- var tileDefinitionManager = Server.ResolveDependency();
-
- TestMap = mapData;
- await Server.WaitPost(() =>
- {
- mapData.MapUid = Server.System().CreateMap(out mapData.MapId, runMapInit: initialized);
- mapData.Grid = Server.MapMan.CreateGridEntity(mapData.MapId);
- mapData.GridCoords = new EntityCoordinates(mapData.Grid, 0, 0);
- var plating = tileDefinitionManager[tile];
- var platingTile = new Tile(plating.TileId);
- Server.System().SetTile(mapData.Grid.Owner, mapData.Grid.Comp, mapData.GridCoords, platingTile);
- mapData.MapCoords = new MapCoordinates(0, 0, mapData.MapId);
- mapData.Tile = Server.System().GetAllTiles(mapData.Grid.Owner, mapData.Grid.Comp).First();
- });
-
- TestMap = mapData;
- if (!Settings.Connected)
- return mapData;
-
- await RunTicksSync(10);
- mapData.CMapUid = ToClientUid(mapData.MapUid);
- mapData.CGridUid = ToClientUid(mapData.Grid);
- mapData.CGridCoords = new EntityCoordinates(mapData.CGridUid, 0, 0);
-
- TestMap = mapData;
- return mapData;
- }
-
- ///
- /// Convert a client-side uid into a server-side uid
- ///
- public EntityUid ToServerUid(EntityUid uid) => ConvertUid(uid, Client, Server);
-
- ///
- /// Convert a server-side uid into a client-side uid
- ///
- public EntityUid ToClientUid(EntityUid uid) => ConvertUid(uid, Server, Client);
-
- private static EntityUid ConvertUid(
- EntityUid uid,
- RobustIntegrationTest.IntegrationInstance source,
- RobustIntegrationTest.IntegrationInstance destination)
- {
- if (!uid.IsValid())
- return EntityUid.Invalid;
-
- if (!source.EntMan.TryGetComponent(uid, out var meta))
- {
- Assert.Fail($"Failed to resolve MetaData while converting the EntityUid for entity {uid}");
- return EntityUid.Invalid;
- }
-
- if (!destination.EntMan.TryGetEntity(meta.NetEntity, out var otherUid))
- {
- Assert.Fail($"Failed to resolve net ID while converting the EntityUid entity {source.EntMan.ToPrettyString(uid)}");
- return EntityUid.Invalid;
- }
-
- return otherUid.Value;
- }
-
- ///
- /// Execute a command on the server and wait some number of ticks.
- ///
- public async Task WaitCommand(string cmd, int numTicks = 10)
- {
- await Server.ExecuteCommand(cmd);
- await RunTicksSync(numTicks);
- }
-
- ///
- /// Execute a command on the client and wait some number of ticks.
- ///
- public async Task WaitClientCommand(string cmd, int numTicks = 10)
- {
- await Client.ExecuteCommand(cmd);
- await RunTicksSync(numTicks);
- }
-
- ///
- /// Retrieve all entity prototypes that have some component.
- ///
- public List<(EntityPrototype, T)> GetPrototypesWithComponent(
- HashSet? ignored = null,
- bool ignoreAbstract = true,
- bool ignoreTestPrototypes = true)
- where T : IComponent, new()
- {
- if (!Server.ResolveDependency().TryGetRegistration(out var reg)
- && !Client.ResolveDependency().TryGetRegistration(out reg))
- {
- Assert.Fail($"Unknown component: {typeof(T).Name}");
- return new();
- }
-
- var id = reg.Name;
- var list = new List<(EntityPrototype, T)>();
- foreach (var proto in Server.ProtoMan.EnumeratePrototypes())
- {
- if (ignored != null && ignored.Contains(proto.ID))
- continue;
-
- if (ignoreAbstract && proto.Abstract)
- continue;
-
- if (ignoreTestPrototypes && IsTestPrototype(proto))
- continue;
-
- if (proto.Components.TryGetComponent(id, out var cmp))
- list.Add((proto, (T)cmp));
- }
-
- return list;
- }
-
- ///
- /// Retrieve all entity prototypes that have some component.
- ///
- public List GetPrototypesWithComponent(Type type,
- HashSet? ignored = null,
- bool ignoreAbstract = true,
- bool ignoreTestPrototypes = true)
- {
- var id = Server.ResolveDependency().GetComponentName(type);
- var list = new List();
- foreach (var proto in Server.ProtoMan.EnumeratePrototypes())
- {
- if (ignored != null && ignored.Contains(proto.ID))
- continue;
-
- if (ignoreAbstract && proto.Abstract)
- continue;
-
- if (ignoreTestPrototypes && IsTestPrototype(proto))
- continue;
-
- if (proto.Components.ContainsKey(id))
- list.Add((proto));
- }
-
- return list;
- }
+ public Task CreateTestMap(bool initialized = true)
+ => CreateTestMap(initialized, "Plating");
///
/// Set a user's antag preferences. Modified preferences are automatically reset at the end of the test.
diff --git a/Content.IntegrationTests/Pair/TestPair.Prototypes.cs b/Content.IntegrationTests/Pair/TestPair.Prototypes.cs
deleted file mode 100644
index e50bc96d65..0000000000
--- a/Content.IntegrationTests/Pair/TestPair.Prototypes.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-#nullable enable
-using System.Collections.Generic;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Utility;
-using Robust.UnitTesting;
-
-namespace Content.IntegrationTests.Pair;
-
-// This partial class contains helper methods to deal with yaml prototypes.
-public sealed partial class TestPair
-{
- private Dictionary> _loadedPrototypes = new();
- private HashSet _loadedEntityPrototypes = new();
-
- public async Task LoadPrototypes(List prototypes)
- {
- await LoadPrototypes(Server, prototypes);
- await LoadPrototypes(Client, prototypes);
- }
-
- private async Task LoadPrototypes(RobustIntegrationTest.IntegrationInstance instance, List prototypes)
- {
- var changed = new Dictionary>();
- foreach (var file in prototypes)
- {
- instance.ProtoMan.LoadString(file, changed: changed);
- }
-
- await instance.WaitPost(() => instance.ProtoMan.ReloadPrototypes(changed));
-
- foreach (var (kind, ids) in changed)
- {
- _loadedPrototypes.GetOrNew(kind).UnionWith(ids);
- }
-
- if (_loadedPrototypes.TryGetValue(typeof(EntityPrototype), out var entIds))
- _loadedEntityPrototypes.UnionWith(entIds);
- }
-
- public bool IsTestPrototype(EntityPrototype proto)
- {
- return _loadedEntityPrototypes.Contains(proto.ID);
- }
-
- public bool IsTestEntityPrototype(string id)
- {
- return _loadedEntityPrototypes.Contains(id);
- }
-
- public bool IsTestPrototype(string id) where TPrototype : IPrototype
- {
- return IsTestPrototype(typeof(TPrototype), id);
- }
-
- public bool IsTestPrototype(TPrototype proto) where TPrototype : IPrototype
- {
- return IsTestPrototype(typeof(TPrototype), proto.ID);
- }
-
- public bool IsTestPrototype(Type kind, string id)
- {
- return _loadedPrototypes.TryGetValue(kind, out var ids) && ids.Contains(id);
- }
-}
diff --git a/Content.IntegrationTests/Pair/TestPair.Recycle.cs b/Content.IntegrationTests/Pair/TestPair.Recycle.cs
index 694d6cfa64..887361a872 100644
--- a/Content.IntegrationTests/Pair/TestPair.Recycle.cs
+++ b/Content.IntegrationTests/Pair/TestPair.Recycle.cs
@@ -8,84 +8,17 @@ using Content.Shared.GameTicking;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Preferences;
-using Robust.Client;
-using Robust.Server.Player;
-using Robust.Shared.Exceptions;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Network;
-using Robust.Shared.Utility;
+using Robust.Shared.Player;
namespace Content.IntegrationTests.Pair;
// This partial class contains logic related to recycling & disposing test pairs.
-public sealed partial class TestPair : IAsyncDisposable
+public sealed partial class TestPair
{
- public PairState State { get; private set; } = PairState.Ready;
-
- private async Task OnDirtyDispose()
+ protected override async Task Cleanup()
{
- var usageTime = Watch.Elapsed;
- Watch.Restart();
- await _testOut.WriteLineAsync($"{nameof(DisposeAsync)}: Test gave back pair {Id} in {usageTime.TotalMilliseconds} ms");
- Kill();
- var disposeTime = Watch.Elapsed;
- await _testOut.WriteLineAsync($"{nameof(DisposeAsync)}: Disposed pair {Id} in {disposeTime.TotalMilliseconds} ms");
- // Test pairs should only dirty dispose if they are failing. If they are not failing, this probably happened
- // because someone forgot to clean-return the pair.
- Assert.Warn("Test was dirty-disposed.");
- }
-
- private async Task OnCleanDispose()
- {
- await Server.WaitIdleAsync();
- await Client.WaitIdleAsync();
+ await base.Cleanup();
await ResetModifiedPreferences();
- await Server.RemoveAllDummySessions();
-
- if (TestMap != null)
- {
- await Server.WaitPost(() => Server.EntMan.DeleteEntity(TestMap.MapUid));
- TestMap = null;
- }
-
- await RevertModifiedCvars();
-
- var usageTime = Watch.Elapsed;
- Watch.Restart();
- await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Test borrowed pair {Id} for {usageTime.TotalMilliseconds} ms");
- // Let any last minute failures the test cause happen.
- await ReallyBeIdle();
- if (!Settings.Destructive)
- {
- if (Client.IsAlive == false)
- {
- throw new Exception($"{nameof(CleanReturnAsync)}: Test killed the client in pair {Id}:", Client.UnhandledException);
- }
-
- if (Server.IsAlive == false)
- {
- throw new Exception($"{nameof(CleanReturnAsync)}: Test killed the server in pair {Id}:", Server.UnhandledException);
- }
- }
-
- if (Settings.MustNotBeReused)
- {
- Kill();
- await ReallyBeIdle();
- await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Clean disposed in {Watch.Elapsed.TotalMilliseconds} ms");
- return;
- }
-
- var sRuntimeLog = Server.ResolveDependency();
- if (sRuntimeLog.ExceptionCount > 0)
- throw new Exception($"{nameof(CleanReturnAsync)}: Server logged exceptions");
- var cRuntimeLog = Client.ResolveDependency();
- if (cRuntimeLog.ExceptionCount > 0)
- throw new Exception($"{nameof(CleanReturnAsync)}: Client logged exceptions");
-
- var returnTime = Watch.Elapsed;
- await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: PoolManager took {returnTime.TotalMilliseconds} ms to put pair {Id} back into the pool");
- State = PairState.Ready;
}
private async Task ResetModifiedPreferences()
@@ -95,61 +28,14 @@ public sealed partial class TestPair : IAsyncDisposable
{
await Server.WaitPost(() => prefMan.SetProfile(user, 0, new HumanoidCharacterProfile()).Wait());
}
+
_modifiedProfiles.Clear();
}
- public async ValueTask CleanReturnAsync()
+ protected override async Task Recycle(PairSettings next, TextWriter testOut)
{
- if (State != PairState.InUse)
- throw new Exception($"{nameof(CleanReturnAsync)}: Unexpected state. Pair: {Id}. State: {State}.");
-
- await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Return of pair {Id} started");
- State = PairState.CleanDisposed;
- await OnCleanDispose();
- DebugTools.Assert(State is PairState.Dead or PairState.Ready);
- PoolManager.NoCheckReturn(this);
- ClearContext();
- }
-
- public async ValueTask DisposeAsync()
- {
- switch (State)
- {
- case PairState.Dead:
- case PairState.Ready:
- break;
- case PairState.InUse:
- await _testOut.WriteLineAsync($"{nameof(DisposeAsync)}: Dirty return of pair {Id} started");
- await OnDirtyDispose();
- PoolManager.NoCheckReturn(this);
- ClearContext();
- break;
- default:
- throw new Exception($"{nameof(DisposeAsync)}: Unexpected state. Pair: {Id}. State: {State}.");
- }
- }
-
- public async Task CleanPooledPair(PoolSettings settings, TextWriter testOut)
- {
- Settings = default!;
- Watch.Restart();
- await testOut.WriteLineAsync($"Recycling...");
-
- var gameTicker = Server.System();
- var cNetMgr = Client.ResolveDependency();
-
- await RunTicksSync(1);
-
- // Disconnect the client if they are connected.
- if (cNetMgr.IsConnected)
- {
- await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Disconnecting client.");
- await Client.WaitPost(() => cNetMgr.ClientDisconnect("Test pooling cleanup disconnect"));
- await RunTicksSync(1);
- }
- Assert.That(cNetMgr.IsConnected, Is.False);
-
// Move to pre-round lobby. Required to toggle dummy ticker on and off
+ var gameTicker = Server.System();
if (gameTicker.RunLevel != GameRunLevel.PreRoundLobby)
{
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Restarting round.");
@@ -162,8 +48,7 @@ public sealed partial class TestPair : IAsyncDisposable
//Apply Cvars
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Setting CVar ");
- await PoolManager.SetupCVars(Client, settings);
- await PoolManager.SetupCVars(Server, settings);
+ await ApplySettings(next);
await RunTicksSync(1);
// Restart server.
@@ -171,52 +56,30 @@ public sealed partial class TestPair : IAsyncDisposable
await Server.WaitPost(() => Server.EntMan.FlushEntities());
await Server.WaitPost(() => gameTicker.RestartRound());
await RunTicksSync(1);
-
- // Connect client
- if (settings.ShouldBeConnected)
- {
- await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Connecting client");
- Client.SetConnectTarget(Server);
- await Client.WaitPost(() => cNetMgr.ClientConnect(null!, 0, null!));
- }
-
- await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Idling");
- await ReallyBeIdle();
- await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Done recycling");
}
- public void ValidateSettings(PoolSettings settings)
+ public override void ValidateSettings(PairSettings s)
{
+ base.ValidateSettings(s);
+ var settings = (PoolSettings) s;
+
var cfg = Server.CfgMan;
Assert.That(cfg.GetCVar(CCVars.AdminLogsEnabled), Is.EqualTo(settings.AdminLogsEnabled));
Assert.That(cfg.GetCVar(CCVars.GameLobbyEnabled), Is.EqualTo(settings.InLobby));
- Assert.That(cfg.GetCVar(CCVars.GameDummyTicker), Is.EqualTo(settings.UseDummyTicker));
+ Assert.That(cfg.GetCVar(CCVars.GameDummyTicker), Is.EqualTo(settings.DummyTicker));
- var entMan = Server.ResolveDependency();
- var ticker = entMan.System();
- Assert.That(ticker.DummyTicker, Is.EqualTo(settings.UseDummyTicker));
+ var ticker = Server.System();
+ Assert.That(ticker.DummyTicker, Is.EqualTo(settings.DummyTicker));
var expectPreRound = settings.InLobby | settings.DummyTicker;
var expectedLevel = expectPreRound ? GameRunLevel.PreRoundLobby : GameRunLevel.InRound;
Assert.That(ticker.RunLevel, Is.EqualTo(expectedLevel));
- var baseClient = Client.ResolveDependency();
- var netMan = Client.ResolveDependency();
- Assert.That(netMan.IsConnected, Is.Not.EqualTo(!settings.ShouldBeConnected));
-
- if (!settings.ShouldBeConnected)
+ if (ticker.DummyTicker || !settings.Connected)
return;
- Assert.That(baseClient.RunLevel, Is.EqualTo(ClientRunLevel.InGame));
- var cPlayer = Client.ResolveDependency();
- var sPlayer = Server.ResolveDependency();
- Assert.That(sPlayer.Sessions.Count(), Is.EqualTo(1));
+ var sPlayer = Server.ResolveDependency();
var session = sPlayer.Sessions.Single();
- Assert.That(cPlayer.LocalSession?.UserId, Is.EqualTo(session.UserId));
-
- if (ticker.DummyTicker)
- return;
-
var status = ticker.PlayerGameStatuses[session.UserId];
var expected = settings.InLobby
? PlayerGameStatus.NotReadyToPlay
@@ -231,11 +94,11 @@ public sealed partial class TestPair : IAsyncDisposable
}
Assert.That(session.AttachedEntity, Is.Not.Null);
- Assert.That(entMan.EntityExists(session.AttachedEntity));
- Assert.That(entMan.HasComponent(session.AttachedEntity));
- var mindCont = entMan.GetComponent(session.AttachedEntity!.Value);
+ Assert.That(Server.EntMan.EntityExists(session.AttachedEntity));
+ Assert.That(Server.EntMan.HasComponent(session.AttachedEntity));
+ var mindCont = Server.EntMan.GetComponent(session.AttachedEntity!.Value);
Assert.That(mindCont.Mind, Is.Not.Null);
- Assert.That(entMan.TryGetComponent(mindCont.Mind, out MindComponent? mind));
+ Assert.That(Server.EntMan.TryGetComponent(mindCont.Mind, out MindComponent? mind));
Assert.That(mind!.VisitingEntity, Is.Null);
Assert.That(mind.OwnedEntity, Is.EqualTo(session.AttachedEntity!.Value));
Assert.That(mind.UserId, Is.EqualTo(session.UserId));
diff --git a/Content.IntegrationTests/Pair/TestPair.Timing.cs b/Content.IntegrationTests/Pair/TestPair.Timing.cs
deleted file mode 100644
index e0859660d4..0000000000
--- a/Content.IntegrationTests/Pair/TestPair.Timing.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-#nullable enable
-
-namespace Content.IntegrationTests.Pair;
-
-// This partial class contains methods for running the server/client pairs for some number of ticks
-public sealed partial class TestPair
-{
- ///
- /// Runs the server-client pair in sync
- ///
- /// How many ticks to run them for
- public async Task RunTicksSync(int ticks)
- {
- for (var i = 0; i < ticks; i++)
- {
- await Server.WaitRunTicks(1);
- await Client.WaitRunTicks(1);
- }
- }
-
- ///
- /// Convert a time interval to some number of ticks.
- ///
- public int SecondsToTicks(float seconds)
- {
- return (int) Math.Ceiling(seconds / Server.Timing.TickPeriod.TotalSeconds);
- }
-
- ///
- /// Run the server & client in sync for some amount of time
- ///
- public async Task RunSeconds(float seconds)
- {
- await RunTicksSync(SecondsToTicks(seconds));
- }
-
- ///
- /// Runs the server-client pair in sync, but also ensures they are both idle each tick.
- ///
- /// How many ticks to run
- public async Task ReallyBeIdle(int runTicks = 25)
- {
- for (var i = 0; i < runTicks; i++)
- {
- await Client.WaitRunTicks(1);
- await Server.WaitRunTicks(1);
- for (var idleCycles = 0; idleCycles < 4; idleCycles++)
- {
- await Client.WaitIdleAsync();
- await Server.WaitIdleAsync();
- }
- }
- }
-
- ///
- /// Run the server/clients until the ticks are synchronized.
- /// By default the client will be one tick ahead of the server.
- ///
- public async Task SyncTicks(int targetDelta = 1)
- {
- var sTick = (int)Server.Timing.CurTick.Value;
- var cTick = (int)Client.Timing.CurTick.Value;
- var delta = cTick - sTick;
-
- if (delta == targetDelta)
- return;
- if (delta > targetDelta)
- await Server.WaitRunTicks(delta - targetDelta);
- else
- await Client.WaitRunTicks(targetDelta - delta);
-
- sTick = (int)Server.Timing.CurTick.Value;
- cTick = (int)Client.Timing.CurTick.Value;
- delta = cTick - sTick;
- Assert.That(delta, Is.EqualTo(targetDelta));
- }
-}
diff --git a/Content.IntegrationTests/Pair/TestPair.cs b/Content.IntegrationTests/Pair/TestPair.cs
index 43b188fd32..947840d5ce 100644
--- a/Content.IntegrationTests/Pair/TestPair.cs
+++ b/Content.IntegrationTests/Pair/TestPair.cs
@@ -1,16 +1,17 @@
#nullable enable
using System.Collections.Generic;
-using System.IO;
-using System.Linq;
+using Content.Client.IoC;
+using Content.Client.Parallax.Managers;
+using Content.IntegrationTests.Tests.Destructible;
+using Content.IntegrationTests.Tests.DeviceNetwork;
using Content.Server.GameTicking;
+using Content.Shared.CCVar;
using Content.Shared.Players;
-using Robust.Shared.Configuration;
+using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
+using Robust.Shared.Log;
using Robust.Shared.Network;
-using Robust.Shared.Player;
-using Robust.Shared.Random;
-using Robust.Shared.Timing;
using Robust.UnitTesting;
namespace Content.IntegrationTests.Pair;
@@ -18,156 +19,99 @@ namespace Content.IntegrationTests.Pair;
///
/// This object wraps a pooled server+client pair.
///
-public sealed partial class TestPair
+public sealed partial class TestPair : RobustIntegrationTest.TestPair
{
- public readonly int Id;
- private bool _initialized;
- private TextWriter _testOut = default!;
- public readonly Stopwatch Watch = new();
- public readonly List TestHistory = new();
- public PoolSettings Settings = default!;
- public TestMapData? TestMap;
private List _modifiedProfiles = new();
- private int _nextServerSeed;
- private int _nextClientSeed;
-
- public int ServerSeed;
- public int ClientSeed;
-
- public RobustIntegrationTest.ServerIntegrationInstance Server { get; private set; } = default!;
- public RobustIntegrationTest.ClientIntegrationInstance Client { get; private set; } = default!;
-
- public void Deconstruct(
- out RobustIntegrationTest.ServerIntegrationInstance server,
- out RobustIntegrationTest.ClientIntegrationInstance client)
- {
- server = Server;
- client = Client;
- }
-
- public ICommonSession? Player => Server.PlayerMan.SessionsDict.GetValueOrDefault(Client.User!.Value);
-
public ContentPlayerData? PlayerData => Player?.Data.ContentData();
- public PoolTestLogHandler ServerLogHandler { get; private set; } = default!;
- public PoolTestLogHandler ClientLogHandler { get; private set; } = default!;
-
- public TestPair(int id)
+ protected override async Task Initialize()
{
- Id = id;
- }
-
- public async Task Initialize(PoolSettings settings, TextWriter testOut, List testPrototypes)
- {
- if (_initialized)
- throw new InvalidOperationException("Already initialized");
-
- _initialized = true;
- Settings = settings;
- (Client, ClientLogHandler) = await PoolManager.GenerateClient(settings, testOut);
- (Server, ServerLogHandler) = await PoolManager.GenerateServer(settings, testOut);
- ActivateContext(testOut);
-
- Client.CfgMan.OnCVarValueChanged += OnClientCvarChanged;
- Server.CfgMan.OnCVarValueChanged += OnServerCvarChanged;
-
- if (!settings.NoLoadTestPrototypes)
- await LoadPrototypes(testPrototypes!);
-
- if (!settings.UseDummyTicker)
+ var settings = (PoolSettings)Settings;
+ if (!settings.DummyTicker)
{
- var gameTicker = Server.ResolveDependency().System();
+ var gameTicker = Server.System();
await Server.WaitPost(() => gameTicker.RestartRound());
}
+ }
- // Always initially connect clients to generate an initial random set of preferences/profiles.
- // This is to try and prevent issues where if the first test that connects the client is consistently some test
- // that uses a fixed seed, it would effectively prevent it from beingrandomized.
+ public override async Task RevertModifiedCvars()
+ {
+ // I just love order dependent cvars
+ // I.e., cvars that when changed automatically cause others to also change.
+ var modified = ModifiedServerCvars.TryGetValue(CCVars.PanicBunkerEnabled.Name, out var panik);
- Client.SetConnectTarget(Server);
- await Client.WaitIdleAsync();
- var netMgr = Client.ResolveDependency();
- await Client.WaitPost(() => netMgr.ClientConnect(null!, 0, null!));
- await ReallyBeIdle(10);
- await Client.WaitRunTicks(1);
+ await base.RevertModifiedCvars();
- if (!settings.ShouldBeConnected)
+ if (!modified)
+ return;
+
+ await Server.WaitPost(() => Server.CfgMan.SetCVar(CCVars.PanicBunkerEnabled.Name, panik!));
+ ClearModifiedCvars();
+ }
+
+ protected override async Task ApplySettings(IIntegrationInstance instance, PairSettings n)
+ {
+ var next = (PoolSettings)n;
+ await base.ApplySettings(instance, next);
+ var cfg = instance.CfgMan;
+ await instance.WaitPost(() =>
{
- await Client.WaitPost(() => netMgr.ClientDisconnect("Initial disconnect"));
- await ReallyBeIdle(10);
- }
+ if (cfg.IsCVarRegistered(CCVars.GameDummyTicker.Name))
+ cfg.SetCVar(CCVars.GameDummyTicker, next.DummyTicker);
- var cRand = Client.ResolveDependency();
- var sRand = Server.ResolveDependency();
- _nextClientSeed = cRand.Next();
- _nextServerSeed = sRand.Next();
+ if (cfg.IsCVarRegistered(CCVars.GameLobbyEnabled.Name))
+ cfg.SetCVar(CCVars.GameLobbyEnabled, next.InLobby);
+
+ if (cfg.IsCVarRegistered(CCVars.GameMap.Name))
+ cfg.SetCVar(CCVars.GameMap, next.Map);
+
+ if (cfg.IsCVarRegistered(CCVars.AdminLogsEnabled.Name))
+ cfg.SetCVar(CCVars.AdminLogsEnabled, next.AdminLogsEnabled);
+ });
}
- public void Kill()
+ protected override RobustIntegrationTest.ClientIntegrationOptions ClientOptions()
{
- State = PairState.Dead;
- ServerLogHandler.ShuttingDown = true;
- ClientLogHandler.ShuttingDown = true;
- Server.Dispose();
- Client.Dispose();
- }
+ var opts = base.ClientOptions();
- private void ClearContext()
- {
- _testOut = default!;
- ServerLogHandler.ClearContext();
- ClientLogHandler.ClearContext();
- }
-
- public void ActivateContext(TextWriter testOut)
- {
- _testOut = testOut;
- ServerLogHandler.ActivateContext(testOut);
- ClientLogHandler.ActivateContext(testOut);
- }
-
- public void Use()
- {
- if (State != PairState.Ready)
- throw new InvalidOperationException($"Pair is not ready to use. State: {State}");
- State = PairState.InUse;
- }
-
- public enum PairState : byte
- {
- Ready = 0,
- InUse = 1,
- CleanDisposed = 2,
- Dead = 3,
- }
-
- public void SetupSeed()
- {
- var sRand = Server.ResolveDependency();
- if (Settings.ServerSeed is { } severSeed)
+ opts.LoadTestAssembly = false;
+ opts.ContentStart = true;
+ opts.FailureLogLevel = LogLevel.Warning;
+ opts.Options = new()
{
- ServerSeed = severSeed;
- sRand.SetSeed(ServerSeed);
- }
- else
- {
- ServerSeed = _nextServerSeed;
- sRand.SetSeed(ServerSeed);
- _nextServerSeed = sRand.Next();
- }
+ LoadConfigAndUserData = false,
+ };
- var cRand = Client.ResolveDependency();
- if (Settings.ClientSeed is { } clientSeed)
+ opts.BeforeStart += () =>
{
- ClientSeed = clientSeed;
- cRand.SetSeed(ClientSeed);
- }
- else
+ IoCManager.Resolve().SetModuleBaseCallbacks(new ClientModuleTestingCallbacks
+ {
+ ClientBeforeIoC = () => IoCManager.Register(true)
+ });
+ };
+ return opts;
+ }
+
+ protected override RobustIntegrationTest.ServerIntegrationOptions ServerOptions()
+ {
+ var opts = base.ServerOptions();
+
+ opts.LoadTestAssembly = false;
+ opts.ContentStart = true;
+ opts.Options = new()
{
- ClientSeed = _nextClientSeed;
- cRand.SetSeed(ClientSeed);
- _nextClientSeed = cRand.Next();
- }
+ LoadConfigAndUserData = false,
+ };
+
+ opts.BeforeStart += () =>
+ {
+ // Server-only systems (i.e., systems that subscribe to events with server-only components)
+ // There's probably a better way to do this.
+ var entSysMan = IoCManager.Resolve();
+ entSysMan.LoadExtraSystemType();
+ entSysMan.LoadExtraSystemType();
+ };
+ return opts;
}
}
diff --git a/Content.IntegrationTests/PoolManager.Cvars.cs b/Content.IntegrationTests/PoolManager.Cvars.cs
index 2c51bdbc3a..b457d4a40b 100644
--- a/Content.IntegrationTests/PoolManager.Cvars.cs
+++ b/Content.IntegrationTests/PoolManager.Cvars.cs
@@ -1,15 +1,14 @@
#nullable enable
using Content.Shared.CCVar;
-using Robust.Shared;
-using Robust.Shared.Configuration;
-using Robust.UnitTesting;
namespace Content.IntegrationTests;
-// Partial class containing cvar logic
+// Partial class containing test cvars
+// This could probably be merged into the main file, but I'm keeping it separate to reduce
+// conflicts for forks.
public static partial class PoolManager
{
- private static readonly (string cvar, string value)[] TestCvars =
+ public static readonly (string cvar, string value)[] TestCvars =
{
// @formatter:off
(CCVars.DatabaseSynchronous.Name, "true"),
@@ -17,10 +16,9 @@ public static partial class PoolManager
(CCVars.HolidaysEnabled.Name, "false"),
(CCVars.GameMap.Name, TestMap),
(CCVars.AdminLogsQueueSendDelay.Name, "0"),
- (CVars.NetPVS.Name, "false"),
(CCVars.NPCMaxUpdates.Name, "999999"),
- (CVars.ThreadParallelCount.Name, "1"),
(CCVars.GameRoleTimers.Name, "false"),
+ (CCVars.GameRoleLoadoutTimers.Name, "false"),
(CCVars.GameRoleWhitelist.Name, "false"),
(CCVars.GridFill.Name, "false"),
(CCVars.PreloadGrids.Name, "false"),
@@ -29,49 +27,13 @@ public static partial class PoolManager
(CCVars.ProcgenPreload.Name, "false"),
(CCVars.WorldgenEnabled.Name, "false"),
(CCVars.GatewayGeneratorEnabled.Name, "false"),
- (CVars.ReplayClientRecordingEnabled.Name, "false"),
- (CVars.ReplayServerRecordingEnabled.Name, "false"),
(CCVars.GameDummyTicker.Name, "true"),
(CCVars.GameLobbyEnabled.Name, "false"),
(CCVars.ConfigPresetDevelopment.Name, "false"),
(CCVars.AdminLogsEnabled.Name, "false"),
(CCVars.AutosaveEnabled.Name, "false"),
- (CVars.NetBufferSize.Name, "0"),
(CCVars.InteractionRateLimitCount.Name, "9999999"),
(CCVars.InteractionRateLimitPeriod.Name, "0.1"),
(CCVars.MovementMobPushing.Name, "false"),
};
-
- public static async Task SetupCVars(RobustIntegrationTest.IntegrationInstance instance, PoolSettings settings)
- {
- var cfg = instance.ResolveDependency();
- await instance.WaitPost(() =>
- {
- if (cfg.IsCVarRegistered(CCVars.GameDummyTicker.Name))
- cfg.SetCVar(CCVars.GameDummyTicker, settings.UseDummyTicker);
-
- if (cfg.IsCVarRegistered(CCVars.GameLobbyEnabled.Name))
- cfg.SetCVar(CCVars.GameLobbyEnabled, settings.InLobby);
-
- if (cfg.IsCVarRegistered(CVars.NetInterp.Name))
- cfg.SetCVar(CVars.NetInterp, settings.DisableInterpolate);
-
- if (cfg.IsCVarRegistered(CCVars.GameMap.Name))
- cfg.SetCVar(CCVars.GameMap, settings.Map);
-
- if (cfg.IsCVarRegistered(CCVars.AdminLogsEnabled.Name))
- cfg.SetCVar(CCVars.AdminLogsEnabled, settings.AdminLogsEnabled);
-
- if (cfg.IsCVarRegistered(CVars.NetInterp.Name))
- cfg.SetCVar(CVars.NetInterp, !settings.DisableInterpolate);
- });
- }
-
- private static void SetDefaultCVars(RobustIntegrationTest.IntegrationOptions options)
- {
- foreach (var (cvar, value) in TestCvars)
- {
- options.CVarOverrides[cvar] = value;
- }
- }
}
diff --git a/Content.IntegrationTests/PoolManager.Prototypes.cs b/Content.IntegrationTests/PoolManager.Prototypes.cs
deleted file mode 100644
index eb7518ea15..0000000000
--- a/Content.IntegrationTests/PoolManager.Prototypes.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-#nullable enable
-using System.Collections.Generic;
-using System.Reflection;
-using Robust.Shared.Utility;
-
-namespace Content.IntegrationTests;
-
-// Partial class for handling the discovering and storing test prototypes.
-public static partial class PoolManager
-{
- private static List _testPrototypes = new();
-
- private const BindingFlags Flags = BindingFlags.Static
- | BindingFlags.NonPublic
- | BindingFlags.Public
- | BindingFlags.DeclaredOnly;
-
- private static void DiscoverTestPrototypes(Assembly assembly)
- {
- foreach (var type in assembly.GetTypes())
- {
- foreach (var field in type.GetFields(Flags))
- {
- if (!field.HasCustomAttribute())
- continue;
-
- var val = field.GetValue(null);
- if (val is not string str)
- throw new Exception($"TestPrototypeAttribute is only valid on non-null string fields");
-
- _testPrototypes.Add(str);
- }
- }
- }
-}
diff --git a/Content.IntegrationTests/PoolManager.cs b/Content.IntegrationTests/PoolManager.cs
index 64aac16751..6e0df92ad4 100644
--- a/Content.IntegrationTests/PoolManager.cs
+++ b/Content.IntegrationTests/PoolManager.cs
@@ -1,373 +1,17 @@
#nullable enable
-using System.Collections.Generic;
-using System.IO;
using System.Linq;
using System.Reflection;
-using System.Text;
-using System.Threading;
-using Content.Client.IoC;
-using Content.Client.Parallax.Managers;
using Content.IntegrationTests.Pair;
-using Content.IntegrationTests.Tests;
-using Content.IntegrationTests.Tests.Destructible;
-using Content.IntegrationTests.Tests.DeviceNetwork;
-using Content.IntegrationTests.Tests.Interaction.Click;
-using Robust.Client;
-using Robust.Server;
-using Robust.Shared.Configuration;
-using Robust.Shared.ContentPack;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Log;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Timing;
+using Content.Shared.CCVar;
using Robust.UnitTesting;
namespace Content.IntegrationTests;
-///
-/// Making clients, and servers is slow, this manages a pool of them so tests can reuse them.
-///
+// The static class exist to avoid breaking changes
public static partial class PoolManager
{
+ public static readonly ContentPoolManager Instance = new();
public const string TestMap = "Empty";
- private static int _pairId;
- private static readonly object PairLock = new();
- private static bool _initialized;
-
- // Pair, IsBorrowed
- private static readonly Dictionary Pairs = new();
- private static bool _dead;
- private static Exception? _poolFailureReason;
-
- private static HashSet _contentAssemblies = default!;
-
- public static async Task<(RobustIntegrationTest.ServerIntegrationInstance, PoolTestLogHandler)> GenerateServer(
- PoolSettings poolSettings,
- TextWriter testOut)
- {
- var options = new RobustIntegrationTest.ServerIntegrationOptions
- {
- ContentStart = true,
- Options = new ServerOptions()
- {
- LoadConfigAndUserData = false,
- LoadContentResources = !poolSettings.NoLoadContent,
- },
- ContentAssemblies = _contentAssemblies.ToArray()
- };
-
- var logHandler = new PoolTestLogHandler("SERVER");
- logHandler.ActivateContext(testOut);
- options.OverrideLogHandler = () => logHandler;
-
- options.BeforeStart += () =>
- {
- // Server-only systems (i.e., systems that subscribe to events with server-only components)
- var entSysMan = IoCManager.Resolve();
- entSysMan.LoadExtraSystemType();
- entSysMan.LoadExtraSystemType();
-
- IoCManager.Resolve().GetSawmill("loc").Level = LogLevel.Error;
- IoCManager.Resolve()
- .OnValueChanged(RTCVars.FailureLogLevel, value => logHandler.FailureLevel = value, true);
- };
-
- SetDefaultCVars(options);
- var server = new RobustIntegrationTest.ServerIntegrationInstance(options);
- await server.WaitIdleAsync();
- await SetupCVars(server, poolSettings);
- return (server, logHandler);
- }
-
- ///
- /// This shuts down the pool, and disposes all the server/client pairs.
- /// This is a one time operation to be used when the testing program is exiting.
- ///
- public static void Shutdown()
- {
- List localPairs;
- lock (PairLock)
- {
- if (_dead)
- return;
- _dead = true;
- localPairs = Pairs.Keys.ToList();
- }
-
- foreach (var pair in localPairs)
- {
- pair.Kill();
- }
-
- _initialized = false;
- }
-
- public static string DeathReport()
- {
- lock (PairLock)
- {
- var builder = new StringBuilder();
- var pairs = Pairs.Keys.OrderBy(pair => pair.Id);
- foreach (var pair in pairs)
- {
- var borrowed = Pairs[pair];
- builder.AppendLine($"Pair {pair.Id}, Tests Run: {pair.TestHistory.Count}, Borrowed: {borrowed}");
- for (var i = 0; i < pair.TestHistory.Count; i++)
- {
- builder.AppendLine($"#{i}: {pair.TestHistory[i]}");
- }
- }
-
- return builder.ToString();
- }
- }
-
- public static async Task<(RobustIntegrationTest.ClientIntegrationInstance, PoolTestLogHandler)> GenerateClient(
- PoolSettings poolSettings,
- TextWriter testOut)
- {
- var options = new RobustIntegrationTest.ClientIntegrationOptions
- {
- FailureLogLevel = LogLevel.Warning,
- ContentStart = true,
- ContentAssemblies = new[]
- {
- typeof(Shared.Entry.EntryPoint).Assembly,
- typeof(Client.Entry.EntryPoint).Assembly,
- typeof(PoolManager).Assembly,
- }
- };
-
- if (poolSettings.NoLoadContent)
- {
- Assert.Warn("NoLoadContent does not work on the client, ignoring");
- }
-
- options.Options = new GameControllerOptions()
- {
- LoadConfigAndUserData = false,
- // LoadContentResources = !poolSettings.NoLoadContent
- };
-
- var logHandler = new PoolTestLogHandler("CLIENT");
- logHandler.ActivateContext(testOut);
- options.OverrideLogHandler = () => logHandler;
-
- options.BeforeStart += () =>
- {
- IoCManager.Resolve().SetModuleBaseCallbacks(new ClientModuleTestingCallbacks
- {
- ClientBeforeIoC = () =>
- {
- // do not register extra systems or components here -- they will get cleared when the client is
- // disconnected. just use reflection.
- IoCManager.Register(true);
- IoCManager.Resolve().GetSawmill("loc").Level = LogLevel.Error;
- IoCManager.Resolve()
- .OnValueChanged(RTCVars.FailureLogLevel, value => logHandler.FailureLevel = value, true);
- }
- });
- };
-
- SetDefaultCVars(options);
- var client = new RobustIntegrationTest.ClientIntegrationInstance(options);
- await client.WaitIdleAsync();
- await SetupCVars(client, poolSettings);
- return (client, logHandler);
- }
-
- ///
- /// Gets a , which can be used to get access to a server, and client
- ///
- /// See
- ///
- public static async Task GetServerClient(
- PoolSettings? poolSettings = null,
- ITestContextLike? testContext = null)
- {
- return await GetServerClientPair(
- poolSettings ?? new PoolSettings(),
- testContext ?? new NUnitTestContextWrap(TestContext.CurrentContext, TestContext.Out));
- }
-
- private static string GetDefaultTestName(ITestContextLike testContext)
- {
- return testContext.FullName.Replace("Content.IntegrationTests.Tests.", "");
- }
-
- private static async Task GetServerClientPair(
- PoolSettings poolSettings,
- ITestContextLike testContext)
- {
- if (!_initialized)
- throw new InvalidOperationException($"Pool manager has not been initialized");
-
- // Trust issues with the AsyncLocal that backs this.
- var testOut = testContext.Out;
-
- DieIfPoolFailure();
- var currentTestName = poolSettings.TestName ?? GetDefaultTestName(testContext);
- var poolRetrieveTimeWatch = new Stopwatch();
- await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Called by test {currentTestName}");
- TestPair? pair = null;
- try
- {
- poolRetrieveTimeWatch.Start();
- if (poolSettings.MustBeNew)
- {
- await testOut.WriteLineAsync(
- $"{nameof(GetServerClientPair)}: Creating pair, because settings of pool settings");
- pair = await CreateServerClientPair(poolSettings, testOut);
- }
- else
- {
- await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Looking in pool for a suitable pair");
- pair = GrabOptimalPair(poolSettings);
- if (pair != null)
- {
- pair.ActivateContext(testOut);
- await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Suitable pair found");
- var canSkip = pair.Settings.CanFastRecycle(poolSettings);
-
- if (canSkip)
- {
- await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Cleanup not needed, Skipping cleanup of pair");
- await SetupCVars(pair.Client, poolSettings);
- await SetupCVars(pair.Server, poolSettings);
- await pair.RunTicksSync(1);
- }
- else
- {
- await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Cleaning existing pair");
- await pair.CleanPooledPair(poolSettings, testOut);
- }
-
- await pair.RunTicksSync(5);
- await pair.SyncTicks(targetDelta: 1);
- }
- else
- {
- await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Creating a new pair, no suitable pair found in pool");
- pair = await CreateServerClientPair(poolSettings, testOut);
- }
- }
- }
- finally
- {
- if (pair != null && pair.TestHistory.Count > 0)
- {
- await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Pair {pair.Id} Test History Start");
- for (var i = 0; i < pair.TestHistory.Count; i++)
- {
- await testOut.WriteLineAsync($"- Pair {pair.Id} Test #{i}: {pair.TestHistory[i]}");
- }
- await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Pair {pair.Id} Test History End");
- }
- }
-
- pair.ValidateSettings(poolSettings);
-
- var poolRetrieveTime = poolRetrieveTimeWatch.Elapsed;
- await testOut.WriteLineAsync(
- $"{nameof(GetServerClientPair)}: Retrieving pair {pair.Id} from pool took {poolRetrieveTime.TotalMilliseconds} ms");
-
- pair.ClearModifiedCvars();
- pair.Settings = poolSettings;
- pair.TestHistory.Add(currentTestName);
- pair.SetupSeed();
- await testOut.WriteLineAsync(
- $"{nameof(GetServerClientPair)}: Returning pair {pair.Id} with client/server seeds: {pair.ClientSeed}/{pair.ServerSeed}");
-
- pair.Watch.Restart();
- return pair;
- }
-
- private static TestPair? GrabOptimalPair(PoolSettings poolSettings)
- {
- lock (PairLock)
- {
- TestPair? fallback = null;
- foreach (var pair in Pairs.Keys)
- {
- if (Pairs[pair])
- continue;
-
- if (!pair.Settings.CanFastRecycle(poolSettings))
- {
- fallback = pair;
- continue;
- }
-
- pair.Use();
- Pairs[pair] = true;
- return pair;
- }
-
- if (fallback != null)
- {
- fallback.Use();
- Pairs[fallback!] = true;
- }
-
- return fallback;
- }
- }
-
- ///
- /// Used by TestPair after checking the server/client pair, Don't use this.
- ///
- public static void NoCheckReturn(TestPair pair)
- {
- lock (PairLock)
- {
- if (pair.State == TestPair.PairState.Dead)
- Pairs.Remove(pair);
- else if (pair.State == TestPair.PairState.Ready)
- Pairs[pair] = false;
- else
- throw new InvalidOperationException($"Attempted to return a pair in an invalid state. Pair: {pair.Id}. State: {pair.State}.");
- }
- }
-
- private static void DieIfPoolFailure()
- {
- if (_poolFailureReason != null)
- {
- // If the _poolFailureReason is not null, we can assume at least one test failed.
- // So we say inconclusive so we don't add more failed tests to search through.
- Assert.Inconclusive(@$"
-In a different test, the pool manager had an exception when trying to create a server/client pair.
-Instead of risking that the pool manager will fail at creating a server/client pairs for every single test,
-we are just going to end this here to save a lot of time. This is the exception that started this:\n {_poolFailureReason}");
- }
-
- if (_dead)
- {
- // If Pairs is null, we ran out of time, we can't assume a test failed.
- // So we are going to tell it all future tests are a failure.
- Assert.Fail("The pool was shut down");
- }
- }
-
- private static async Task CreateServerClientPair(PoolSettings poolSettings, TextWriter testOut)
- {
- try
- {
- var id = Interlocked.Increment(ref _pairId);
- var pair = new TestPair(id);
- await pair.Initialize(poolSettings, testOut, _testPrototypes);
- pair.Use();
- await pair.RunTicksSync(5);
- await pair.SyncTicks(targetDelta: 1);
- return pair;
- }
- catch (Exception ex)
- {
- _poolFailureReason = ex;
- throw;
- }
- }
///
/// Runs a server, or a client until a condition is true
@@ -423,29 +67,42 @@ we are just going to end this here to save a lot of time. This is the exception
Assert.That(passed);
}
- ///
- /// Initialize the pool manager.
- ///
- /// Assemblies to search for to discover extra prototypes and systems.
- public static void Startup(params Assembly[] extraAssemblies)
+ public static async Task GetServerClient(
+ PoolSettings? settings = null,
+ ITestContextLike? testContext = null)
{
- if (_initialized)
- throw new InvalidOperationException("Already initialized");
+ return await Instance.GetPair(settings, testContext);
+ }
- _initialized = true;
- _contentAssemblies =
- [
- typeof(Shared.Entry.EntryPoint).Assembly,
- typeof(Server.Entry.EntryPoint).Assembly,
- typeof(PoolManager).Assembly
- ];
- _contentAssemblies.UnionWith(extraAssemblies);
+ public static void Startup(params Assembly[] extra)
+ => Instance.Startup(extra);
- _testPrototypes.Clear();
- DiscoverTestPrototypes(typeof(PoolManager).Assembly);
- foreach (var assembly in extraAssemblies)
- {
- DiscoverTestPrototypes(assembly);
- }
+ public static void Shutdown() => Instance.Shutdown();
+ public static string DeathReport() => Instance.DeathReport();
+}
+
+///
+/// Making clients, and servers is slow, this manages a pool of them so tests can reuse them.
+///
+public sealed class ContentPoolManager : PoolManager
+{
+ public override PairSettings DefaultSettings => new PoolSettings();
+ protected override string GetDefaultTestName(ITestContextLike testContext)
+ {
+ return testContext.FullName.Replace("Content.IntegrationTests.Tests.", "");
+ }
+
+ public override void Startup(params Assembly[] extraAssemblies)
+ {
+ DefaultCvars.AddRange(PoolManager.TestCvars);
+
+ var shared = extraAssemblies
+ .Append(typeof(Shared.Entry.EntryPoint).Assembly)
+ .Append(typeof(PoolManager).Assembly)
+ .ToArray();
+
+ Startup([typeof(Client.Entry.EntryPoint).Assembly],
+ [typeof(Server.Entry.EntryPoint).Assembly],
+ shared);
}
}
diff --git a/Content.IntegrationTests/PoolSettings.cs b/Content.IntegrationTests/PoolSettings.cs
index 9da514e66b..fe37c38fe3 100644
--- a/Content.IntegrationTests/PoolSettings.cs
+++ b/Content.IntegrationTests/PoolSettings.cs
@@ -1,43 +1,31 @@
-#nullable enable
+namespace Content.IntegrationTests;
-using Robust.Shared.Random;
-
-namespace Content.IntegrationTests;
-
-///
-/// Settings for the pooled server, and client pair.
-/// Some options are for changing the pair, and others are
-/// so the pool can properly clean up what you borrowed.
-///
-public sealed class PoolSettings
+///
+public sealed class PoolSettings : PairSettings
{
- ///
- /// Set to true if the test will ruin the server/client pair.
- ///
- public bool Destructive { get; init; }
+ public override bool Connected
+ {
+ get => _connected || InLobby;
+ init => _connected = value;
+ }
- ///
- /// Set to true if the given server/client pair should be created fresh.
- ///
- public bool Fresh { get; init; }
+ private readonly bool _dummyTicker = true;
+ private readonly bool _connected;
///
/// Set to true if the given server should be using a dummy ticker. Ignored if is true.
///
- public bool DummyTicker { get; init; } = true;
+ public bool DummyTicker
+ {
+ get => _dummyTicker && !InLobby;
+ init => _dummyTicker = value;
+ }
///
/// If true, this enables the creation of admin logs during the test.
///
public bool AdminLogsEnabled { get; init; }
- ///
- /// Set to true if the given server/client pair should be connected from each other.
- /// Defaults to disconnected as it makes dirty recycling slightly faster.
- /// If is true, this option is ignored.
- ///
- public bool Connected { get; init; }
-
///
/// Set to true if the given server/client pair should be in the lobby.
/// If the pair is not in the lobby at the end of the test, this test must be marked as dirty.
@@ -53,81 +41,22 @@ public sealed class PoolSettings
///
public bool NoLoadContent { get; init; }
- ///
- /// This will return a server-client pair that has not loaded test prototypes.
- /// Try avoiding this whenever possible, as this will always create & destroy a new pair.
- /// Use if you need to exclude test prototypees.
- ///
- public bool NoLoadTestPrototypes { get; init; }
-
- ///
- /// Set this to true to disable the NetInterp CVar on the given server/client pair
- ///
- public bool DisableInterpolate { get; init; }
-
- ///
- /// Set this to true to always clean up the server/client pair before giving it to another borrower
- ///
- public bool Dirty { get; init; }
-
///
/// Set this to the path of a map to have the given server/client pair load the map.
///
public string Map { get; init; } = PoolManager.TestMap;
- ///
- /// Overrides the test name detection, and uses this in the test history instead
- ///
- public string? TestName { get; set; }
-
- ///
- /// If set, this will be used to call
- ///
- public int? ServerSeed { get; set; }
-
- ///
- /// If set, this will be used to call
- ///
- public int? ClientSeed { get; set; }
-
- #region Inferred Properties
-
- ///
- /// If the returned pair must not be reused
- ///
- public bool MustNotBeReused => Destructive || NoLoadContent || NoLoadTestPrototypes;
-
- ///
- /// If the given pair must be brand new
- ///
- public bool MustBeNew => Fresh || NoLoadContent || NoLoadTestPrototypes;
-
- public bool UseDummyTicker => !InLobby && DummyTicker;
-
- public bool ShouldBeConnected => InLobby || Connected;
-
- #endregion
-
- ///
- /// Tries to guess if we can skip recycling the server/client pair.
- ///
- /// The next set of settings the old pair will be set to
- /// If we can skip cleaning it up
- public bool CanFastRecycle(PoolSettings nextSettings)
+ public override bool CanFastRecycle(PairSettings nextSettings)
{
- if (MustNotBeReused)
- throw new InvalidOperationException("Attempting to recycle a non-reusable test.");
+ if (!base.CanFastRecycle(nextSettings))
+ return false;
- if (nextSettings.MustBeNew)
- throw new InvalidOperationException("Attempting to recycle a test while requesting a fresh test.");
-
- if (Dirty)
+ if (nextSettings is not PoolSettings next)
return false;
// Check that certain settings match.
- return !ShouldBeConnected == !nextSettings.ShouldBeConnected
- && UseDummyTicker == nextSettings.UseDummyTicker
- && Map == nextSettings.Map
- && InLobby == nextSettings.InLobby;
+ return DummyTicker == next.DummyTicker
+ && Map == next.Map
+ && InLobby == next.InLobby;
}
}
diff --git a/Content.IntegrationTests/PoolTestLogHandler.cs b/Content.IntegrationTests/PoolTestLogHandler.cs
deleted file mode 100644
index 909bee9785..0000000000
--- a/Content.IntegrationTests/PoolTestLogHandler.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-using System.IO;
-using Robust.Shared.Log;
-using Robust.Shared.Timing;
-using Serilog.Events;
-
-namespace Content.IntegrationTests;
-
-#nullable enable
-
-///
-/// Log handler intended for pooled integration tests.
-///
-///
-///
-/// This class logs to two places: an NUnit
-/// (so it nicely gets attributed to a test in your IDE),
-/// and an in-memory ring buffer for diagnostic purposes.
-/// If test pooling breaks, the ring buffer can be used to see what the broken instance has gone through.
-///
-///
-/// The active test context can be swapped out so pooled instances can correctly have their logs attributed.
-///
-///
-public sealed class PoolTestLogHandler : ILogHandler
-{
- private readonly string? _prefix;
-
- private RStopwatch _stopwatch;
-
- public TextWriter? ActiveContext { get; private set; }
-
- public LogLevel? FailureLevel { get; set; }
-
- public PoolTestLogHandler(string? prefix)
- {
- _prefix = prefix != null ? $"{prefix}: " : "";
- }
-
- public bool ShuttingDown;
-
- public void Log(string sawmillName, LogEvent message)
- {
- var level = message.Level.ToRobust();
-
- if (ShuttingDown && (FailureLevel == null || level < FailureLevel))
- return;
-
- if (ActiveContext is not { } testContext)
- {
- // If this gets hit it means something is logging to this instance while it's "between" tests.
- // This is a bug in either the game or the testing system, and must always be investigated.
- throw new InvalidOperationException("Log to pool test log handler without active test context");
- }
-
- var name = LogMessage.LogLevelToName(level);
- var seconds = _stopwatch.Elapsed.TotalSeconds;
- var rendered = message.RenderMessage();
- var line = $"{_prefix}{seconds:F3}s [{name}] {sawmillName}: {rendered}";
-
- testContext.WriteLine(line);
-
- if (FailureLevel == null || level < FailureLevel)
- return;
-
- testContext.Flush();
- Assert.Fail($"{line} Exception: {message.Exception}");
- }
-
- public void ClearContext()
- {
- ActiveContext = null;
- }
-
- public void ActivateContext(TextWriter context)
- {
- _stopwatch.Restart();
- ActiveContext = context;
- }
-}
diff --git a/Content.IntegrationTests/TestPrototypesAttribute.cs b/Content.IntegrationTests/TestPrototypesAttribute.cs
deleted file mode 100644
index a6728d6728..0000000000
--- a/Content.IntegrationTests/TestPrototypesAttribute.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using JetBrains.Annotations;
-
-namespace Content.IntegrationTests;
-
-///
-/// Attribute that indicates that a string contains yaml prototype data that should be loaded by integration tests.
-///
-[AttributeUsage(AttributeTargets.Field)]
-[MeansImplicitUse]
-public sealed class TestPrototypesAttribute : Attribute
-{
-}
diff --git a/Content.IntegrationTests/Tests/Access/AccessReaderTest.cs b/Content.IntegrationTests/Tests/Access/AccessReaderTest.cs
index b98f030b06..a0c8c775b1 100644
--- a/Content.IntegrationTests/Tests/Access/AccessReaderTest.cs
+++ b/Content.IntegrationTests/Tests/Access/AccessReaderTest.cs
@@ -54,7 +54,7 @@ namespace Content.IntegrationTests.Tests.Access
system.ClearDenyTags(reader);
// test one list
- system.AddAccess(reader, "A");
+ system.TryAddAccess(reader, "A");
Assert.Multiple(() =>
{
Assert.That(system.AreAccessTagsAllowed(new List> { "A" }, reader), Is.True);
@@ -62,10 +62,10 @@ namespace Content.IntegrationTests.Tests.Access
Assert.That(system.AreAccessTagsAllowed(new List> { "A", "B" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(Array.Empty>(), reader), Is.False);
});
- system.ClearAccesses(reader);
+ system.TryClearAccesses(reader);
// test one list - two items
- system.AddAccess(reader, new HashSet> { "A", "B" });
+ system.TryAddAccess(reader, new HashSet> { "A", "B" });
Assert.Multiple(() =>
{
Assert.That(system.AreAccessTagsAllowed(new List> { "A" }, reader), Is.False);
@@ -73,14 +73,14 @@ namespace Content.IntegrationTests.Tests.Access
Assert.That(system.AreAccessTagsAllowed(new List> { "A", "B" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(Array.Empty>(), reader), Is.False);
});
- system.ClearAccesses(reader);
+ system.TryClearAccesses(reader);
// test two list
var accesses = new List>>() {
new HashSet> () { "A" },
new HashSet> () { "B", "C" }
};
- system.AddAccesses(reader, accesses);
+ system.TryAddAccesses(reader, accesses);
Assert.Multiple(() =>
{
Assert.That(system.AreAccessTagsAllowed(new List> { "A" }, reader), Is.True);
@@ -90,10 +90,10 @@ namespace Content.IntegrationTests.Tests.Access
Assert.That(system.AreAccessTagsAllowed(new List> { "C", "B", "A" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(Array.Empty>(), reader), Is.False);
});
- system.ClearAccesses(reader);
+ system.TryClearAccesses(reader);
// test deny list
- system.AddAccess(reader, new HashSet> { "A" });
+ system.TryAddAccess(reader, new HashSet> { "A" });
system.AddDenyTag(reader, "B");
Assert.Multiple(() =>
{
@@ -102,7 +102,7 @@ namespace Content.IntegrationTests.Tests.Access
Assert.That(system.AreAccessTagsAllowed(new List> { "A", "B" }, reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(Array.Empty>(), reader), Is.False);
});
- system.ClearAccesses(reader);
+ system.TryClearAccesses(reader);
system.ClearDenyTags(reader);
});
await pair.CleanReturnAsync();
diff --git a/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs b/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs
new file mode 100644
index 0000000000..9dda130847
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs
@@ -0,0 +1,417 @@
+using System.Linq;
+using System.Numerics;
+using Content.Server.Atmos;
+using Content.Server.Atmos.Components;
+using Content.Server.Atmos.EntitySystems;
+using Content.Shared.Atmos;
+using Robust.Shared.EntitySerialization;
+using Robust.Shared.EntitySerialization.Systems;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Utility;
+
+namespace Content.IntegrationTests.Tests.Atmos;
+
+///
+/// Tests for AtmosphereSystem.DeltaPressure and surrounding systems
+/// handling the DeltaPressureComponent.
+///
+[TestFixture]
+[TestOf(typeof(DeltaPressureSystem))]
+public sealed class DeltaPressureTest
+{
+ #region Prototypes
+
+ [TestPrototypes]
+ private const string Prototypes = @"
+- type: entity
+ parent: BaseStructure
+ id: DeltaPressureSolidTest
+ placement:
+ mode: SnapgridCenter
+ snap:
+ - Wall
+ components:
+ - type: Physics
+ bodyType: Static
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape:
+ !type:PhysShapeAabb
+ bounds: ""-0.5,-0.5,0.5,0.5""
+ mask:
+ - FullTileMask
+ layer:
+ - WallLayer
+ density: 1000
+ - type: Airtight
+ - type: DeltaPressure
+ minPressure: 15000
+ minPressureDelta: 10000
+ scalingType: Threshold
+ baseDamage:
+ types:
+ Structural: 1000
+ - type: Damageable
+ - type: Destructible
+ thresholds:
+ - trigger:
+ !type:DamageTrigger
+ damage: 300
+ behaviors:
+ - !type:SpawnEntitiesBehavior
+ spawn:
+ Girder:
+ min: 1
+ max: 1
+ - !type:DoActsBehavior
+ acts: [ ""Destruction"" ]
+
+- type: entity
+ parent: DeltaPressureSolidTest
+ id: DeltaPressureSolidTestNoAutoJoin
+ components:
+ - type: DeltaPressure
+ autoJoinProcessingList: false
+
+- type: entity
+ parent: DeltaPressureSolidTest
+ id: DeltaPressureSolidTestAbsolute
+ components:
+ - type: DeltaPressure
+ minPressure: 10000
+ minPressureDelta: 15000
+ scalingType: Threshold
+ baseDamage:
+ types:
+ Structural: 1000
+";
+
+ #endregion
+
+ private readonly ResPath _testMap = new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml");
+
+ ///
+ /// Asserts that an entity with a DeltaPressureComponent with autoJoinProcessingList
+ /// set to true is automatically added to the DeltaPressure processing list
+ /// on the grid's GridAtmosphereComponent.
+ ///
+ /// Also asserts that an entity with a DeltaPressureComponent with autoJoinProcessingList
+ /// set to false is not automatically added to the DeltaPressure processing list.
+ ///
+ [Test]
+ public async Task ProcessingListAutoJoinTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entMan = server.EntMan;
+ var mapLoader = entMan.System();
+ var atmosphereSystem = entMan.System();
+ var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
+
+ Entity grid = default;
+ Entity dpEnt;
+
+ // Load our test map in and assert that it exists.
+ await server.WaitPost(() =>
+ {
+#pragma warning disable NUnit2045
+ Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
+ $"Failed to load map {_testMap}.");
+ Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
+#pragma warning restore NUnit2045
+
+ grid = gridSet.First();
+ });
+
+ await server.WaitAssertion(() =>
+ {
+ var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
+ dpEnt = new Entity(uid, entMan.GetComponent(uid));
+
+ Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have automatically joined!");
+ entMan.DeleteEntity(uid);
+ Assert.That(!atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was still in processing list after deletion!");
+ });
+
+ await pair.CleanReturnAsync();
+ }
+
+ ///
+ /// Asserts that an entity that doesn't need to be damaged by DeltaPressure
+ /// is not damaged by DeltaPressure.
+ ///
+ [Test]
+ public async Task ProcessingDeltaStandbyTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entMan = server.EntMan;
+ var mapLoader = entMan.System();
+ var atmosphereSystem = entMan.System();
+ var transformSystem = entMan.System();
+ var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
+
+ Entity grid = default;
+ Entity dpEnt = default;
+ TileAtmosphere tile = null!;
+ AtmosDirection direction = default;
+
+ // Load our test map in and assert that it exists.
+ await server.WaitPost(() =>
+ {
+#pragma warning disable NUnit2045
+ Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
+ $"Failed to load map {_testMap}.");
+ Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
+#pragma warning restore NUnit2045
+
+ grid = gridSet.First();
+ var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
+ dpEnt = new Entity(uid, entMan.GetComponent(uid));
+ Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
+ });
+
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ await server.WaitPost(() =>
+ {
+ var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
+ var gridAtmosComp = entMan.GetComponent(grid);
+
+ direction = (AtmosDirection)(1 << i);
+ var offsetIndices = indices.Offset(direction);
+ tile = gridAtmosComp.Tiles[offsetIndices];
+
+ Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
+
+ var toPressurize = dpEnt.Comp!.MinPressureDelta - 10;
+ var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
+
+ tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
+ });
+
+ await server.WaitRunTicks(30);
+
+ // Entity should exist, if it took one tick of damage then it should be instantly destroyed.
+ await server.WaitAssertion(() =>
+ {
+ Assert.That(!entMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold pressure from {direction} side!");
+ tile.Air!.Clear();
+ });
+
+ await server.WaitRunTicks(30);
+ }
+
+ await pair.CleanReturnAsync();
+ }
+
+ ///
+ /// Asserts that an entity that needs to be damaged by DeltaPressure
+ /// is damaged by DeltaPressure when the pressure is above the threshold.
+ ///
+ [Test]
+ public async Task ProcessingDeltaDamageTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entMan = server.EntMan;
+ var mapLoader = entMan.System();
+ var atmosphereSystem = entMan.System();
+ var transformSystem = entMan.System();
+ var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
+
+ Entity grid = default;
+ Entity dpEnt = default;
+ TileAtmosphere tile = null!;
+ AtmosDirection direction = default;
+
+ // Load our test map in and assert that it exists.
+ await server.WaitPost(() =>
+ {
+#pragma warning disable NUnit2045
+ Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
+ $"Failed to load map {_testMap}.");
+ Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
+#pragma warning restore NUnit2045
+
+ grid = gridSet.First();
+ });
+
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ await server.WaitPost(() =>
+ {
+ // Need to spawn an entity each run to ensure it works for all directions.
+ var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
+ dpEnt = new Entity