diff --git a/Content.Benchmarks/ComponentQueryBenchmark.cs b/Content.Benchmarks/ComponentQueryBenchmark.cs
new file mode 100644
index 00000000000..11c7ab9d5f5
--- /dev/null
+++ b/Content.Benchmarks/ComponentQueryBenchmark.cs
@@ -0,0 +1,273 @@
+#nullable enable
+using System;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Configs;
+using Content.IntegrationTests;
+using Content.IntegrationTests.Pair;
+using Content.Shared.Clothing.Components;
+using Content.Shared.Doors.Components;
+using Content.Shared.Item;
+using Robust.Server.GameObjects;
+using Robust.Shared;
+using Robust.Shared.Analyzers;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Random;
+
+namespace Content.Benchmarks;
+
+///
+/// Benchmarks for comparing the speed of various component fetching/lookup related methods, including directed event
+/// subscriptions
+///
+[Virtual]
+[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
+[CategoriesColumn]
+public class ComponentQueryBenchmark
+{
+ public const string Map = "Maps/atlas.yml";
+
+ private TestPair _pair = default!;
+ private IEntityManager _entMan = default!;
+ private MapId _mapId = new(10);
+ private EntityQuery _itemQuery;
+ private EntityQuery _clothingQuery;
+ private EntityQuery _mapQuery;
+ private EntityUid[] _items = default!;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ ProgramShared.PathOffset = "../../../../";
+ PoolManager.Startup(typeof(QueryBenchSystem).Assembly);
+
+ _pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
+ _entMan = _pair.Server.ResolveDependency();
+
+ _itemQuery = _entMan.GetEntityQuery();
+ _clothingQuery = _entMan.GetEntityQuery();
+ _mapQuery = _entMan.GetEntityQuery();
+
+ _pair.Server.ResolveDependency().SetSeed(42);
+ _pair.Server.WaitPost(() =>
+ {
+ var success = _entMan.System().TryLoad(_mapId, Map, out _);
+ if (!success)
+ throw new Exception("Map load failed");
+ _pair.Server.MapMan.DoMapInitialize(_mapId);
+ }).GetAwaiter().GetResult();
+
+ _items = new EntityUid[_entMan.Count()];
+ var i = 0;
+ var enumerator = _entMan.AllEntityQueryEnumerator();
+ while (enumerator.MoveNext(out var uid, out _))
+ {
+ _items[i++] = uid;
+ }
+ }
+
+ [GlobalCleanup]
+ public async Task Cleanup()
+ {
+ await _pair.DisposeAsync();
+ PoolManager.Shutdown();
+ }
+
+ #region TryComp
+
+ ///
+ /// Baseline TryComp benchmark. When the benchmark was created, around 40% of the items were clothing.
+ ///
+ [Benchmark(Baseline = true)]
+ [BenchmarkCategory("TryComp")]
+ public int TryComp()
+ {
+ var hashCode = 0;
+ foreach (var uid in _items)
+ {
+ if (_clothingQuery.TryGetComponent(uid, out var clothing))
+ hashCode = HashCode.Combine(hashCode, clothing.GetHashCode());
+ }
+ return hashCode;
+ }
+
+ ///
+ /// Variant of that is meant to always fail to get a component.
+ ///
+ [Benchmark]
+ [BenchmarkCategory("TryComp")]
+ public int TryCompFail()
+ {
+ var hashCode = 0;
+ foreach (var uid in _items)
+ {
+ if (_mapQuery.TryGetComponent(uid, out var map))
+ hashCode = HashCode.Combine(hashCode, map.GetHashCode());
+ }
+ return hashCode;
+ }
+
+ ///
+ /// Variant of that is meant to always succeed getting a component.
+ ///
+ [Benchmark]
+ [BenchmarkCategory("TryComp")]
+ public int TryCompSucceed()
+ {
+ var hashCode = 0;
+ foreach (var uid in _items)
+ {
+ if (_itemQuery.TryGetComponent(uid, out var item))
+ hashCode = HashCode.Combine(hashCode, item.GetHashCode());
+ }
+ return hashCode;
+ }
+
+ ///
+ /// Variant of that uses `Resolve()` to try get the component.
+ ///
+ [Benchmark]
+ [BenchmarkCategory("TryComp")]
+ public int Resolve()
+ {
+ var hashCode = 0;
+ foreach (var uid in _items)
+ {
+ DoResolve(uid, ref hashCode);
+ }
+ return hashCode;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void DoResolve(EntityUid uid, ref int hash, ClothingComponent? clothing = null)
+ {
+ if (_clothingQuery.Resolve(uid, ref clothing, false))
+ hash = HashCode.Combine(hash, clothing.GetHashCode());
+ }
+
+ #endregion
+
+ #region Enumeration
+
+ [Benchmark]
+ [BenchmarkCategory("Item Enumerator")]
+ public int SingleItemEnumerator()
+ {
+ var hashCode = 0;
+ var enumerator = _entMan.AllEntityQueryEnumerator();
+ while (enumerator.MoveNext(out var item))
+ {
+ hashCode = HashCode.Combine(hashCode, item.GetHashCode());
+ }
+
+ return hashCode;
+ }
+
+ [Benchmark]
+ [BenchmarkCategory("Item Enumerator")]
+ public int DoubleItemEnumerator()
+ {
+ var hashCode = 0;
+ var enumerator = _entMan.AllEntityQueryEnumerator();
+ while (enumerator.MoveNext(out _, out var item))
+ {
+ hashCode = HashCode.Combine(hashCode, item.GetHashCode());
+ }
+
+ return hashCode;
+ }
+
+ [Benchmark]
+ [BenchmarkCategory("Item Enumerator")]
+ public int TripleItemEnumerator()
+ {
+ var hashCode = 0;
+ var enumerator = _entMan.AllEntityQueryEnumerator();
+ while (enumerator.MoveNext(out _, out _, out var xform))
+ {
+ hashCode = HashCode.Combine(hashCode, xform.GetHashCode());
+ }
+
+ return hashCode;
+ }
+
+ [Benchmark]
+ [BenchmarkCategory("Airlock Enumerator")]
+ public int SingleAirlockEnumerator()
+ {
+ var hashCode = 0;
+ var enumerator = _entMan.AllEntityQueryEnumerator();
+ while (enumerator.MoveNext(out var airlock))
+ {
+ hashCode = HashCode.Combine(hashCode, airlock.GetHashCode());
+ }
+
+ return hashCode;
+ }
+
+ [Benchmark]
+ [BenchmarkCategory("Airlock Enumerator")]
+ public int DoubleAirlockEnumerator()
+ {
+ var hashCode = 0;
+ var enumerator = _entMan.AllEntityQueryEnumerator();
+ while (enumerator.MoveNext(out _, out var door))
+ {
+ hashCode = HashCode.Combine(hashCode, door.GetHashCode());
+ }
+
+ return hashCode;
+ }
+
+ [Benchmark]
+ [BenchmarkCategory("Airlock Enumerator")]
+ public int TripleAirlockEnumerator()
+ {
+ var hashCode = 0;
+ var enumerator = _entMan.AllEntityQueryEnumerator();
+ while (enumerator.MoveNext(out _, out _, out var xform))
+ {
+ hashCode = HashCode.Combine(hashCode, xform.GetHashCode());
+ }
+
+ return hashCode;
+ }
+
+ #endregion
+
+ [Benchmark(Baseline = true)]
+ [BenchmarkCategory("Events")]
+ public int StructEvents()
+ {
+ var ev = new QueryBenchEvent();
+ foreach (var uid in _items)
+ {
+ _entMan.EventBus.RaiseLocalEvent(uid, ref ev);
+ }
+
+ return ev.HashCode;
+ }
+}
+
+[ByRefEvent]
+public struct QueryBenchEvent
+{
+ public int HashCode;
+}
+
+public sealed class QueryBenchSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnEvent);
+ }
+
+ private void OnEvent(EntityUid uid, ClothingComponent component, ref QueryBenchEvent args)
+ {
+ args.HashCode = HashCode.Combine(args.HashCode, component.GetHashCode());
+ }
+}
diff --git a/Content.Benchmarks/EntityQueryBenchmark.cs b/Content.Benchmarks/EntityQueryBenchmark.cs
deleted file mode 100644
index cef6a5e35c5..00000000000
--- a/Content.Benchmarks/EntityQueryBenchmark.cs
+++ /dev/null
@@ -1,137 +0,0 @@
-#nullable enable
-using System;
-using System.Threading.Tasks;
-using BenchmarkDotNet.Attributes;
-using Content.IntegrationTests;
-using Content.IntegrationTests.Pair;
-using Content.Shared.Clothing.Components;
-using Content.Shared.Item;
-using Robust.Server.GameObjects;
-using Robust.Shared;
-using Robust.Shared.Analyzers;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Map;
-using Robust.Shared.Random;
-
-namespace Content.Benchmarks;
-
-[Virtual]
-public class EntityQueryBenchmark
-{
- public const string Map = "Maps/atlas.yml";
-
- private TestPair _pair = default!;
- private IEntityManager _entMan = default!;
- private MapId _mapId = new MapId(10);
- private EntityQuery _clothingQuery;
-
- [GlobalSetup]
- public void Setup()
- {
- ProgramShared.PathOffset = "../../../../";
- PoolManager.Startup(null);
-
- _pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
- _entMan = _pair.Server.ResolveDependency();
-
- _pair.Server.ResolveDependency().SetSeed(42);
- _pair.Server.WaitPost(() =>
- {
- var success = _entMan.System().TryLoad(_mapId, Map, out _);
- if (!success)
- throw new Exception("Map load failed");
- _pair.Server.MapMan.DoMapInitialize(_mapId);
- }).GetAwaiter().GetResult();
-
- _clothingQuery = _entMan.GetEntityQuery();
-
- // Apparently ~40% of entities are items, and 1 in 6 of those are clothing.
- /*
- var entCount = _entMan.EntityCount;
- var itemCount = _entMan.Count();
- var clothingCount = _entMan.Count();
- var itemRatio = (float) itemCount / entCount;
- var clothingRatio = (float) clothingCount / entCount;
- Console.WriteLine($"Entities: {entCount}. Items: {itemRatio:P2}. Clothing: {clothingRatio:P2}.");
- */
- }
-
- [GlobalCleanup]
- public async Task Cleanup()
- {
- await _pair.DisposeAsync();
- PoolManager.Shutdown();
- }
-
- [Benchmark]
- public int HasComponent()
- {
- var hashCode = 0;
- var enumerator = _entMan.AllEntityQueryEnumerator();
- while (enumerator.MoveNext(out var uid, out var _))
- {
- if (_entMan.HasComponent(uid))
- hashCode = HashCode.Combine(hashCode, uid.Id);
- }
-
- return hashCode;
- }
-
- [Benchmark]
- public int HasComponentQuery()
- {
- var hashCode = 0;
- var enumerator = _entMan.AllEntityQueryEnumerator();
- while (enumerator.MoveNext(out var uid, out var _))
- {
- if (_clothingQuery.HasComponent(uid))
- hashCode = HashCode.Combine(hashCode, uid.Id);
- }
-
- return hashCode;
- }
-
- [Benchmark]
- public int TryGetComponent()
- {
- var hashCode = 0;
- var enumerator = _entMan.AllEntityQueryEnumerator();
- while (enumerator.MoveNext(out var uid, out var _))
- {
- if (_entMan.TryGetComponent(uid, out ClothingComponent? clothing))
- hashCode = HashCode.Combine(hashCode, clothing.GetHashCode());
- }
-
- return hashCode;
- }
-
- [Benchmark]
- public int TryGetComponentQuery()
- {
- var hashCode = 0;
- var enumerator = _entMan.AllEntityQueryEnumerator();
- while (enumerator.MoveNext(out var uid, out var _))
- {
- if (_clothingQuery.TryGetComponent(uid, out var clothing))
- hashCode = HashCode.Combine(hashCode, clothing.GetHashCode());
- }
-
- return hashCode;
- }
-
- ///
- /// Enumerate all entities with both an item and clothing component.
- ///
- [Benchmark]
- public int Enumerator()
- {
- var hashCode = 0;
- var enumerator = _entMan.AllEntityQueryEnumerator();
- while (enumerator.MoveNext(out var _, out var clothing))
- {
- hashCode = HashCode.Combine(hashCode, clothing.GetHashCode());
- }
-
- return hashCode;
- }
-}
diff --git a/Content.Benchmarks/MapLoadBenchmark.cs b/Content.Benchmarks/MapLoadBenchmark.cs
index 7caa9958361..0cb1da4b401 100644
--- a/Content.Benchmarks/MapLoadBenchmark.cs
+++ b/Content.Benchmarks/MapLoadBenchmark.cs
@@ -26,7 +26,7 @@ public class MapLoadBenchmark
public void Setup()
{
ProgramShared.PathOffset = "../../../../";
- PoolManager.Startup(null);
+ PoolManager.Startup();
_pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
var server = _pair.Server;
diff --git a/Content.Benchmarks/PvsBenchmark.cs b/Content.Benchmarks/PvsBenchmark.cs
index c7f22bdb0cd..0b4dd907621 100644
--- a/Content.Benchmarks/PvsBenchmark.cs
+++ b/Content.Benchmarks/PvsBenchmark.cs
@@ -49,7 +49,7 @@ public void Setup()
#if !DEBUG
ProgramShared.PathOffset = "../../../../";
#endif
- PoolManager.Startup(null);
+ PoolManager.Startup();
_pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
_entMan = _pair.Server.ResolveDependency();
diff --git a/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs b/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs
index 8512107b69d..0638d945aa5 100644
--- a/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs
+++ b/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs
@@ -32,7 +32,7 @@ public class SpawnEquipDeleteBenchmark
public async Task SetupAsync()
{
ProgramShared.PathOffset = "../../../../";
- PoolManager.Startup(null);
+ PoolManager.Startup();
_pair = await PoolManager.GetServerClient();
var server = _pair.Server;
diff --git a/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs b/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs
index f8d06f758f4..999eba4d29d 100644
--- a/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs
+++ b/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs
@@ -16,14 +16,17 @@ public BwoinkWindow()
Bwoink.ChannelSelector.OnSelectionChanged += sel =>
{
- if (sel is not null)
+ if (sel is null)
{
- Title = $"{sel.CharacterName} / {sel.Username}";
+ Title = Loc.GetString("bwoink-none-selected");
+ return;
+ }
+
+ Title = $"{sel.CharacterName} / {sel.Username}";
- if (sel.OverallPlaytime != null)
- {
- Title += $" | {Loc.GetString("generic-playtime-title")}: {sel.PlaytimeString}";
- }
+ if (sel.OverallPlaytime != null)
+ {
+ Title += $" | {Loc.GetString("generic-playtime-title")}: {sel.PlaytimeString}";
}
};
diff --git a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs
index fdf935d7c04..12522d552d7 100644
--- a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs
+++ b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs
@@ -20,7 +20,7 @@ public sealed partial class PlayerListControl : BoxContainer
private List _playerList = new();
private readonly List _sortedPlayerList = new();
- public event Action? OnSelectionChanged;
+ public event Action? OnSelectionChanged;
public IReadOnlyList PlayerInfo => _playerList;
public Func? OverrideText;
@@ -41,12 +41,19 @@ public PlayerListControl()
PlayerListContainer.ItemPressed += PlayerListItemPressed;
PlayerListContainer.ItemKeyBindDown += PlayerListItemKeyBindDown;
PlayerListContainer.GenerateItem += GenerateButton;
+ PlayerListContainer.NoItemSelected += PlayerListNoItemSelected;
PopulateList(_adminSystem.PlayerList);
FilterLineEdit.OnTextChanged += _ => FilterList();
_adminSystem.PlayerListChanged += PopulateList;
BackgroundPanel.PanelOverride = new StyleBoxFlat {BackgroundColor = new Color(32, 32, 40)};
}
+ private void PlayerListNoItemSelected()
+ {
+ _selectedPlayer = null;
+ OnSelectionChanged?.Invoke(null);
+ }
+
private void PlayerListItemPressed(BaseButton.ButtonEventArgs? args, ListData? data)
{
if (args == null || data is not PlayerListData {Info: var selectedPlayer})
diff --git a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml
index 3071bf8358b..25a96df1d37 100644
--- a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml
+++ b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml
@@ -1,21 +1,19 @@
+ xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
+ xmlns:co="clr-namespace:Content.Client.UserInterface.Controls">
-
-
-
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
diff --git a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs
index 33a1d2361f2..a8bfaddecf4 100644
--- a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs
+++ b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs
@@ -1,5 +1,6 @@
using System.Linq;
using Content.Client.Administration.Systems;
+using Content.Client.UserInterface.Controls;
using Content.Shared.Administration;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
@@ -28,15 +29,14 @@ public sealed partial class PlayerTab : Control
private bool _ascending = true;
private bool _showDisconnected;
- public event Action? OnEntryKeyBindDown;
+ public event Action? OnEntryKeyBindDown;
public PlayerTab()
{
IoCManager.InjectDependencies(this);
- _adminSystem = _entManager.System();
RobustXamlLoader.Load(this);
- RefreshPlayerList(_adminSystem.PlayerList);
+ _adminSystem = _entManager.System();
_adminSystem.PlayerListChanged += RefreshPlayerList;
_adminSystem.OverlayEnabled += OverlayEnabled;
_adminSystem.OverlayDisabled += OverlayDisabled;
@@ -46,8 +46,17 @@ public PlayerTab()
ListHeader.BackgroundColorPanel.PanelOverride = new StyleBoxFlat(_altColor);
ListHeader.OnHeaderClicked += HeaderClicked;
+
+ SearchList.SearchBar = SearchLineEdit;
+ SearchList.GenerateItem += GenerateButton;
+ SearchList.DataFilterCondition += DataFilterCondition;
+ SearchList.ItemKeyBindDown += (args, data) => OnEntryKeyBindDown?.Invoke(args, data);
+
+ RefreshPlayerList(_adminSystem.PlayerList);
}
+ #region Antag Overlay
+
private void OverlayEnabled()
{
OverlayButton.Pressed = true;
@@ -70,6 +79,8 @@ private void OverlayButtonPressed(ButtonEventArgs args)
}
}
+ #endregion
+
private void ShowDisconnectedPressed(ButtonEventArgs args)
{
_showDisconnected = args.Button.Pressed;
@@ -92,14 +103,10 @@ protected override void Dispose(bool disposing)
}
}
+ #region ListContainer
+
private void RefreshPlayerList(IReadOnlyList players)
{
- foreach (var child in PlayerList.Children.ToArray())
- {
- if (child is PlayerTabEntry)
- child.Dispose();
- }
-
_players = players;
PlayerCount.Text = $"Players: {_playerMan.PlayerCount}";
@@ -108,29 +115,66 @@ private void RefreshPlayerList(IReadOnlyList players)
UpdateHeaderSymbols();
- var useAltColor = false;
- foreach (var player in sortedPlayers)
+ SearchList.PopulateList(sortedPlayers.Select(info => new PlayerListData(info,
+ $"{info.Username} {info.CharacterName} {info.IdentityName} {info.StartingJob}"))
+ .ToList());
+ }
+
+ private void GenerateButton(ListData data, ListContainerButton button)
+ {
+ if (data is not PlayerListData { Info: var player})
+ return;
+
+ var entry = new PlayerTabEntry(player, new StyleBoxFlat(button.Index % 2 == 0 ? _altColor : _defaultColor));
+ button.AddChild(entry);
+ button.ToolTip = $"{player.Username}, {player.CharacterName}, {player.IdentityName}, {player.StartingJob}";
+ }
+
+ ///
+ /// Determines whether is contained in .FilteringString.
+ /// If all characters are lowercase, the comparison ignores case.
+ /// If there is an uppercase character, the comparison is case sensitive.
+ ///
+ ///
+ ///
+ /// Whether is contained in .FilteringString.
+ private bool DataFilterCondition(string filter, ListData listData)
+ {
+ if (listData is not PlayerListData {Info: var info, FilteringString: var playerString})
+ return false;
+
+ if (!_showDisconnected && !info.Connected)
+ return false;
+
+ if (IsAllLower(filter))
{
- if (!_showDisconnected && !player.Connected)
- continue;
-
- var entry = new PlayerTabEntry(player.Username,
- player.CharacterName,
- player.IdentityName,
- player.StartingJob,
- player.Antag ? "YES" : "NO",
- new StyleBoxFlat(useAltColor ? _altColor : _defaultColor),
- player.Connected,
- player.PlaytimeString);
- entry.PlayerEntity = player.NetEntity;
- entry.OnKeyBindDown += args => OnEntryKeyBindDown?.Invoke(entry, args);
- entry.ToolTip = Loc.GetString("player-tab-entry-tooltip");
- PlayerList.AddChild(entry);
-
- useAltColor ^= true;
+ if (!playerString.Contains(filter, StringComparison.CurrentCultureIgnoreCase))
+ return false;
}
+ else
+ {
+ if (!playerString.Contains(filter))
+ return false;
+ }
+
+ return true;
}
+ private bool IsAllLower(string input)
+ {
+ foreach (var c in input)
+ {
+ if (char.IsLetter(c) && !char.IsLower(c))
+ return false;
+ }
+
+ return true;
+ }
+
+ #endregion
+
+ #region Header
+
private void UpdateHeaderSymbols()
{
ListHeader.ResetHeaderText();
@@ -174,5 +218,9 @@ private void HeaderClicked(Header header)
RefreshPlayerList(_adminSystem.PlayerList);
}
+
+ #endregion
}
+
+ public record PlayerListData(PlayerInfo Info, string FilteringString) : ListData;
}
diff --git a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabEntry.xaml b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabEntry.xaml
index 8ac90305ca9..e1371ec6f73 100644
--- a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabEntry.xaml
+++ b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabEntry.xaml
@@ -1,6 +1,6 @@
-
-
+
@@ -15,17 +15,18 @@
ClipText="True"/>
+
-
+
diff --git a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabEntry.xaml.cs b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabEntry.xaml.cs
index 80a68f4cd2d..89c5808afc7 100644
--- a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabEntry.xaml.cs
+++ b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabEntry.xaml.cs
@@ -1,4 +1,5 @@
-using Robust.Client.AutoGenerated;
+using Content.Shared.Administration;
+using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
@@ -6,23 +7,24 @@
namespace Content.Client.Administration.UI.Tabs.PlayerTab;
[GenerateTypedNameReferences]
-public sealed partial class PlayerTabEntry : ContainerButton
+public sealed partial class PlayerTabEntry : PanelContainer
{
public NetEntity? PlayerEntity;
- public PlayerTabEntry(string username, string character, string identity, string job, string antagonist, StyleBox styleBox, bool connected, string overallPlaytime)
+ public PlayerTabEntry(PlayerInfo player, StyleBoxFlat styleBoxFlat)
{
RobustXamlLoader.Load(this);
- UsernameLabel.Text = username;
- if (!connected)
+ UsernameLabel.Text = player.Username;
+ if (!player.Connected)
UsernameLabel.StyleClasses.Add("Disabled");
- JobLabel.Text = job;
- CharacterLabel.Text = character;
- if (identity != character)
- CharacterLabel.Text += $" [{identity}]";
- AntagonistLabel.Text = antagonist;
- BackgroundColorPanel.PanelOverride = styleBox;
- OverallPlaytimeLabel.Text = overallPlaytime;
+ JobLabel.Text = player.StartingJob;
+ CharacterLabel.Text = player.CharacterName;
+ if (player.IdentityName != player.CharacterName)
+ CharacterLabel.Text += $" [{player.IdentityName}]";
+ AntagonistLabel.Text = Loc.GetString(player.Antag ? "player-tab-is-antag-yes" : "player-tab-is-antag-no");
+ BackgroundColorPanel.PanelOverride = styleBoxFlat;
+ OverallPlaytimeLabel.Text = player.PlaytimeString;
+ PlayerEntity = player.NetEntity;
}
}
diff --git a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabHeader.xaml b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabHeader.xaml
index 2fd3f31b903..05007b0fea1 100644
--- a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabHeader.xaml
+++ b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabHeader.xaml
@@ -19,23 +19,25 @@
MouseFilter="Pass"/>
+
+ MouseFilter="Pass"
+ ToolTip="{Loc player-tab-entry-tooltip}"/>
diff --git a/Content.Client/Alerts/ClientAlertsSystem.cs b/Content.Client/Alerts/ClientAlertsSystem.cs
index 223bf7876ac..359c8957f9d 100644
--- a/Content.Client/Alerts/ClientAlertsSystem.cs
+++ b/Content.Client/Alerts/ClientAlertsSystem.cs
@@ -91,7 +91,7 @@ private void OnPlayerDetached(EntityUid uid, AlertsComponent component, LocalPla
ClearAlerts?.Invoke(this, EventArgs.Empty);
}
- public void AlertClicked(AlertType alertType)
+ public void AlertClicked(ProtoId alertType)
{
RaiseNetworkEvent(new ClickAlertEvent(alertType));
}
diff --git a/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs b/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs
index 78185ce6b0e..86cf0a9eb82 100644
--- a/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs
+++ b/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs
@@ -1,4 +1,5 @@
using Content.Client.Atmos.Overlays;
+using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using JetBrains.Annotations;
@@ -36,28 +37,38 @@ public override void Shutdown()
private void OnHandleState(EntityUid gridUid, GasTileOverlayComponent comp, ref ComponentHandleState args)
{
- if (args.Current is not GasTileOverlayState state)
- return;
+ Dictionary modifiedChunks;
- // is this a delta or full state?
- if (!state.FullState)
+ switch (args.Current)
{
- foreach (var index in comp.Chunks.Keys)
+ // is this a delta or full state?
+ case GasTileOverlayDeltaState delta:
{
- if (!state.AllChunks!.Contains(index))
- comp.Chunks.Remove(index);
+ modifiedChunks = delta.ModifiedChunks;
+ foreach (var index in comp.Chunks.Keys)
+ {
+ if (!delta.AllChunks.Contains(index))
+ comp.Chunks.Remove(index);
+ }
+
+ break;
}
- }
- else
- {
- foreach (var index in comp.Chunks.Keys)
+ case GasTileOverlayState state:
{
- if (!state.Chunks.ContainsKey(index))
- comp.Chunks.Remove(index);
+ modifiedChunks = state.Chunks;
+ foreach (var index in comp.Chunks.Keys)
+ {
+ if (!state.Chunks.ContainsKey(index))
+ comp.Chunks.Remove(index);
+ }
+
+ break;
}
+ default:
+ return;
}
- foreach (var (index, data) in state.Chunks)
+ foreach (var (index, data) in modifiedChunks)
{
comp.Chunks[index] = data;
}
diff --git a/Content.Client/Cabinet/ItemCabinetSystem.cs b/Content.Client/Cabinet/ItemCabinetSystem.cs
deleted file mode 100644
index aba4fdae00e..00000000000
--- a/Content.Client/Cabinet/ItemCabinetSystem.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using Content.Shared.Cabinet;
-using Robust.Client.GameObjects;
-
-namespace Content.Client.Cabinet;
-
-public sealed class ItemCabinetSystem : SharedItemCabinetSystem
-{
- protected override void UpdateAppearance(EntityUid uid, ItemCabinetComponent? cabinet = null)
- {
- if (!Resolve(uid, ref cabinet))
- return;
-
- if (!TryComp(uid, out var sprite))
- return;
-
- var state = cabinet.Opened ? cabinet.OpenState : cabinet.ClosedState;
- if (state != null)
- sprite.LayerSetState(ItemCabinetVisualLayers.Door, state);
- sprite.LayerSetVisible(ItemCabinetVisualLayers.ContainsItem, cabinet.CabinetSlot.HasItem);
- }
-}
-
-public enum ItemCabinetVisualLayers
-{
- Door,
- ContainsItem
-}
diff --git a/Content.Client/Decals/DecalSystem.cs b/Content.Client/Decals/DecalSystem.cs
index 901ab270fb5..41e5f39c286 100644
--- a/Content.Client/Decals/DecalSystem.cs
+++ b/Content.Client/Decals/DecalSystem.cs
@@ -56,34 +56,43 @@ protected override void OnDecalRemoved(EntityUid gridId, uint decalId, DecalGrid
private void OnHandleState(EntityUid gridUid, DecalGridComponent gridComp, ref ComponentHandleState args)
{
- if (args.Current is not DecalGridState state)
- return;
-
// is this a delta or full state?
_removedChunks.Clear();
+ Dictionary modifiedChunks;
- if (!state.FullState)
+ switch (args.Current)
{
- foreach (var key in gridComp.ChunkCollection.ChunkCollection.Keys)
+ case DecalGridDeltaState delta:
{
- if (!state.AllChunks!.Contains(key))
- _removedChunks.Add(key);
+ modifiedChunks = delta.ModifiedChunks;
+ foreach (var key in gridComp.ChunkCollection.ChunkCollection.Keys)
+ {
+ if (!delta.AllChunks.Contains(key))
+ _removedChunks.Add(key);
+ }
+
+ break;
}
- }
- else
- {
- foreach (var key in gridComp.ChunkCollection.ChunkCollection.Keys)
+ case DecalGridState state:
{
- if (!state.Chunks.ContainsKey(key))
- _removedChunks.Add(key);
+ modifiedChunks = state.Chunks;
+ foreach (var key in gridComp.ChunkCollection.ChunkCollection.Keys)
+ {
+ if (!state.Chunks.ContainsKey(key))
+ _removedChunks.Add(key);
+ }
+
+ break;
}
+ default:
+ return;
}
if (_removedChunks.Count > 0)
RemoveChunks(gridUid, gridComp, _removedChunks);
- if (state.Chunks.Count > 0)
- UpdateChunks(gridUid, gridComp, state.Chunks);
+ if (modifiedChunks.Count > 0)
+ UpdateChunks(gridUid, gridComp, modifiedChunks);
}
private void OnChunkUpdate(DecalChunkUpdateEvent ev)
diff --git a/Content.Client/Doors/FirelockSystem.cs b/Content.Client/Doors/FirelockSystem.cs
index cfd84a47133..f64b4c8e522 100644
--- a/Content.Client/Doors/FirelockSystem.cs
+++ b/Content.Client/Doors/FirelockSystem.cs
@@ -1,9 +1,10 @@
using Content.Shared.Doors.Components;
+using Content.Shared.Doors.Systems;
using Robust.Client.GameObjects;
namespace Content.Client.Doors;
-public sealed class FirelockSystem : EntitySystem
+public sealed class FirelockSystem : SharedFirelockSystem
{
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
diff --git a/Content.Client/Examine/ExamineSystem.cs b/Content.Client/Examine/ExamineSystem.cs
index 45db4efa53c..b476971a13a 100644
--- a/Content.Client/Examine/ExamineSystem.cs
+++ b/Content.Client/Examine/ExamineSystem.cs
@@ -239,8 +239,8 @@ public void OpenTooltip(EntityUid player, EntityUid target, bool centeredOnCurso
if (knowTarget)
{
- var itemName = FormattedMessage.RemoveMarkup(Identity.Name(target, EntityManager, player));
- var labelMessage = FormattedMessage.FromMarkup($"[bold]{itemName}[/bold]");
+ var itemName = FormattedMessage.EscapeText(Identity.Name(target, EntityManager, player));
+ var labelMessage = FormattedMessage.FromMarkupPermissive($"[bold]{itemName}[/bold]");
var label = new RichTextLabel();
label.SetMessage(labelMessage);
hBox.AddChild(label);
@@ -248,7 +248,7 @@ public void OpenTooltip(EntityUid player, EntityUid target, bool centeredOnCurso
else
{
var label = new RichTextLabel();
- label.SetMessage(FormattedMessage.FromMarkup("[bold]???[/bold]"));
+ label.SetMessage(FormattedMessage.FromMarkupOrThrow("[bold]???[/bold]"));
hBox.AddChild(label);
}
diff --git a/Content.Client/FlavorText/FlavorText.xaml.cs b/Content.Client/FlavorText/FlavorText.xaml.cs
index ffcf653f119..91b59046a47 100644
--- a/Content.Client/FlavorText/FlavorText.xaml.cs
+++ b/Content.Client/FlavorText/FlavorText.xaml.cs
@@ -17,7 +17,7 @@ public FlavorText()
var loc = IoCManager.Resolve();
CFlavorTextInput.Placeholder = new Rope.Leaf(loc.GetString("flavor-text-placeholder"));
- CFlavorTextInput.OnKeyBindDown += _ => FlavorTextChanged();
+ CFlavorTextInput.OnTextChanged += _ => FlavorTextChanged();
}
public void FlavorTextChanged()
diff --git a/Content.Client/Gravity/GravitySystem.Shake.cs b/Content.Client/Gravity/GravitySystem.Shake.cs
index c4356588d35..9b9918ca3e7 100644
--- a/Content.Client/Gravity/GravitySystem.Shake.cs
+++ b/Content.Client/Gravity/GravitySystem.Shake.cs
@@ -25,7 +25,7 @@ private void OnShakeInit(EntityUid uid, GravityShakeComponent component, Compone
{
var localPlayer = _playerManager.LocalEntity;
- if (!TryComp(localPlayer, out var xform) ||
+ if (!TryComp(localPlayer, out TransformComponent? xform) ||
xform.GridUid != uid && xform.MapUid != uid)
{
return;
@@ -46,7 +46,7 @@ protected override void ShakeGrid(EntityUid uid, GravityComponent? gravity = nul
var localPlayer = _playerManager.LocalEntity;
- if (!TryComp(localPlayer, out var xform))
+ if (!TryComp(localPlayer, out TransformComponent? xform))
return;
if (xform.GridUid != uid ||
diff --git a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml
index f46e319abeb..73a17e9bcc9 100644
--- a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml
+++ b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml
@@ -47,6 +47,17 @@
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs
index 537494933bc..87931bf8455 100644
--- a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs
+++ b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs
@@ -157,6 +157,39 @@ private void GenerateControl(ReagentPrototype reagent)
}
#endregion
+ #region PlantMetabolisms
+ if (_chemistryGuideData.ReagentGuideRegistry.TryGetValue(reagent.ID, out var guideEntryRegistryPlant) &&
+ guideEntryRegistryPlant.PlantMetabolisms != null &&
+ guideEntryRegistryPlant.PlantMetabolisms.Count > 0)
+ {
+ PlantMetabolismsDescriptionContainer.Children.Clear();
+ var metabolismLabel = new RichTextLabel();
+ metabolismLabel.SetMarkup(Loc.GetString("guidebook-reagent-plant-metabolisms-rate"));
+ var descriptionLabel = new RichTextLabel
+ {
+ Margin = new Thickness(25, 0, 10, 0)
+ };
+ var descMsg = new FormattedMessage();
+ var descriptionsCount = guideEntryRegistryPlant.PlantMetabolisms.Count;
+ var i = 0;
+ foreach (var effectString in guideEntryRegistryPlant.PlantMetabolisms)
+ {
+ descMsg.AddMarkup(effectString);
+ i++;
+ if (i < descriptionsCount)
+ descMsg.PushNewline();
+ }
+ descriptionLabel.SetMessage(descMsg);
+
+ PlantMetabolismsDescriptionContainer.AddChild(metabolismLabel);
+ PlantMetabolismsDescriptionContainer.AddChild(descriptionLabel);
+ }
+ else
+ {
+ PlantMetabolismsContainer.Visible = false;
+ }
+ #endregion
+
GenerateSources(reagent);
FormattedMessage description = new();
diff --git a/Content.Client/Guidebook/GuidebookSystem.cs b/Content.Client/Guidebook/GuidebookSystem.cs
index 0aa2c85142e..86dcf769424 100644
--- a/Content.Client/Guidebook/GuidebookSystem.cs
+++ b/Content.Client/Guidebook/GuidebookSystem.cs
@@ -148,7 +148,7 @@ private void OnGuidebookControlsTestInteractHand(EntityUid uid, GuidebookControl
public void FakeClientActivateInWorld(EntityUid activated)
{
- var activateMsg = new ActivateInWorldEvent(GetGuidebookUser(), activated);
+ var activateMsg = new ActivateInWorldEvent(GetGuidebookUser(), activated, true);
RaiseLocalEvent(activated, activateMsg);
}
diff --git a/Content.Client/Instruments/UI/InstrumentBoundUserInterface.cs b/Content.Client/Instruments/UI/InstrumentBoundUserInterface.cs
index 2a846ff708a..0f5729f55b1 100644
--- a/Content.Client/Instruments/UI/InstrumentBoundUserInterface.cs
+++ b/Content.Client/Instruments/UI/InstrumentBoundUserInterface.cs
@@ -37,14 +37,8 @@ public InstrumentBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, u
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
{
- switch (message)
- {
- case InstrumentBandResponseBuiMessage bandRx:
- _bandMenu?.Populate(bandRx.Nearby, EntMan);
- break;
- default:
- break;
- }
+ if (message is InstrumentBandResponseBuiMessage bandRx)
+ _bandMenu?.Populate(bandRx.Nearby, EntMan);
}
protected override void Open()
diff --git a/Content.Client/Interactable/InteractionSystem.cs b/Content.Client/Interactable/InteractionSystem.cs
index 0af8830e9ac..ff0a607920f 100644
--- a/Content.Client/Interactable/InteractionSystem.cs
+++ b/Content.Client/Interactable/InteractionSystem.cs
@@ -4,24 +4,6 @@
namespace Content.Client.Interactable
{
- public sealed class InteractionSystem : SharedInteractionSystem
- {
- [Dependency] private readonly SharedContainerSystem _container = default!;
-
- public override bool CanAccessViaStorage(EntityUid user, EntityUid target)
- {
- if (!EntityManager.EntityExists(target))
- return false;
-
- if (!_container.TryGetContainingContainer(target, out var container))
- return false;
-
- if (!HasComp(container.Owner))
- return false;
-
- // we don't check if the user can access the storage entity itself. This should be handed by the UI system.
- // Need to return if UI is open or not
- return true;
- }
- }
+ // TODO Remove Shared prefix
+ public sealed class InteractionSystem : SharedInteractionSystem;
}
diff --git a/Content.Client/Interaction/DragDropHelper.cs b/Content.Client/Interaction/DragDropHelper.cs
index abe35bf6d9a..e453dfd74bf 100644
--- a/Content.Client/Interaction/DragDropHelper.cs
+++ b/Content.Client/Interaction/DragDropHelper.cs
@@ -73,11 +73,6 @@ public DragDropHelper(OnBeginDrag onBeginDrag, OnContinueDrag onContinueDrag, On
_cfg.OnValueChanged(CCVars.DragDropDeadZone, SetDeadZone, true);
}
- ~DragDropHelper()
- {
- _cfg.UnsubValueChanged(CCVars.DragDropDeadZone, SetDeadZone);
- }
-
///
/// Tell the helper that the mouse button was pressed down on
/// a target, thus a drag has the possibility to begin for this target.
diff --git a/Content.Client/Inventory/StrippableBoundUserInterface.cs b/Content.Client/Inventory/StrippableBoundUserInterface.cs
index 33f38688edf..7e50eb1c68a 100644
--- a/Content.Client/Inventory/StrippableBoundUserInterface.cs
+++ b/Content.Client/Inventory/StrippableBoundUserInterface.cs
@@ -21,7 +21,6 @@
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input;
using Robust.Shared.Map;
-using Robust.Shared.Prototypes;
using static Content.Client.Inventory.ClientInventorySystem;
using static Robust.Client.UserInterface.Control;
@@ -53,9 +52,13 @@ public StrippableBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, u
_inv = EntMan.System();
_cuffable = EntMan.System();
+ // TODO update name when identity changes
var title = Loc.GetString("strippable-bound-user-interface-stripping-menu-title", ("ownerName", Identity.Name(Owner, EntMan)));
_strippingMenu = new StrippingMenu(title, this);
_strippingMenu.OnClose += Close;
+
+ // TODO use global entity
+ // BUIs are opened and closed while applying comp sates, so spawning entities here is probably not the best idea.
_virtualHiddenEntity = EntMan.SpawnEntity(HiddenPocketEntityId, MapCoordinates.Nullspace);
}
diff --git a/Content.Client/Lobby/LobbyUIController.cs b/Content.Client/Lobby/LobbyUIController.cs
index ae9196c1100..f6a3eed962c 100644
--- a/Content.Client/Lobby/LobbyUIController.cs
+++ b/Content.Client/Lobby/LobbyUIController.cs
@@ -22,7 +22,6 @@
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.Manager;
using Robust.Shared.Utility;
namespace Content.Client.Lobby;
@@ -70,12 +69,9 @@ public override void Initialize()
_profileEditor?.RefreshFlavorText();
});
- _configurationManager.OnValueChanged(CCVars.GameRoleTimers, args =>
- {
- _profileEditor?.RefreshAntags();
- _profileEditor?.RefreshJobs();
- _profileEditor?.RefreshLoadouts();
- });
+ _configurationManager.OnValueChanged(CCVars.GameRoleTimers, _ => RefreshProfileEditor());
+
+ _configurationManager.OnValueChanged(CCVars.GameRoleWhitelist, _ => RefreshProfileEditor());
}
private LobbyCharacterPreviewPanel? GetLobbyPreview()
@@ -193,6 +189,13 @@ private void RefreshLobbyPreview()
PreviewPanel.SetSummaryText(humanoid.Summary);
}
+ private void RefreshProfileEditor()
+ {
+ _profileEditor?.RefreshAntags();
+ _profileEditor?.RefreshJobs();
+ _profileEditor?.RefreshLoadouts();
+ }
+
private void SaveProfile()
{
DebugTools.Assert(EditedProfile != null);
diff --git a/Content.Client/Lobby/UI/ObserveWarningWindow.xaml b/Content.Client/Lobby/UI/ObserveWarningWindow.xaml
index 3fe8e83f57d..2feac5792a1 100644
--- a/Content.Client/Lobby/UI/ObserveWarningWindow.xaml
+++ b/Content.Client/Lobby/UI/ObserveWarningWindow.xaml
@@ -4,10 +4,11 @@
-
+
-
+
+
diff --git a/Content.Client/Lobby/UI/ObserveWarningWindow.xaml.cs b/Content.Client/Lobby/UI/ObserveWarningWindow.xaml.cs
index 718f40b2aa0..a002043ab11 100644
--- a/Content.Client/Lobby/UI/ObserveWarningWindow.xaml.cs
+++ b/Content.Client/Lobby/UI/ObserveWarningWindow.xaml.cs
@@ -1,5 +1,7 @@
+using Content.Shared.Administration.Managers;
using JetBrains.Annotations;
using Robust.Client.AutoGenerated;
+using Robust.Client.Player;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
@@ -9,11 +11,22 @@ namespace Content.Client.Lobby.UI;
[UsedImplicitly]
public sealed partial class ObserveWarningWindow : DefaultWindow
{
+ [Dependency] private readonly ISharedAdminManager _adminManager = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+
public ObserveWarningWindow()
{
Title = Loc.GetString("observe-warning-window-title");
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
+ var player = _playerManager.LocalSession;
+
+ if (player != null && _adminManager.IsAdmin(player))
+ {
+ ObserveButton.Text = Loc.GetString("observe-as-player");
+ ObserveAsAdminButton.Visible = true;
+ ObserveAsAdminButton.OnPressed += _ => { this.Close(); };
+ }
ObserveButton.OnPressed += _ => { this.Close(); };
NevermindButton.OnPressed += _ => { this.Close(); };
diff --git a/Content.Client/Maps/GridDraggingSystem.cs b/Content.Client/Maps/GridDraggingSystem.cs
index 16357c89838..e82786847e3 100644
--- a/Content.Client/Maps/GridDraggingSystem.cs
+++ b/Content.Client/Maps/GridDraggingSystem.cs
@@ -61,7 +61,7 @@ private void StopDragging()
{
if (_dragging == null) return;
- if (_lastMousePosition != null && TryComp(_dragging.Value, out var xform) &&
+ if (_lastMousePosition != null && TryComp(_dragging.Value, out TransformComponent? xform) &&
TryComp(_dragging.Value, out var body) &&
xform.MapID == _lastMousePosition.Value.MapId)
{
@@ -104,7 +104,7 @@ public override void Update(float frameTime)
StartDragging(gridUid, Transform(gridUid).InvWorldMatrix.Transform(mousePos.Position));
}
- if (!TryComp(_dragging, out var xform))
+ if (!TryComp(_dragging, out TransformComponent? xform))
{
StopDragging();
return;
diff --git a/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs b/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs
index 7f98e3e0c3d..5e068f1e9c5 100644
--- a/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs
+++ b/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs
@@ -76,7 +76,7 @@ private void OnPreview(BaseButton.ButtonEventArgs eventArgs)
TextEditPanel.Visible = !_preview;
PreviewPanel.Visible = _preview;
- PreviewLabel.SetMarkup(Rope.Collapse(ContentField.TextRope));
+ PreviewLabel.SetMarkupPermissive(Rope.Collapse(ContentField.TextRope));
}
private void OnCancel(BaseButton.ButtonEventArgs eventArgs)
diff --git a/Content.Client/Message/RichTextLabelExt.cs b/Content.Client/Message/RichTextLabelExt.cs
index ab6d17bf44d..7ff6390764b 100644
--- a/Content.Client/Message/RichTextLabelExt.cs
+++ b/Content.Client/Message/RichTextLabelExt.cs
@@ -5,9 +5,27 @@ namespace Content.Client.Message;
public static class RichTextLabelExt
{
+
+
+ ///
+ /// Sets the labels markup.
+ ///
+ ///
+ /// Invalid markup will cause exceptions to be thrown. Don't use this for user input!
+ ///
public static RichTextLabel SetMarkup(this RichTextLabel label, string markup)
{
label.SetMessage(FormattedMessage.FromMarkup(markup));
return label;
}
+
+ ///
+ /// Sets the labels markup.
+ /// Uses FormatedMessage.FromMarkupPermissive which treats invalid markup as text.
+ ///
+ public static RichTextLabel SetMarkupPermissive(this RichTextLabel label, string markup)
+ {
+ label.SetMessage(FormattedMessage.FromMarkupPermissive(markup));
+ return label;
+ }
}
diff --git a/Content.Client/Movement/Systems/FloorOcclusionSystem.cs b/Content.Client/Movement/Systems/FloorOcclusionSystem.cs
index 5c75f25ca2d..44573f8e084 100644
--- a/Content.Client/Movement/Systems/FloorOcclusionSystem.cs
+++ b/Content.Client/Movement/Systems/FloorOcclusionSystem.cs
@@ -10,51 +10,56 @@ public sealed class FloorOcclusionSystem : SharedFloorOcclusionSystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
+ private EntityQuery _spriteQuery;
+
public override void Initialize()
{
base.Initialize();
+
+ _spriteQuery = GetEntityQuery();
+
SubscribeLocalEvent(OnOcclusionStartup);
+ SubscribeLocalEvent(OnOcclusionShutdown);
SubscribeLocalEvent(OnOcclusionAuto);
}
- private void OnOcclusionAuto(EntityUid uid, FloorOcclusionComponent component, ref AfterAutoHandleStateEvent args)
+ private void OnOcclusionAuto(Entity ent, ref AfterAutoHandleStateEvent args)
{
- SetEnabled(uid, component, component.Enabled);
+ SetShader(ent.Owner, ent.Comp.Enabled);
}
- private void OnOcclusionStartup(EntityUid uid, FloorOcclusionComponent component, ComponentStartup args)
+ private void OnOcclusionStartup(Entity ent, ref ComponentStartup args)
{
- if (component.Enabled && TryComp(uid, out var sprite))
- SetShader(sprite, true);
+ SetShader(ent.Owner, ent.Comp.Enabled);
}
- protected override void SetEnabled(EntityUid uid, FloorOcclusionComponent component, bool enabled)
+ private void OnOcclusionShutdown(Entity ent, ref ComponentShutdown args)
{
- if (component.Enabled == enabled)
- return;
-
- base.SetEnabled(uid, component, enabled);
-
- if (!TryComp(uid, out var sprite))
- return;
+ SetShader(ent.Owner, false);
+ }
- SetShader(sprite, enabled);
+ protected override void SetEnabled(Entity entity)
+ {
+ SetShader(entity.Owner, entity.Comp.Enabled);
}
- private void SetShader(SpriteComponent sprite, bool enabled)
+ private void SetShader(Entity sprite, bool enabled)
{
+ if (!_spriteQuery.Resolve(sprite.Owner, ref sprite.Comp, false))
+ return;
+
var shader = _proto.Index("HorizontalCut").Instance();
- if (sprite.PostShader is not null && sprite.PostShader != shader)
+ if (sprite.Comp.PostShader is not null && sprite.Comp.PostShader != shader)
return;
if (enabled)
{
- sprite.PostShader = shader;
+ sprite.Comp.PostShader = shader;
}
else
{
- sprite.PostShader = null;
+ sprite.Comp.PostShader = null;
}
}
}
diff --git a/Content.Client/Nyanotrasen/ReverseEngineering/ReverseEngineeringMachineBoundUserInterface.cs b/Content.Client/Nyanotrasen/ReverseEngineering/ReverseEngineeringMachineBoundUserInterface.cs
index 3053e5549a3..8986e6f9e22 100644
--- a/Content.Client/Nyanotrasen/ReverseEngineering/ReverseEngineeringMachineBoundUserInterface.cs
+++ b/Content.Client/Nyanotrasen/ReverseEngineering/ReverseEngineeringMachineBoundUserInterface.cs
@@ -1,13 +1,15 @@
using Content.Shared.ReverseEngineering;
-using JetBrains.Annotations;
using Robust.Client.GameObjects;
+using Robust.Shared.Timing;
namespace Content.Client.Nyanotrasen.ReverseEngineering;
-[UsedImplicitly]
public sealed class ReverseEngineeringMachineBoundUserInterface : BoundUserInterface
{
- private ReverseEngineeringMachineMenu? _revMenu;
+ [Dependency] private readonly IEntityManager _entMan = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+
+ private ReverseEngineeringMachineMenu? _menu;
public ReverseEngineeringMachineBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
@@ -17,34 +19,40 @@ protected override void Open()
{
base.Open();
- _revMenu = new ReverseEngineeringMachineMenu();
+ if (_menu != null)
+ return;
+
+ _menu = new ReverseEngineeringMachineMenu(Owner, _entMan, _timing);
- _revMenu.OnClose += Close;
- _revMenu.OpenCentered();
+ _menu.OnClose += Close;
+ _menu.OpenCentered();
- _revMenu.OnScanButtonPressed += _ =>
+ _menu.OnScanButtonPressed += () =>
{
- SendMessage(new ReverseEngineeringMachineScanButtonPressedMessage());
+ // every button flickering is bad so no prediction
+ SendMessage(new ReverseEngineeringScanMessage());
};
- _revMenu.OnSafetyButtonToggled += safetyArgs =>
+ _menu.OnSafetyButtonToggled += () =>
{
- SendMessage(new ReverseEngineeringMachineSafetyButtonToggledMessage(safetyArgs.Pressed));
+ SendPredictedMessage(new ReverseEngineeringSafetyMessage());
};
- _revMenu.OnAutoScanButtonToggled += autoArgs =>
+ _menu.OnAutoScanButtonToggled += () =>
{
- SendMessage(new ReverseEngineeringMachineAutoScanButtonToggledMessage(autoArgs.Pressed));
+ SendPredictedMessage(new ReverseEngineeringAutoScanMessage());
};
- _revMenu.OnStopButtonPressed += _ =>
+ _menu.OnStopButtonPressed += () =>
{
- SendMessage(new ReverseEngineeringMachineStopButtonPressedMessage());
+ // see scan button
+ SendMessage(new ReverseEngineeringStopMessage());
};
- _revMenu.OnEjectButtonPressed += _ =>
+ _menu.OnEjectButtonPressed += () =>
{
- SendMessage(new ReverseEngineeringMachineEjectButtonPressedMessage());
+ // doesn't sound nice when predicted
+ SendMessage(new ReverseEngineeringEjectMessage());
};
}
@@ -52,14 +60,10 @@ protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
- switch (state)
- {
- case ReverseEngineeringMachineScanUpdateState msg:
- _revMenu?.SetButtonsDisabled(msg);
- _revMenu?.UpdateInformationDisplay(msg);
- _revMenu?.UpdateProbeTickProgressBar(msg);
- break;
- }
+ if (state is not ReverseEngineeringMachineState cast)
+ return;
+
+ _menu?.UpdateState(cast);
}
protected override void Dispose(bool disposing)
@@ -69,7 +73,8 @@ protected override void Dispose(bool disposing)
if (!disposing)
return;
- _revMenu?.Dispose();
+ _menu?.Close();
+ _menu?.Dispose();
}
}
diff --git a/Content.Client/Nyanotrasen/ReverseEngineering/ReverseEngineeringMachineMenu.xaml b/Content.Client/Nyanotrasen/ReverseEngineering/ReverseEngineeringMachineMenu.xaml
index bd8c62b440a..f2ebee5657b 100644
--- a/Content.Client/Nyanotrasen/ReverseEngineering/ReverseEngineeringMachineMenu.xaml
+++ b/Content.Client/Nyanotrasen/ReverseEngineering/ReverseEngineeringMachineMenu.xaml
@@ -30,21 +30,21 @@
Text="{Loc 'reverse-engineering-machine-eject-button'}"
ToolTip="{Loc 'reverse-engineering-machine-eject-tooltip-info'}">
-
+
-
-
+
+
-
+
@@ -57,7 +57,7 @@
-
+
@@ -70,7 +70,7 @@
Scale="2 2">
-
+
diff --git a/Content.Client/Nyanotrasen/ReverseEngineering/ReverseEngineeringMachineMenu.xaml.cs b/Content.Client/Nyanotrasen/ReverseEngineering/ReverseEngineeringMachineMenu.xaml.cs
index 1cd056d5fc5..77243798d76 100644
--- a/Content.Client/Nyanotrasen/ReverseEngineering/ReverseEngineeringMachineMenu.xaml.cs
+++ b/Content.Client/Nyanotrasen/ReverseEngineering/ReverseEngineeringMachineMenu.xaml.cs
@@ -5,40 +5,40 @@
using Robust.Client.State;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Utility;
+using Robust.Shared.Timing;
namespace Content.Client.Nyanotrasen.ReverseEngineering;
[GenerateTypedNameReferences]
public sealed partial class ReverseEngineeringMachineMenu : FancyWindow
{
- [Dependency] private readonly IEntityManager _ent = default!;
- public event Action? OnScanButtonPressed;
- public event Action? OnSafetyButtonToggled;
- public event Action? OnAutoScanButtonToggled;
- public event Action? OnStopButtonPressed;
- public event Action? OnEjectButtonPressed;
-
- public ReverseEngineeringMachineMenu()
+ private readonly IEntityManager _entMan;
+ private readonly IGameTiming _timing;
+ private readonly SharedReverseEngineeringSystem _revEng;
+
+ private readonly Entity _owner;
+
+ public event Action? OnScanButtonPressed;
+ public event Action? OnSafetyButtonToggled;
+ public event Action? OnAutoScanButtonToggled;
+ public event Action? OnStopButtonPressed;
+ public event Action? OnEjectButtonPressed;
+
+ public ReverseEngineeringMachineMenu(EntityUid owner, IEntityManager entMan, IGameTiming timing)
{
RobustXamlLoader.Load(this);
- IoCManager.InjectDependencies(this);
- ScanButton.OnPressed += a => OnScanButtonPressed?.Invoke(a);
- SafetyButton.OnToggled += a => OnSafetyButtonToggled?.Invoke(a);
- AutoScanButton.OnToggled += a => OnAutoScanButtonToggled?.Invoke(a);
- StopButton.OnPressed += a => OnStopButtonPressed?.Invoke(a);
- EjectButton.OnPressed += a => OnEjectButtonPressed?.Invoke(a);
- }
+ _entMan = entMan;
+ _timing = timing;
+ _revEng = entMan.System();
+ _owner = (owner, entMan.GetComponent(owner));
- public void SetButtonsDisabled(ReverseEngineeringMachineScanUpdateState state)
- {
- ScanButton.Disabled = !state.CanScan;
- StopButton.Disabled = !state.Scanning;
- SafetyButton.Pressed = state.Safety;
- AutoScanButton.Pressed = state.AutoProbe;
- EjectButton.Disabled = (state.Target == null || state.Scanning);
+ ScanButton.OnPressed += _ => OnScanButtonPressed?.Invoke();
+ SafetyButton.OnToggled += _ => OnSafetyButtonToggled?.Invoke();
+ AutoScanButton.OnToggled += _ => OnAutoScanButtonToggled?.Invoke();
+ StopButton.OnPressed += _ => OnStopButtonPressed?.Invoke();
+ EjectButton.OnPressed += _ => OnEjectButtonPressed?.Invoke();
}
private void UpdateArtifactIcon(EntityUid? uid)
@@ -48,61 +48,40 @@ private void UpdateArtifactIcon(EntityUid? uid)
ItemDisplay.Visible = false;
return;
}
- ItemDisplay.Visible = true;
+ ItemDisplay.Visible = true;
ItemDisplay.SetEntity(uid);
}
- public void UpdateInformationDisplay(ReverseEngineeringMachineScanUpdateState state)
+ public void UpdateState(ReverseEngineeringMachineState state)
{
- var message = new FormattedMessage();
- _ent.TryGetEntity(state.Target, out var entityTarget);
+ Information.SetMessage(state.ScanMessage);
+ }
- UpdateArtifactIcon(entityTarget);
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
- if (state.ScanReport == null)
- {
- if (!state.CanScan) //no item
- message.AddMarkup(Loc.GetString("analysis-console-info-no-artifact"));
- else if (state.Target == null) //ready to go
- message.AddMarkup(Loc.GetString("analysis-console-info-ready"));
- }
- else
- {
- message.AddMessage(state.ScanReport);
- }
+ var scanning = _revEng.IsActive(_owner);
+ var item = _revEng.GetItem(_owner);
+ ScanButton.Disabled = scanning || item == null;
+ StopButton.Disabled = !scanning;
+ SafetyButton.Pressed = _owner.Comp.SafetyOn;
+ AutoScanButton.Pressed = _owner.Comp.AutoScan;
+ EjectButton.Disabled = ScanButton.Disabled;
- Information.SetMessage(message);
- }
+ UpdateArtifactIcon(item);
- public void UpdateProbeTickProgressBar(ReverseEngineeringMachineScanUpdateState state)
- {
- ProgressBar.Visible = state.Scanning;
- ProgressLabel.Visible = state.Scanning;
+ ProgressBox.Visible = scanning;
- if (!state.Scanning)
+ if (!_entMan.TryGetComponent(_owner, out var active)
+ || !_entMan.TryGetComponent(item, out var rev))
return;
- if (state.Target != null)
- {
- TotalProgressLabel.Visible = true;
- TotalProgressLabel.Text = Loc.GetString("reverse-engineering-total-progress-label");
- TotalProgressBar.Visible = true;
- TotalProgressBar.Value = (float) state.TotalProgress / 100f;
- } else
- {
- TotalProgressLabel.Visible = false;
- TotalProgressBar.Visible = false;
- }
+ TotalProgressBar.Value = (float) rev.Progress;
- ProgressLabel.Text = Loc.GetString("analysis-console-progress-text",
- ("seconds", (int) state.TotalTime.TotalSeconds - (int) state.TimeRemaining.TotalSeconds));
- ProgressBar.Value = (float) state.TimeRemaining.Divide(state.TotalTime);
- }
-
- public override void Close()
- {
- base.Close();
+ var remaining = Math.Max(active.NextProbe.TotalSeconds - _timing.CurTime.TotalSeconds, 0.0);
+ ProgressLabel.Text = Loc.GetString("analysis-console-progress-text", ("seconds", (int) remaining));
+ ProgressBar.Value = 1f - (float) (remaining / _owner.Comp.AnalysisDuration.TotalSeconds);
}
}
-
diff --git a/Content.Client/Nyanotrasen/ReverseEngineering/ReverseEngineeringSystem.cs b/Content.Client/Nyanotrasen/ReverseEngineering/ReverseEngineeringSystem.cs
new file mode 100644
index 00000000000..6a95bbe4281
--- /dev/null
+++ b/Content.Client/Nyanotrasen/ReverseEngineering/ReverseEngineeringSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared.ReverseEngineering;
+
+namespace Content.Client.ReverseEngineering;
+
+public sealed class ReverseEngineeringSystem : SharedReverseEngineeringSystem;
diff --git a/Content.Client/Options/UI/OptionsMenu.xaml b/Content.Client/Options/UI/OptionsMenu.xaml
index 69daaa2cea7..9278752f6ab 100644
--- a/Content.Client/Options/UI/OptionsMenu.xaml
+++ b/Content.Client/Options/UI/OptionsMenu.xaml
@@ -8,7 +8,6 @@
-
diff --git a/Content.Client/Options/UI/OptionsMenu.xaml.cs b/Content.Client/Options/UI/OptionsMenu.xaml.cs
index bb2c1ce0ed9..4a44b7d6493 100644
--- a/Content.Client/Options/UI/OptionsMenu.xaml.cs
+++ b/Content.Client/Options/UI/OptionsMenu.xaml.cs
@@ -19,8 +19,7 @@ public OptionsMenu()
Tabs.SetTabTitle(1, Loc.GetString("ui-options-tab-graphics"));
Tabs.SetTabTitle(2, Loc.GetString("ui-options-tab-controls"));
Tabs.SetTabTitle(3, Loc.GetString("ui-options-tab-audio"));
- Tabs.SetTabTitle(4, Loc.GetString("ui-options-tab-network"));
- Tabs.SetTabTitle(5, Loc.GetString("ui-options-tab-deltav")); // DeltaV specific settings
+ Tabs.SetTabTitle(4, Loc.GetString("ui-options-tab-deltav")); // DeltaV specific settings
UpdateTabs();
}
diff --git a/Content.Client/Options/UI/Tabs/NetworkTab.xaml b/Content.Client/Options/UI/Tabs/NetworkTab.xaml
deleted file mode 100644
index d010f0bd314..00000000000
--- a/Content.Client/Options/UI/Tabs/NetworkTab.xaml
+++ /dev/null
@@ -1,102 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Content.Client/Options/UI/Tabs/NetworkTab.xaml.cs b/Content.Client/Options/UI/Tabs/NetworkTab.xaml.cs
deleted file mode 100644
index 0cdc3b53fb8..00000000000
--- a/Content.Client/Options/UI/Tabs/NetworkTab.xaml.cs
+++ /dev/null
@@ -1,125 +0,0 @@
-using System.Globalization;
-using Robust.Client.AutoGenerated;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.XAML;
-using Robust.Shared;
-using Robust.Shared.Configuration;
-using Robust.Client.GameStates;
-using Content.Client.Entry;
-
-namespace Content.Client.Options.UI.Tabs
-{
- [GenerateTypedNameReferences]
- public sealed partial class NetworkTab : Control
- {
- [Dependency] private readonly IConfigurationManager _cfg = default!;
- [Dependency] private readonly IClientGameStateManager _stateMan = default!;
-
- public NetworkTab()
- {
-
- RobustXamlLoader.Load(this);
- IoCManager.InjectDependencies(this);
-
- ApplyButton.OnPressed += OnApplyButtonPressed;
- ResetButton.OnPressed += OnResetButtonPressed;
- DefaultButton.OnPressed += OnDefaultButtonPressed;
- NetPredictCheckbox.OnToggled += OnPredictToggled;
- NetInterpRatioSlider.OnValueChanged += OnSliderChanged;
- NetInterpRatioSlider.MinValue = _stateMan.MinBufferSize;
- NetPredictTickBiasSlider.OnValueChanged += OnSliderChanged;
- NetPvsSpawnSlider.OnValueChanged += OnSliderChanged;
- NetPvsEntrySlider.OnValueChanged += OnSliderChanged;
- NetPvsLeaveSlider.OnValueChanged += OnSliderChanged;
-
- Reset();
- }
-
- protected override void Dispose(bool disposing)
- {
- ApplyButton.OnPressed -= OnApplyButtonPressed;
- ResetButton.OnPressed -= OnResetButtonPressed;
- DefaultButton.OnPressed -= OnDefaultButtonPressed;
- NetPredictCheckbox.OnToggled -= OnPredictToggled;
- NetInterpRatioSlider.OnValueChanged -= OnSliderChanged;
- NetPredictTickBiasSlider.OnValueChanged -= OnSliderChanged;
- NetPvsSpawnSlider.OnValueChanged -= OnSliderChanged;
- NetPvsEntrySlider.OnValueChanged -= OnSliderChanged;
- NetPvsLeaveSlider.OnValueChanged -= OnSliderChanged;
- base.Dispose(disposing);
- }
-
- private void OnPredictToggled(BaseButton.ButtonToggledEventArgs obj)
- {
- UpdateChanges();
- }
-
- private void OnSliderChanged(Robust.Client.UserInterface.Controls.Range range)
- {
- UpdateChanges();
- }
-
- private void OnApplyButtonPressed(BaseButton.ButtonEventArgs args)
- {
- _cfg.SetCVar(CVars.NetBufferSize, (int) NetInterpRatioSlider.Value - _stateMan.MinBufferSize);
- _cfg.SetCVar(CVars.NetPredictTickBias, (int) NetPredictTickBiasSlider.Value);
- _cfg.SetCVar(CVars.NetPVSEntityBudget, (int) NetPvsSpawnSlider.Value);
- _cfg.SetCVar(CVars.NetPVSEntityEnterBudget, (int) NetPvsEntrySlider.Value);
- _cfg.SetCVar(CVars.NetPVSEntityExitBudget, (int) NetPvsLeaveSlider.Value);
- _cfg.SetCVar(CVars.NetPredict, NetPredictCheckbox.Pressed);
-
- _cfg.SaveToFile();
- UpdateChanges();
- }
-
- private void OnResetButtonPressed(BaseButton.ButtonEventArgs args)
- {
- Reset();
- }
-
- private void OnDefaultButtonPressed(BaseButton.ButtonEventArgs obj)
- {
- NetPredictTickBiasSlider.Value = CVars.NetPredictTickBias.DefaultValue;
- NetPvsSpawnSlider.Value = CVars.NetPVSEntityBudget.DefaultValue;
- NetPvsEntrySlider.Value = CVars.NetPVSEntityEnterBudget.DefaultValue;
- NetPvsLeaveSlider.Value = CVars.NetPVSEntityExitBudget.DefaultValue;
- NetInterpRatioSlider.Value = CVars.NetBufferSize.DefaultValue + _stateMan.MinBufferSize;
-
- UpdateChanges();
- }
-
- private void Reset()
- {
- NetInterpRatioSlider.Value = _cfg.GetCVar(CVars.NetBufferSize) + _stateMan.MinBufferSize;
- NetPredictTickBiasSlider.Value = _cfg.GetCVar(CVars.NetPredictTickBias);
- NetPvsSpawnSlider.Value = _cfg.GetCVar(CVars.NetPVSEntityBudget);
- NetPvsEntrySlider.Value = _cfg.GetCVar(CVars.NetPVSEntityEnterBudget);
- NetPvsLeaveSlider.Value = _cfg.GetCVar(CVars.NetPVSEntityExitBudget);
- NetPredictCheckbox.Pressed = _cfg.GetCVar(CVars.NetPredict);
- UpdateChanges();
- }
-
- private void UpdateChanges()
- {
- var isEverythingSame =
- NetInterpRatioSlider.Value == _cfg.GetCVar(CVars.NetBufferSize) + _stateMan.MinBufferSize &&
- NetPredictTickBiasSlider.Value == _cfg.GetCVar(CVars.NetPredictTickBias) &&
- NetPredictCheckbox.Pressed == _cfg.GetCVar(CVars.NetPredict) &&
- NetPvsSpawnSlider.Value == _cfg.GetCVar(CVars.NetPVSEntityBudget) &&
- NetPvsEntrySlider.Value == _cfg.GetCVar(CVars.NetPVSEntityEnterBudget) &&
- NetPvsLeaveSlider.Value == _cfg.GetCVar(CVars.NetPVSEntityExitBudget);
-
- ApplyButton.Disabled = isEverythingSame;
- ResetButton.Disabled = isEverythingSame;
- NetInterpRatioLabel.Text = NetInterpRatioSlider.Value.ToString(CultureInfo.InvariantCulture);
- NetPredictTickBiasLabel.Text = NetPredictTickBiasSlider.Value.ToString(CultureInfo.InvariantCulture);
- NetPvsSpawnLabel.Text = NetPvsSpawnSlider.Value.ToString(CultureInfo.InvariantCulture);
- NetPvsEntryLabel.Text = NetPvsEntrySlider.Value.ToString(CultureInfo.InvariantCulture);
- NetPvsLeaveLabel.Text = NetPvsLeaveSlider.Value.ToString(CultureInfo.InvariantCulture);
-
- // TODO disable / grey-out the predict and interp sliders if prediction is disabled.
- // Currently no option to do this, but should be added to the slider control in general
- }
- }
-}
diff --git a/Content.Client/Overlays/EntityHealthBarOverlay.cs b/Content.Client/Overlays/EntityHealthBarOverlay.cs
index c1c0ae93ec1..2b2ff14a22b 100644
--- a/Content.Client/Overlays/EntityHealthBarOverlay.cs
+++ b/Content.Client/Overlays/EntityHealthBarOverlay.cs
@@ -1,15 +1,14 @@
+using System.Numerics;
+using Content.Client.UserInterface.Systems;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
+using Content.Shared.StatusIcon.Components;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
-using System.Numerics;
-using Content.Shared.StatusIcon.Components;
-using Content.Client.UserInterface.Systems;
-using Robust.Shared.Prototypes;
using static Robust.Shared.Maths.Color;
namespace Content.Client.Overlays;
@@ -79,6 +78,10 @@ protected override void Draw(in OverlayDrawArgs args)
continue;
}
+ // we are all progressing towards death every day
+ if (CalcProgress(uid, mobStateComponent, damageableComponent, mobThresholdsComponent) is not { } deathProgress)
+ continue;
+
var worldPosition = _transform.GetWorldPosition(xform);
var worldMatrix = Matrix3.CreateTranslation(worldPosition);
@@ -91,10 +94,6 @@ protected override void Draw(in OverlayDrawArgs args)
var widthOfMob = bounds.Width * EyeManager.PixelsPerMeter;
var position = new Vector2(-widthOfMob / EyeManager.PixelsPerMeter / 2, yOffset / EyeManager.PixelsPerMeter);
-
- // we are all progressing towards death every day
- (float ratio, bool inCrit) deathProgress = CalcProgress(uid, mobStateComponent, damageableComponent, mobThresholdsComponent);
-
var color = GetProgressColor(deathProgress.ratio, deathProgress.inCrit);
// Hardcoded width of the progress bar because it doesn't match the texture.
@@ -122,10 +121,13 @@ protected override void Draw(in OverlayDrawArgs args)
///
/// Returns a ratio between 0 and 1, and whether the entity is in crit.
///
- private (float, bool) CalcProgress(EntityUid uid, MobStateComponent component, DamageableComponent dmg, MobThresholdsComponent thresholds)
+ private (float ratio, bool inCrit)? CalcProgress(EntityUid uid, MobStateComponent component, DamageableComponent dmg, MobThresholdsComponent thresholds)
{
if (_mobStateSystem.IsAlive(uid, component))
{
+ if (dmg.HealthBarThreshold != null && dmg.TotalDamage < dmg.HealthBarThreshold)
+ return null;
+
if (!_mobThresholdSystem.TryGetThresholdForState(uid, MobState.Critical, out var threshold, thresholds) &&
!_mobThresholdSystem.TryGetThresholdForState(uid, MobState.Dead, out threshold, thresholds))
return (1, false);
diff --git a/Content.Client/Pinpointer/NavMapSystem.cs b/Content.Client/Pinpointer/NavMapSystem.cs
index e33bc5d3291..9aeb792a429 100644
--- a/Content.Client/Pinpointer/NavMapSystem.cs
+++ b/Content.Client/Pinpointer/NavMapSystem.cs
@@ -14,27 +14,40 @@ public override void Initialize()
private void OnHandleState(EntityUid uid, NavMapComponent component, ref ComponentHandleState args)
{
- if (args.Current is not NavMapComponentState state)
- return;
+ Dictionary modifiedChunks;
+ Dictionary beacons;
- if (!state.FullState)
+ switch (args.Current)
{
- foreach (var index in component.Chunks.Keys)
+ case NavMapDeltaState delta:
{
- if (!state.AllChunks!.Contains(index))
- component.Chunks.Remove(index);
+ modifiedChunks = delta.ModifiedChunks;
+ beacons = delta.Beacons;
+ foreach (var index in component.Chunks.Keys)
+ {
+ if (!delta.AllChunks!.Contains(index))
+ component.Chunks.Remove(index);
+ }
+
+ break;
}
- }
- else
- {
- foreach (var index in component.Chunks.Keys)
+ case NavMapState state:
{
- if (!state.Chunks.ContainsKey(index))
- component.Chunks.Remove(index);
+ modifiedChunks = state.Chunks;
+ beacons = state.Beacons;
+ foreach (var index in component.Chunks.Keys)
+ {
+ if (!state.Chunks.ContainsKey(index))
+ component.Chunks.Remove(index);
+ }
+
+ break;
}
+ default:
+ return;
}
- foreach (var (origin, chunk) in state.Chunks)
+ foreach (var (origin, chunk) in modifiedChunks)
{
var newChunk = new NavMapChunk(origin);
Array.Copy(chunk, newChunk.TileData, chunk.Length);
@@ -42,7 +55,7 @@ private void OnHandleState(EntityUid uid, NavMapComponent component, ref Compone
}
component.Beacons.Clear();
- foreach (var (nuid, beacon) in state.Beacons)
+ foreach (var (nuid, beacon) in beacons)
{
component.Beacons[nuid] = beacon;
}
diff --git a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
index 58b48aa7d12..80683fae711 100644
--- a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
+++ b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.CCVar;
using Content.Shared.Players;
+using Content.Shared.Players.JobWhitelist;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Roles;
using Robust.Client;
@@ -24,6 +25,7 @@ public sealed partial class JobRequirementsManager : ISharedPlaytimeManager
private readonly Dictionary _roles = new();
private readonly List _roleBans = new();
+ private readonly List _jobWhitelists = new();
private ISawmill _sawmill = default!;
@@ -36,6 +38,7 @@ public void Initialize()
// Yeah the client manager handles role bans and playtime but the server ones are separate DEAL.
_net.RegisterNetMessage(RxRoleBans);
_net.RegisterNetMessage(RxPlayTime);
+ _net.RegisterNetMessage(RxJobWhitelist);
_net.RegisterNetMessage(RxWhitelist);
_client.RunLevelChanged += ClientOnRunLevelChanged;
@@ -80,6 +83,13 @@ private void RxPlayTime(MsgPlayTime message)
Updated?.Invoke();
}
+ private void RxJobWhitelist(MsgJobWhitelist message)
+ {
+ _jobWhitelists.Clear();
+ _jobWhitelists.AddRange(message.Whitelist);
+ Updated?.Invoke();
+ }
+
public bool IsAllowed(JobPrototype job, [NotNullWhen(false)] out FormattedMessage? reason)
{
reason = null;
@@ -90,6 +100,9 @@ public bool IsAllowed(JobPrototype job, [NotNullWhen(false)] out FormattedMessag
return false;
}
+ if (!CheckWhitelist(job, out reason))
+ return false;
+
var player = _playerManager.LocalSession;
if (player == null)
return true;
@@ -117,6 +130,21 @@ public bool CheckRoleTime(HashSet? requirements, [NotNullWhen(fa
return reason == null;
}
+ public bool CheckWhitelist(JobPrototype job, [NotNullWhen(false)] out FormattedMessage? reason)
+ {
+ reason = default;
+ if (!_cfg.GetCVar(CCVars.GameRoleWhitelist))
+ return true;
+
+ if (job.Whitelisted && !_jobWhitelists.Contains(job.ID))
+ {
+ reason = FormattedMessage.FromUnformatted(Loc.GetString("role-not-whitelisted"));
+ return false;
+ }
+
+ return true;
+ }
+
public TimeSpan FetchOverallPlaytime()
{
return _roles.TryGetValue("Overall", out var overallPlaytime) ? overallPlaytime : TimeSpan.Zero;
diff --git a/Content.Client/Popups/PopupSystem.cs b/Content.Client/Popups/PopupSystem.cs
index 1ef8dfba2d1..700f6b6d26f 100644
--- a/Content.Client/Popups/PopupSystem.cs
+++ b/Content.Client/Popups/PopupSystem.cs
@@ -1,18 +1,20 @@
using System.Linq;
+using Content.Shared.Containers;
using Content.Shared.Examine;
using Content.Shared.GameTicking;
using Content.Shared.Popups;
+using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.UserInterface;
+using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Replays;
using Robust.Shared.Timing;
-using Robust.Shared.Utility;
namespace Content.Client.Popups
{
@@ -29,11 +31,11 @@ public sealed class PopupSystem : SharedPopupSystem
[Dependency] private readonly ExamineSystemShared _examine = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
- public IReadOnlyList WorldLabels => _aliveWorldLabels;
- public IReadOnlyList CursorLabels => _aliveCursorLabels;
+ public IReadOnlyCollection WorldLabels => _aliveWorldLabels.Values;
+ public IReadOnlyCollection CursorLabels => _aliveCursorLabels.Values;
- private readonly List _aliveWorldLabels = new();
- private readonly List _aliveCursorLabels = new();
+ private readonly Dictionary _aliveWorldLabels = new();
+ private readonly Dictionary _aliveCursorLabels = new();
public const float MinimumPopupLifetime = 0.7f;
public const float MaximumPopupLifetime = 5f;
@@ -65,6 +67,15 @@ public override void Shutdown()
.RemoveOverlay();
}
+ private void WrapAndRepeatPopup(PopupLabel existingLabel, string popupMessage)
+ {
+ existingLabel.TotalTime = 0;
+ existingLabel.Repeats += 1;
+ existingLabel.Text = Loc.GetString("popup-system-repeated-popup-stacking-wrap",
+ ("popup-message", popupMessage),
+ ("count", existingLabel.Repeats));
+ }
+
private void PopupMessage(string? message, PopupType type, EntityCoordinates coordinates, EntityUid? entity, bool recordReplay)
{
if (message == null)
@@ -78,13 +89,20 @@ private void PopupMessage(string? message, PopupType type, EntityCoordinates coo
_replayRecording.RecordClientMessage(new PopupCoordinatesEvent(message, type, GetNetCoordinates(coordinates)));
}
+ var popupData = new WorldPopupData(message, type, coordinates, entity);
+ if (_aliveWorldLabels.TryGetValue(popupData, out var existingLabel))
+ {
+ WrapAndRepeatPopup(existingLabel, popupData.Message);
+ return;
+ }
+
var label = new WorldPopupLabel(coordinates)
{
Text = message,
Type = type,
};
- _aliveWorldLabels.Add(label);
+ _aliveWorldLabels.Add(popupData, label);
}
#region Abstract Method Implementations
@@ -113,13 +131,20 @@ private void PopupCursorInternal(string? message, PopupType type, bool recordRep
if (recordReplay && _replayRecording.IsRecording)
_replayRecording.RecordClientMessage(new PopupCursorEvent(message, type));
+ var popupData = new CursorPopupData(message, type);
+ if (_aliveCursorLabels.TryGetValue(popupData, out var existingLabel))
+ {
+ WrapAndRepeatPopup(existingLabel, popupData.Message);
+ return;
+ }
+
var label = new CursorPopupLabel(_inputManager.MouseScreenPosition)
{
Text = message,
Type = type,
};
- _aliveCursorLabels.Add(label);
+ _aliveCursorLabels.Add(popupData, label);
}
public override void PopupCursor(string? message, PopupType type = PopupType.Small)
@@ -249,27 +274,37 @@ public override void FrameUpdate(float frameTime)
if (_aliveWorldLabels.Count == 0 && _aliveCursorLabels.Count == 0)
return;
- for (var i = 0; i < _aliveWorldLabels.Count; i++)
+ if (_aliveWorldLabels.Count > 0)
{
- var label = _aliveWorldLabels[i];
- label.TotalTime += frameTime;
-
- if (label.TotalTime > GetPopupLifetime(label) || Deleted(label.InitialPos.EntityId))
+ var aliveWorldToRemove = new ValueList();
+ foreach (var (data, label) in _aliveWorldLabels)
+ {
+ label.TotalTime += frameTime;
+ if (label.TotalTime > GetPopupLifetime(label) || Deleted(label.InitialPos.EntityId))
+ {
+ aliveWorldToRemove.Add(data);
+ }
+ }
+ foreach (var data in aliveWorldToRemove)
{
- _aliveWorldLabels.RemoveSwap(i);
- i--;
+ _aliveWorldLabels.Remove(data);
}
}
- for (var i = 0; i < _aliveCursorLabels.Count; i++)
+ if (_aliveCursorLabels.Count > 0)
{
- var label = _aliveCursorLabels[i];
- label.TotalTime += frameTime;
-
- if (label.TotalTime > GetPopupLifetime(label))
+ var aliveCursorToRemove = new ValueList();
+ foreach (var (data, label) in _aliveCursorLabels)
+ {
+ label.TotalTime += frameTime;
+ if (label.TotalTime > GetPopupLifetime(label))
+ {
+ aliveCursorToRemove.Add(data);
+ }
+ }
+ foreach (var data in aliveCursorToRemove)
{
- _aliveCursorLabels.RemoveSwap(i);
- i--;
+ _aliveCursorLabels.Remove(data);
}
}
}
@@ -279,29 +314,32 @@ public abstract class PopupLabel
public PopupType Type = PopupType.Small;
public string Text { get; set; } = string.Empty;
public float TotalTime { get; set; }
+ public int Repeats = 1;
}
- public sealed class CursorPopupLabel : PopupLabel
- {
- public ScreenCoordinates InitialPos;
-
- public CursorPopupLabel(ScreenCoordinates screenCoords)
- {
- InitialPos = screenCoords;
- }
- }
-
- public sealed class WorldPopupLabel : PopupLabel
+ public sealed class WorldPopupLabel(EntityCoordinates coordinates) : PopupLabel
{
///
/// The original EntityCoordinates of the label.
///
- public EntityCoordinates InitialPos;
+ public EntityCoordinates InitialPos = coordinates;
+ }
- public WorldPopupLabel(EntityCoordinates coordinates)
- {
- InitialPos = coordinates;
- }
+ public sealed class CursorPopupLabel(ScreenCoordinates screenCoords) : PopupLabel
+ {
+ public ScreenCoordinates InitialPos = screenCoords;
}
+
+ [UsedImplicitly]
+ private record struct WorldPopupData(
+ string Message,
+ PopupType Type,
+ EntityCoordinates Coordinates,
+ EntityUid? Entity);
+
+ [UsedImplicitly]
+ private record struct CursorPopupData(
+ string Message,
+ PopupType Type);
}
}
diff --git a/Content.Client/Power/ActivatableUIRequiresPowerSystem.cs b/Content.Client/Power/ActivatableUIRequiresPowerSystem.cs
index 60ed8d87b9e..5a082485a5a 100644
--- a/Content.Client/Power/ActivatableUIRequiresPowerSystem.cs
+++ b/Content.Client/Power/ActivatableUIRequiresPowerSystem.cs
@@ -1,21 +1,27 @@
+using Content.Client.Power.EntitySystems;
+using Content.Shared.Popups;
using Content.Shared.Power.Components;
+using Content.Shared.Power.EntitySystems;
using Content.Shared.UserInterface;
using Content.Shared.Wires;
namespace Content.Client.Power;
-public sealed class ActivatableUIRequiresPowerSystem : EntitySystem
+public sealed class ActivatableUIRequiresPowerSystem : SharedActivatableUIRequiresPowerSystem
{
- public override void Initialize()
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+
+ protected override void OnActivate(Entity ent, ref ActivatableUIOpenAttemptEvent args)
{
- base.Initialize();
+ if (args.Cancelled || this.IsPowered(ent.Owner, EntityManager))
+ {
+ return;
+ }
- SubscribeLocalEvent(OnActivate);
- }
+ if (TryComp(ent.Owner, out var panel) && panel.Open)
+ return;
- private void OnActivate(EntityUid uid, ActivatableUIRequiresPowerComponent component, ActivatableUIOpenAttemptEvent args)
- {
- // Client can't predict the power properly at the moment so rely upon the server to do it.
+ _popup.PopupClient(Loc.GetString("base-computer-ui-component-not-powered", ("machine", ent.Owner)), args.User, args.User);
args.Cancel();
}
}
diff --git a/Content.Client/Power/Components/ApcPowerReceiverComponent.cs b/Content.Client/Power/Components/ApcPowerReceiverComponent.cs
new file mode 100644
index 00000000000..fbebcb7cf83
--- /dev/null
+++ b/Content.Client/Power/Components/ApcPowerReceiverComponent.cs
@@ -0,0 +1,8 @@
+using Content.Shared.Power.Components;
+
+namespace Content.Client.Power.Components;
+
+[RegisterComponent]
+public sealed partial class ApcPowerReceiverComponent : SharedApcPowerReceiverComponent
+{
+}
diff --git a/Content.Client/Power/EntitySystems/PowerReceiverSystem.cs b/Content.Client/Power/EntitySystems/PowerReceiverSystem.cs
new file mode 100644
index 00000000000..4d56592c232
--- /dev/null
+++ b/Content.Client/Power/EntitySystems/PowerReceiverSystem.cs
@@ -0,0 +1,23 @@
+using Content.Client.Power.Components;
+using Content.Shared.Power.Components;
+using Content.Shared.Power.EntitySystems;
+using Robust.Shared.GameStates;
+
+namespace Content.Client.Power.EntitySystems;
+
+public sealed class PowerReceiverSystem : SharedPowerReceiverSystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnHandleState);
+ }
+
+ private void OnHandleState(EntityUid uid, ApcPowerReceiverComponent component, ref ComponentHandleState args)
+ {
+ if (args.Current is not ApcPowerReceiverComponentState state)
+ return;
+
+ component.Powered = state.Powered;
+ }
+}
diff --git a/Content.Client/Power/EntitySystems/StaticPowerSystem.cs b/Content.Client/Power/EntitySystems/StaticPowerSystem.cs
new file mode 100644
index 00000000000..2ca945cbbd8
--- /dev/null
+++ b/Content.Client/Power/EntitySystems/StaticPowerSystem.cs
@@ -0,0 +1,16 @@
+using Content.Client.Power.Components;
+
+namespace Content.Client.Power.EntitySystems;
+
+public static class StaticPowerSystem
+{
+ // Using this makes the call shorter.
+ // ReSharper disable once UnusedParameter.Global
+ public static bool IsPowered(this EntitySystem system, EntityUid uid, IEntityManager entManager, ApcPowerReceiverComponent? receiver = null)
+ {
+ if (receiver == null && !entManager.TryGetComponent(uid, out receiver))
+ return false;
+
+ return receiver.Powered;
+ }
+}
diff --git a/Content.Client/Revenant/RevenantSystem.cs b/Content.Client/Revenant/RevenantSystem.cs
index 49d29d8a5f4..e050fe35aa2 100644
--- a/Content.Client/Revenant/RevenantSystem.cs
+++ b/Content.Client/Revenant/RevenantSystem.cs
@@ -1,5 +1,4 @@
using Content.Client.Alerts;
-using Content.Shared.Alert;
using Content.Shared.Revenant;
using Content.Shared.Revenant.Components;
using Robust.Client.GameObjects;
@@ -42,7 +41,7 @@ private void OnAppearanceChange(EntityUid uid, RevenantComponent component, ref
private void OnUpdateAlert(Entity ent, ref UpdateAlertSpriteEvent args)
{
- if (args.Alert.AlertType != AlertType.Essence)
+ if (args.Alert.ID != ent.Comp.EssenceAlert)
return;
var sprite = args.SpriteViewEnt.Comp;
diff --git a/Content.Client/Salvage/SalvageSystem.cs b/Content.Client/Salvage/SalvageSystem.cs
index fb305c5fdc4..e1bce367cae 100644
--- a/Content.Client/Salvage/SalvageSystem.cs
+++ b/Content.Client/Salvage/SalvageSystem.cs
@@ -38,7 +38,7 @@ private void OnPlayAmbientMusic(ref PlayAmbientMusicEvent ev)
var player = _playerManager.LocalEntity;
- if (!TryComp(player, out var xform) ||
+ if (!TryComp(player, out TransformComponent? xform) ||
!TryComp(xform.MapUid, out var expedition) ||
expedition.Stage < ExpeditionStage.MusicCountdown)
{
diff --git a/Content.Client/Sprite/SpriteFadeSystem.cs b/Content.Client/Sprite/SpriteFadeSystem.cs
index d9584b60a65..676a6e583d5 100644
--- a/Content.Client/Sprite/SpriteFadeSystem.cs
+++ b/Content.Client/Sprite/SpriteFadeSystem.cs
@@ -45,7 +45,7 @@ public override void FrameUpdate(float frameTime)
var spriteQuery = GetEntityQuery();
var change = ChangeRate * frameTime;
- if (TryComp(player, out var playerXform) &&
+ if (TryComp(player, out TransformComponent? playerXform) &&
_stateManager.CurrentState is GameplayState state &&
spriteQuery.TryGetComponent(player, out var playerSprite))
{
diff --git a/Content.Client/SurveillanceCamera/UI/SurveillanceCameraSetupBoundUi.cs b/Content.Client/SurveillanceCamera/UI/SurveillanceCameraSetupBoundUi.cs
index 1880e431eea..295724788e5 100644
--- a/Content.Client/SurveillanceCamera/UI/SurveillanceCameraSetupBoundUi.cs
+++ b/Content.Client/SurveillanceCamera/UI/SurveillanceCameraSetupBoundUi.cs
@@ -31,7 +31,7 @@ protected override void Open()
_window.OpenCentered();
_window.OnNameConfirm += SendDeviceName;
_window.OnNetworkConfirm += SendSelectedNetwork;
-
+ _window.OnClose += Close;
}
private void SendSelectedNetwork(int idx)
@@ -63,7 +63,8 @@ protected override void Dispose(bool disposing)
if (disposing)
{
- _window!.Dispose();
+ _window?.Dispose();
+ _window = null;
}
}
}
diff --git a/Content.Client/Tips/TippyUIController.cs b/Content.Client/Tips/TippyUIController.cs
index 67c3ee45a7e..2cc694d97d4 100644
--- a/Content.Client/Tips/TippyUIController.cs
+++ b/Content.Client/Tips/TippyUIController.cs
@@ -175,7 +175,7 @@ private void NextState(TippyUI tippy)
sprite.LayerSetVisible("hiding", false);
}
sprite.Rotation = 0;
- tippy.Label.SetMarkup(_currentMessage.Msg);
+ tippy.Label.SetMarkupPermissive(_currentMessage.Msg);
tippy.Label.Visible = false;
tippy.LabelPanel.Visible = false;
tippy.Visible = true;
diff --git a/Content.Client/UserInterface/Controls/ListContainer.cs b/Content.Client/UserInterface/Controls/ListContainer.cs
index 05ae0a4bb15..e1b3b948f04 100644
--- a/Content.Client/UserInterface/Controls/ListContainer.cs
+++ b/Content.Client/UserInterface/Controls/ListContainer.cs
@@ -8,7 +8,8 @@
namespace Content.Client.UserInterface.Controls;
-public sealed class ListContainer : Control
+[Virtual]
+public class ListContainer : Control
{
public const string StylePropertySeparation = "separation";
public const string StyleClassListContainerButton = "list-container-button";
@@ -21,9 +22,26 @@ public bool Group
set => _buttonGroup = value ? new ButtonGroup() : null;
}
public bool Toggle { get; set; }
+
+ ///
+ /// Called when creating a button on the UI.
+ /// The provided is the generated button that Controls should be parented to.
+ ///
public Action? GenerateItem;
- public Action? ItemPressed;
- public Action? ItemKeyBindDown;
+
+ ///
+ public Action? ItemPressed;
+
+ ///
+ /// Invoked when a KeyBind is pressed on a ListContainerButton.
+ ///
+ public Action? ItemKeyBindDown;
+
+ ///
+ /// Invoked when the selected item does not exist in the new data when PopulateList is called.
+ ///
+ public Action? NoItemSelected;
+
public IReadOnlyList Data => _data;
private const int DefaultSeparation = 3;
@@ -72,11 +90,11 @@ public ListContainer()
_vScrollBar.OnValueChanged += ScrollValueChanged;
}
- public void PopulateList(IReadOnlyList data)
+ public virtual void PopulateList(IReadOnlyList data)
{
if ((_itemHeight == 0 || _data is {Count: 0}) && data.Count > 0)
{
- ListContainerButton control = new(data[0]);
+ ListContainerButton control = new(data[0], 0);
GenerateItem?.Invoke(data[0], control);
control.Measure(Vector2Helpers.Infinity);
_itemHeight = control.DesiredSize.Y;
@@ -97,7 +115,7 @@ public void PopulateList(IReadOnlyList data)
if (_selected != null && !data.Contains(_selected))
{
_selected = null;
- ItemPressed?.Invoke(null, null);
+ NoItemSelected?.Invoke();
}
}
@@ -116,7 +134,7 @@ public void Select(ListData data)
if (_buttons.TryGetValue(data, out var button) && Toggle)
button.Pressed = true;
_selected = data;
- button ??= new ListContainerButton(data);
+ button ??= new ListContainerButton(data, _data.IndexOf(data));
OnItemPressed(new BaseButton.ButtonEventArgs(button,
new GUIBoundKeyEventArgs(EngineKeyFunctions.UIClick, BoundKeyState.Up,
new ScreenCoordinates(0, 0, WindowId.Main), true, Vector2.Zero, Vector2.Zero)));
@@ -260,7 +278,7 @@ protected override Vector2 ArrangeOverride(Vector2 finalSize)
toRemove.Remove(data);
else
{
- button = new ListContainerButton(data);
+ button = new ListContainerButton(data, i);
button.OnPressed += OnItemPressed;
button.OnKeyBindDown += args => OnItemKeyBindDown(button, args);
button.ToggleMode = Toggle;
@@ -360,11 +378,14 @@ protected override void MouseWheel(GUIMouseWheelEventArgs args)
public sealed class ListContainerButton : ContainerButton, IEntityControl
{
public readonly ListData Data;
+
+ public readonly int Index;
// public PanelContainer Background;
- public ListContainerButton(ListData data)
+ public ListContainerButton(ListData data, int index)
{
Data = data;
+ Index = index;
// AddChild(Background = new PanelContainer
// {
// HorizontalExpand = true,
diff --git a/Content.Client/UserInterface/Controls/SearchListContainer.cs b/Content.Client/UserInterface/Controls/SearchListContainer.cs
new file mode 100644
index 00000000000..603d7f184c0
--- /dev/null
+++ b/Content.Client/UserInterface/Controls/SearchListContainer.cs
@@ -0,0 +1,68 @@
+using System.Linq;
+using Robust.Client.UserInterface.Controls;
+
+namespace Content.Client.UserInterface.Controls;
+
+public sealed class SearchListContainer : ListContainer
+{
+ private LineEdit? _searchBar;
+ private List _unfilteredData = new();
+
+ ///
+ /// The that is used to filter the list data.
+ ///
+ public LineEdit? SearchBar
+ {
+ get => _searchBar;
+ set
+ {
+ if (_searchBar is not null)
+ _searchBar.OnTextChanged -= FilterList;
+
+ _searchBar = value;
+
+ if (_searchBar is null)
+ return;
+
+ _searchBar.OnTextChanged += FilterList;
+ }
+ }
+
+ ///
+ /// Runs over the ListData to determine if it should pass the filter.
+ ///
+ public Func? DataFilterCondition = null;
+
+ public override void PopulateList(IReadOnlyList data)
+ {
+ _unfilteredData = data.ToList();
+ FilterList();
+ }
+
+ private void FilterList(LineEdit.LineEditEventArgs obj)
+ {
+ FilterList();
+ }
+
+ private void FilterList()
+ {
+ var filterText = SearchBar?.Text;
+
+ if (DataFilterCondition is null || string.IsNullOrEmpty(filterText))
+ {
+ base.PopulateList(_unfilteredData);
+ return;
+ }
+
+ var filteredData = new List();
+ foreach (var data in _unfilteredData)
+ {
+ if (!DataFilterCondition(filterText, data))
+ continue;
+
+ filteredData.Add(data);
+ }
+
+ base.PopulateList(filteredData);
+ }
+}
diff --git a/Content.Client/UserInterface/Systems/Actions/Controls/ActionButtonContainer.cs b/Content.Client/UserInterface/Systems/Actions/Controls/ActionButtonContainer.cs
index c5f8adbdea3..38c08dc4721 100644
--- a/Content.Client/UserInterface/Systems/Actions/Controls/ActionButtonContainer.cs
+++ b/Content.Client/UserInterface/Systems/Actions/Controls/ActionButtonContainer.cs
@@ -119,9 +119,4 @@ public IEnumerable GetButtons()
yield return button;
}
}
-
- ~ActionButtonContainer()
- {
- UserInterfaceManager.GetUIController().RemoveActionContainer();
- }
}
diff --git a/Content.Client/UserInterface/Systems/Admin/AdminUIController.cs b/Content.Client/UserInterface/Systems/Admin/AdminUIController.cs
index cccd9201a29..a7397aff38d 100644
--- a/Content.Client/UserInterface/Systems/Admin/AdminUIController.cs
+++ b/Content.Client/UserInterface/Systems/Admin/AdminUIController.cs
@@ -177,12 +177,15 @@ private void Toggle()
}
}
- private void PlayerTabEntryKeyBindDown(PlayerTabEntry entry, GUIBoundKeyEventArgs args)
+ private void PlayerTabEntryKeyBindDown(GUIBoundKeyEventArgs args, ListData? data)
{
- if (entry.PlayerEntity == null)
+ if (data is not PlayerListData {Info: var info})
return;
- var entity = entry.PlayerEntity.Value;
+ if (info.NetEntity == null)
+ return;
+
+ var entity = info.NetEntity.Value;
var function = args.Function;
if (function == EngineKeyFunctions.UIClick)
diff --git a/Content.Client/UserInterface/Systems/Alerts/AlertsUIController.cs b/Content.Client/UserInterface/Systems/Alerts/AlertsUIController.cs
index 3b85972a9b2..5c195120389 100644
--- a/Content.Client/UserInterface/Systems/Alerts/AlertsUIController.cs
+++ b/Content.Client/UserInterface/Systems/Alerts/AlertsUIController.cs
@@ -7,6 +7,7 @@
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
+using Robust.Shared.Prototypes;
namespace Content.Client.UserInterface.Systems.Alerts;
@@ -43,7 +44,7 @@ private void OnScreenLoad()
SyncAlerts();
}
- private void OnAlertPressed(object? sender, AlertType e)
+ private void OnAlertPressed(object? sender, ProtoId e)
{
_alertsSystem?.AlertClicked(e);
}
diff --git a/Content.Client/UserInterface/Systems/Alerts/Widgets/AlertsUI.xaml.cs b/Content.Client/UserInterface/Systems/Alerts/Widgets/AlertsUI.xaml.cs
index a1a494c47b3..d6a79a81c46 100644
--- a/Content.Client/UserInterface/Systems/Alerts/Widgets/AlertsUI.xaml.cs
+++ b/Content.Client/UserInterface/Systems/Alerts/Widgets/AlertsUI.xaml.cs
@@ -4,6 +4,7 @@
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Input;
+using Robust.Shared.Prototypes;
namespace Content.Client.UserInterface.Systems.Alerts.Widgets;
@@ -21,8 +22,10 @@ public AlertsUI()
RobustXamlLoader.Load(this);
}
- public void SyncControls(AlertsSystem alertsSystem, AlertOrderPrototype? alertOrderPrototype,
- IReadOnlyDictionary alertStates)
+ public void SyncControls(AlertsSystem alertsSystem,
+ AlertOrderPrototype? alertOrderPrototype,
+ IReadOnlyDictionary alertStates)
{
// remove any controls with keys no longer present
if (SyncRemoveControls(alertStates))
@@ -46,7 +49,7 @@ public void ClearAllControls()
_alertControls.Clear();
}
- public event EventHandler? AlertPressed;
+ public event EventHandler>? AlertPressed;
private bool SyncRemoveControls(IReadOnlyDictionary alertStates)
{
@@ -88,7 +91,7 @@ private void SyncUpdateControls(AlertsSystem alertsSystem, AlertOrderPrototype?
}
if (_alertControls.TryGetValue(newAlert.AlertKey, out var existingAlertControl) &&
- existingAlertControl.Alert.AlertType == newAlert.AlertType)
+ existingAlertControl.Alert.ID == newAlert.ID)
{
// key is the same, simply update the existing control severity / cooldown
existingAlertControl.SetSeverity(alertState.Severity);
@@ -155,6 +158,6 @@ private void AlertControlPressed(BaseButton.ButtonEventArgs args)
if (args.Event.Function != EngineKeyFunctions.UIClick)
return;
- AlertPressed?.Invoke(this, control.Alert.AlertType);
+ AlertPressed?.Invoke(this, control.Alert.ID);
}
}
diff --git a/Content.Client/UserInterface/Systems/Inventory/Controls/ItemSlotButtonContainer.cs b/Content.Client/UserInterface/Systems/Inventory/Controls/ItemSlotButtonContainer.cs
index 78c3b40adac..7a027fc4f81 100644
--- a/Content.Client/UserInterface/Systems/Inventory/Controls/ItemSlotButtonContainer.cs
+++ b/Content.Client/UserInterface/Systems/Inventory/Controls/ItemSlotButtonContainer.cs
@@ -22,9 +22,4 @@ public ItemSlotButtonContainer()
{
_inventoryController = UserInterfaceManager.GetUIController();
}
-
- ~ItemSlotButtonContainer()
- {
- _inventoryController.RemoveSlotGroup(SlotGroup);
- }
}
diff --git a/Content.Client/UserInterface/Systems/Storage/Controls/ItemGridPiece.cs b/Content.Client/UserInterface/Systems/Storage/Controls/ItemGridPiece.cs
index f436cc8c39b..dd9986e4c6e 100644
--- a/Content.Client/UserInterface/Systems/Storage/Controls/ItemGridPiece.cs
+++ b/Content.Client/UserInterface/Systems/Storage/Controls/ItemGridPiece.cs
@@ -9,7 +9,7 @@
namespace Content.Client.UserInterface.Systems.Storage.Controls;
-public sealed class ItemGridPiece : Control
+public sealed class ItemGridPiece : Control, IEntityControl
{
private readonly IEntityManager _entityManager;
private readonly StorageUIController _storageController;
@@ -287,6 +287,8 @@ public static Vector2 GetCenterOffset(Entity entity, ItemStorage
var actualSize = new Vector2(boxSize.X + 1, boxSize.Y + 1);
return actualSize * new Vector2i(8, 8);
}
+
+ public EntityUid? UiEntity => Entity;
}
public enum ItemGridPieceMarks
diff --git a/Content.Client/Verbs/UI/VerbMenuUIController.cs b/Content.Client/Verbs/UI/VerbMenuUIController.cs
index c3fc8c83567..e9c3f90641f 100644
--- a/Content.Client/Verbs/UI/VerbMenuUIController.cs
+++ b/Content.Client/Verbs/UI/VerbMenuUIController.cs
@@ -8,6 +8,7 @@
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
+using Robust.Shared.Collections;
using Robust.Shared.Input;
using Robust.Shared.Utility;
@@ -115,6 +116,13 @@ public void OpenVerbMenu(NetEntity target, bool force = false, ContextMenuPopup?
private void FillVerbPopup(ContextMenuPopup popup)
{
HashSet listedCategories = new();
+ var extras = new ValueList(ExtraCategories.Count);
+
+ foreach (var cat in ExtraCategories)
+ {
+ extras.Add(cat.Text);
+ }
+
foreach (var verb in CurrentVerbs)
{
if (verb.Category == null)
@@ -122,17 +130,15 @@ private void FillVerbPopup(ContextMenuPopup popup)
var element = new VerbMenuElement(verb);
_context.AddElement(popup, element);
}
- else if (listedCategories.Add(verb.Category.Text))
+ // Add the category if it's not an extra (this is to avoid shuffling if we're filling from server verbs response).
+ else if (!extras.Contains(verb.Category.Text) && listedCategories.Add(verb.Category.Text))
AddVerbCategory(verb.Category, popup);
}
- if (ExtraCategories != null)
+ foreach (var category in ExtraCategories)
{
- foreach (var category in ExtraCategories)
- {
- if (listedCategories.Add(category.Text))
- AddVerbCategory(category, popup);
- }
+ if (listedCategories.Add(category.Text))
+ AddVerbCategory(category, popup);
}
popup.InvalidateMeasure();
diff --git a/Content.Client/Weapons/Misc/TetherGunSystem.cs b/Content.Client/Weapons/Misc/TetherGunSystem.cs
index 634dbd24e79..398aeabb839 100644
--- a/Content.Client/Weapons/Misc/TetherGunSystem.cs
+++ b/Content.Client/Weapons/Misc/TetherGunSystem.cs
@@ -82,7 +82,7 @@ public override void Update(float frameTime)
const float BufferDistance = 0.1f;
- if (TryComp(gun.TetherEntity, out var tetherXform) &&
+ if (TryComp(gun.TetherEntity, out TransformComponent? tetherXform) &&
tetherXform.Coordinates.TryDistance(EntityManager, TransformSystem, coords, out var distance) &&
distance < BufferDistance)
{
diff --git a/Content.IntegrationTests/Pair/TestPair.Cvars.cs b/Content.IntegrationTests/Pair/TestPair.Cvars.cs
new file mode 100644
index 00000000000..81df31fc9a3
--- /dev/null
+++ b/Content.IntegrationTests/Pair/TestPair.Cvars.cs
@@ -0,0 +1,69 @@
+#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 0ea6d3e2dcc..cc83232a066 100644
--- a/Content.IntegrationTests/Pair/TestPair.Helpers.cs
+++ b/Content.IntegrationTests/Pair/TestPair.Helpers.cs
@@ -2,6 +2,9 @@
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.Prototypes;
@@ -128,4 +131,29 @@ public List GetPrototypesWithComponent(
return list;
}
+
+ ///
+ /// Helper method for enabling or disabling a antag role
+ ///
+ public async Task SetAntagPref(ProtoId id, bool value)
+ {
+ var prefMan = Server.ResolveDependency();
+
+ var prefs = prefMan.GetPreferences(Client.User!.Value);
+ // what even is the point of ICharacterProfile if we always cast it to HumanoidCharacterProfile to make it usable?
+ var profile = (HumanoidCharacterProfile) prefs.SelectedCharacter;
+
+ Assert.That(profile.AntagPreferences.Contains(id), Is.EqualTo(!value));
+ var newProfile = profile.WithAntagPreference(id, value);
+
+ await Server.WaitPost(() =>
+ {
+ prefMan.SetProfile(Client.User.Value, prefs.SelectedCharacterIndex, newProfile).Wait();
+ });
+
+ // And why the fuck does it always create a new preference and profile object instead of just reusing them?
+ var newPrefs = prefMan.GetPreferences(Client.User.Value);
+ var newProf = (HumanoidCharacterProfile) newPrefs.SelectedCharacter;
+ Assert.That(newProf.AntagPreferences.Contains(id), Is.EqualTo(value));
+ }
}
diff --git a/Content.IntegrationTests/Pair/TestPair.Recycle.cs b/Content.IntegrationTests/Pair/TestPair.Recycle.cs
index c0f4b3b745f..8d1e425553b 100644
--- a/Content.IntegrationTests/Pair/TestPair.Recycle.cs
+++ b/Content.IntegrationTests/Pair/TestPair.Recycle.cs
@@ -40,6 +40,8 @@ private async Task OnCleanDispose()
TestMap = null;
}
+ await RevertModifiedCvars();
+
var usageTime = Watch.Elapsed;
Watch.Restart();
await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Test borrowed pair {Id} for {usageTime.TotalMilliseconds} ms");
@@ -132,6 +134,7 @@ public async Task CleanPooledPair(PoolSettings settings, TextWriter testOut)
if (gameTicker.RunLevel != GameRunLevel.PreRoundLobby)
{
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Restarting round.");
+ Server.CfgMan.SetCVar(CCVars.GameDummyTicker, false);
Assert.That(gameTicker.DummyTicker, Is.False);
Server.CfgMan.SetCVar(CCVars.GameLobbyEnabled, true);
await Server.WaitPost(() => gameTicker.RestartRound());
diff --git a/Content.IntegrationTests/Pair/TestPair.cs b/Content.IntegrationTests/Pair/TestPair.cs
index 916a94c9c4c..7ee5dbd55c7 100644
--- a/Content.IntegrationTests/Pair/TestPair.cs
+++ b/Content.IntegrationTests/Pair/TestPair.cs
@@ -4,6 +4,7 @@
using System.Linq;
using Content.Server.GameTicking;
using Content.Shared.Players;
+using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Network;
@@ -58,6 +59,9 @@ public async Task Initialize(PoolSettings settings, TextWriter testOut, List _contentAssemblies = default!;
+
public static async Task<(RobustIntegrationTest.ServerIntegrationInstance, PoolTestLogHandler)> GenerateServer(
PoolSettings poolSettings,
TextWriter testOut)
@@ -54,12 +56,7 @@ public static partial class PoolManager
LoadConfigAndUserData = false,
LoadContentResources = !poolSettings.NoLoadContent,
},
- ContentAssemblies = new[]
- {
- typeof(Shared.Entry.EntryPoint).Assembly,
- typeof(Server.Entry.EntryPoint).Assembly,
- typeof(PoolManager).Assembly
- }
+ ContentAssemblies = _contentAssemblies.ToArray()
};
var logHandler = new PoolTestLogHandler("SERVER");
@@ -68,11 +65,11 @@ public static partial class PoolManager
options.BeforeStart += () =>
{
+ // Server-only systems (i.e., systems that subscribe to events with server-only components)
var entSysMan = IoCManager.Resolve();
- entSysMan.LoadExtraSystemType();
- entSysMan.LoadExtraSystemType();
entSysMan.LoadExtraSystemType();
entSysMan.LoadExtraSystemType();
+
IoCManager.Resolve().GetSawmill("loc").Level = LogLevel.Error;
IoCManager.Resolve()
.OnValueChanged(RTCVars.FailureLogLevel, value => logHandler.FailureLevel = value, true);
@@ -140,7 +137,7 @@ public static string DeathReport()
{
typeof(Shared.Entry.EntryPoint).Assembly,
typeof(Client.Entry.EntryPoint).Assembly,
- typeof(PoolManager).Assembly
+ typeof(PoolManager).Assembly,
}
};
@@ -273,6 +270,8 @@ await testOut.WriteLineAsync(
$"{nameof(GetServerClientPair)}: Retrieving pair {pair.Id} from pool took {poolRetrieveTime.TotalMilliseconds} ms");
await testOut.WriteLineAsync(
$"{nameof(GetServerClientPair)}: Returning pair {pair.Id}");
+
+ pair.ClearModifiedCvars();
pair.Settings = poolSettings;
pair.TestHistory.Add(currentTestName);
pair.Watch.Restart();
@@ -422,13 +421,26 @@ public static async Task WaitUntil(RobustIntegrationTest.IntegrationInstance ins
///
/// Initialize the pool manager.
///
- /// Assembly to search for to discover extra test prototypes.
- public static void Startup(Assembly? assembly)
+ /// Assemblies to search for to discover extra prototypes and systems.
+ public static void Startup(params Assembly[] extraAssemblies)
{
if (_initialized)
throw new InvalidOperationException("Already initialized");
_initialized = true;
- DiscoverTestPrototypes(assembly);
+ _contentAssemblies =
+ [
+ typeof(Shared.Entry.EntryPoint).Assembly,
+ typeof(Server.Entry.EntryPoint).Assembly,
+ typeof(PoolManager).Assembly
+ ];
+ _contentAssemblies.UnionWith(extraAssemblies);
+
+ _testPrototypes.Clear();
+ DiscoverTestPrototypes(typeof(PoolManager).Assembly);
+ foreach (var assembly in extraAssemblies)
+ {
+ DiscoverTestPrototypes(assembly);
+ }
}
}
diff --git a/Content.IntegrationTests/PoolManagerTestEventHandler.cs b/Content.IntegrationTests/PoolManagerTestEventHandler.cs
index d37dffff50a..3b26d6637fd 100644
--- a/Content.IntegrationTests/PoolManagerTestEventHandler.cs
+++ b/Content.IntegrationTests/PoolManagerTestEventHandler.cs
@@ -13,7 +13,7 @@ public sealed class PoolManagerTestEventHandler
[OneTimeSetUp]
public void Setup()
{
- PoolManager.Startup(typeof(PoolManagerTestEventHandler).Assembly);
+ PoolManager.Startup();
// If the tests seem to be stuck, we try to end it semi-nicely
_ = Task.Delay(MaximumTotalTestingTimeLimit).ContinueWith(_ =>
{
diff --git a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs
index 7c700d9fb8a..57ac63b1247 100644
--- a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs
+++ b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs
@@ -29,6 +29,7 @@ public sealed class BuckleTest
components:
- type: Buckle
- type: Hands
+ - type: ComplexInteraction
- type: InputMover
- type: Body
prototype: Human
diff --git a/Content.IntegrationTests/Tests/DeltaV/ReverseEngineeringTest.cs b/Content.IntegrationTests/Tests/DeltaV/ReverseEngineeringTest.cs
new file mode 100644
index 00000000000..fd386851856
--- /dev/null
+++ b/Content.IntegrationTests/Tests/DeltaV/ReverseEngineeringTest.cs
@@ -0,0 +1,66 @@
+using System.Collections.Generic;
+using Content.Shared.Lathe;
+using Content.Shared.ReverseEngineering;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.DeltaV;
+
+[TestFixture]
+public sealed class ReverseEngineeringTest
+{
+ [Test]
+ public async Task AllReverseEngineeredPrintableTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var protoManager = server.ResolveDependency();
+
+ await server.WaitAssertion(() =>
+ {
+ var lathes = new List();
+ var reverseEngineered = new HashSet();
+ foreach (var proto in protoManager.EnumeratePrototypes())
+ {
+ if (proto.Abstract)
+ continue;
+
+ if (pair.IsTestPrototype(proto))
+ continue;
+
+ if (proto.TryGetComponent(out var lathe))
+ lathes.Add(lathe);
+
+ if (!proto.TryGetComponent(out var rev))
+ continue;
+
+ foreach (var recipe in rev.Recipes)
+ {
+ reverseEngineered.Add(recipe);
+ }
+ }
+
+ var latheRecipes = new HashSet();
+ foreach (var lathe in lathes)
+ {
+ if (lathe.DynamicRecipes == null)
+ continue;
+
+ foreach (var recipe in lathe.DynamicRecipes)
+ {
+ latheRecipes.Add(recipe);
+ }
+ }
+
+ Assert.Multiple(() =>
+ {
+ foreach (var recipe in reverseEngineered)
+ {
+ Assert.That(latheRecipes, Does.Contain(recipe), $"Reverse engineered recipe \"{recipe}\" cannot be unlocked on any lathe.");
+ }
+ });
+ });
+
+ await pair.CleanReturnAsync();
+ }
+}
diff --git a/Content.IntegrationTests/Tests/EntityTest.cs b/Content.IntegrationTests/Tests/EntityTest.cs
index d3b1fb47221..54af64122be 100644
--- a/Content.IntegrationTests/Tests/EntityTest.cs
+++ b/Content.IntegrationTests/Tests/EntityTest.cs
@@ -350,8 +350,12 @@ public async Task AllComponentsOneToOneDeleteTest()
"DebrisFeaturePlacerController", // Above.
"LoadedChunk", // Worldgen chunk loading malding.
"BiomeSelection", // Whaddya know, requires config.
+ "ActivatableUI", // Requires enum key
};
+ // TODO TESTS
+ // auto ignore any components that have a "required" data field.
+
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entityManager = server.ResolveDependency();
diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs b/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs
index c6a8e618cc1..0ac6b68a3ec 100644
--- a/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs
+++ b/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs
@@ -24,6 +24,7 @@ public sealed class HandCuffTest
components:
- type: Cuffable
- type: Hands
+ - type: ComplexInteraction
- type: Body
prototype: Human
diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs b/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs
index 1da77ac5589..ef4e6326cda 100644
--- a/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs
+++ b/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs
@@ -5,7 +5,6 @@
using Robust.Client.UserInterface;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs
{
@@ -45,8 +44,8 @@ await server.WaitAssertion(() =>
Assert.That(alerts, Is.Not.Null);
var alertCount = alerts.Count;
- alertsSystem.ShowAlert(playerUid, AlertType.Debug1);
- alertsSystem.ShowAlert(playerUid, AlertType.Debug2);
+ alertsSystem.ShowAlert(playerUid, "Debug1");
+ alertsSystem.ShowAlert(playerUid, "Debug2");
Assert.That(alerts, Has.Count.EqualTo(alertCount + 2));
});
@@ -87,14 +86,14 @@ static AlertsUI FindAlertsUI(Control control)
// we should be seeing 3 alerts - our health, and the 2 debug alerts, in a specific order.
Assert.That(clientAlertsUI.AlertContainer.ChildCount, Is.GreaterThanOrEqualTo(3));
var alertControls = clientAlertsUI.AlertContainer.Children.Select(c => (AlertControl) c);
- var alertIDs = alertControls.Select(ac => ac.Alert.AlertType).ToArray();
- var expectedIDs = new[] { AlertType.HumanHealth, AlertType.Debug1, AlertType.Debug2 };
+ var alertIDs = alertControls.Select(ac => ac.Alert.ID).ToArray();
+ var expectedIDs = new[] { "HumanHealth", "Debug1", "Debug2" };
Assert.That(alertIDs, Is.SupersetOf(expectedIDs));
});
await server.WaitAssertion(() =>
{
- alertsSystem.ClearAlert(playerUid, AlertType.Debug1);
+ alertsSystem.ClearAlert(playerUid, "Debug1");
});
await pair.RunTicksSync(5);
@@ -104,8 +103,8 @@ await client.WaitAssertion(() =>
// we should be seeing 2 alerts now because one was cleared
Assert.That(clientAlertsUI.AlertContainer.ChildCount, Is.GreaterThanOrEqualTo(2));
var alertControls = clientAlertsUI.AlertContainer.Children.Select(c => (AlertControl) c);
- var alertIDs = alertControls.Select(ac => ac.Alert.AlertType).ToArray();
- var expectedIDs = new[] { AlertType.HumanHealth, AlertType.Debug2 };
+ var alertIDs = alertControls.Select(ac => ac.Alert.ID).ToArray();
+ var expectedIDs = new[] { "HumanHealth", "Debug2" };
Assert.That(alertIDs, Is.SupersetOf(expectedIDs));
});
diff --git a/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs b/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs
new file mode 100644
index 00000000000..662ea3b9747
--- /dev/null
+++ b/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs
@@ -0,0 +1,76 @@
+#nullable enable
+using System.Collections.Generic;
+using System.Linq;
+using Content.Server.Antag;
+using Content.Server.Antag.Components;
+using Content.Server.GameTicking;
+using Content.Shared.GameTicking;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Player;
+using Robust.Shared.Random;
+
+namespace Content.IntegrationTests.Tests.GameRules;
+
+// Once upon a time, players in the lobby weren't ever considered eligible for antag roles.
+// Lets not let that happen again.
+[TestFixture]
+public sealed class AntagPreferenceTest
+{
+ [Test]
+ public async Task TestLobbyPlayersValid()
+ {
+ await using var pair = await PoolManager.GetServerClient(new PoolSettings
+ {
+ DummyTicker = false,
+ Connected = true,
+ InLobby = true
+ });
+
+ var server = pair.Server;
+ var client = pair.Client;
+ var ticker = server.System();
+ var sys = server.System();
+
+ // Initially in the lobby
+ Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
+ Assert.That(client.AttachedEntity, Is.Null);
+ Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.NotReadyToPlay));
+
+ EntityUid uid = default;
+ await server.WaitPost(() => uid = server.EntMan.Spawn("Traitor"));
+ var rule = new Entity(uid, server.EntMan.GetComponent(uid));
+ var def = rule.Comp.Definitions.Single();
+
+ // IsSessionValid & IsEntityValid are preference agnostic and should always be true for players in the lobby.
+ // Though maybe that will change in the future, but then GetPlayerPool() needs to be updated to reflect that.
+ Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
+ Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);
+
+ // By default, traitor/antag preferences are disabled, so the pool should be empty.
+ var sessions = new List{pair.Player!};
+ var pool = sys.GetPlayerPool(rule, sessions, def);
+ Assert.That(pool.Count, Is.EqualTo(0));
+
+ // Opt into the traitor role.
+ await pair.SetAntagPref("Traitor", true);
+
+ Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
+ Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);
+ pool = sys.GetPlayerPool(rule, sessions, def);
+ Assert.That(pool.Count, Is.EqualTo(1));
+ pool.TryPickAndTake(pair.Server.ResolveDependency(), out var picked);
+ Assert.That(picked, Is.EqualTo(pair.Player));
+ Assert.That(sessions.Count, Is.EqualTo(1));
+
+ // opt back out
+ await pair.SetAntagPref("Traitor", false);
+
+ Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
+ Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);
+ pool = sys.GetPlayerPool(rule, sessions, def);
+ Assert.That(pool.Count, Is.EqualTo(0));
+
+ await server.WaitPost(() => server.EntMan.DeleteEntity(uid));
+ await pair.CleanReturnAsync();
+ }
+}
diff --git a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs
index da5385f802f..6e43196eb93 100644
--- a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs
+++ b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs
@@ -50,10 +50,6 @@ public async Task TryStopNukeOpsFromConstantlyFailing()
var invSys = server.System();
var factionSys = server.System();
- // test urist is a noob let him be nukie
- server.CfgMan.SetCVar(CCVars.GameRoleTimers, false);
-
- Assert.That(server.CfgMan.GetCVar(CCVars.GridFill), Is.False);
server.CfgMan.SetCVar(CCVars.GridFill, true);
// Initially in the lobby
@@ -61,6 +57,9 @@ public async Task TryStopNukeOpsFromConstantlyFailing()
Assert.That(client.AttachedEntity, Is.Null);
Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.NotReadyToPlay));
+ // Opt into the nukies role.
+ await pair.SetAntagPref("NukeopsCommander", true);
+
// There are no grids or maps
Assert.That(entMan.Count(), Is.Zero);
Assert.That(entMan.Count(), Is.Zero);
@@ -75,8 +74,6 @@ public async Task TryStopNukeOpsFromConstantlyFailing()
Assert.That(entMan.Count(), Is.Zero);
Assert.That(entMan.Count(), Is.Zero);
- /* DeltaV - test temporarily disabled until someone smart fixes opting in during test
-
// Ready up and start nukeops
await pair.WaitClientCommand("toggleready True");
Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.ReadyToPlay));
@@ -201,10 +198,8 @@ public async Task TryStopNukeOpsFromConstantlyFailing()
Assert.That(damage.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
}
- DeltaV - end of commenting out */
-
ticker.SetGamePreset((GamePresetPrototype?)null);
- server.CfgMan.SetCVar(CCVars.GridFill, false);
+ await pair.SetAntagPref("NukeopsCommander", false);
await pair.CleanReturnAsync();
}
}
diff --git a/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs b/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs
index 0ad198d6ef2..74641126aee 100644
--- a/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs
+++ b/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs
@@ -1,5 +1,6 @@
using Content.Server.Gravity;
using Content.Shared.Alert;
+using Content.Shared.Gravity;
using Robust.Shared.GameObjects;
namespace Content.IntegrationTests.Tests.Gravity
@@ -38,6 +39,7 @@ public async Task WeightlessStatusTest()
var entityManager = server.ResolveDependency();
var alertsSystem = server.ResolveDependency().GetEntitySystem();
+ var weightlessAlert = SharedGravitySystem.WeightlessAlert;
EntityUid human = default;
@@ -56,7 +58,7 @@ await server.WaitAssertion(() =>
await server.WaitAssertion(() =>
{
// No gravity without a gravity generator
- Assert.That(alertsSystem.IsShowingAlert(human, AlertType.Weightless));
+ Assert.That(alertsSystem.IsShowingAlert(human, weightlessAlert));
generatorUid = entityManager.SpawnEntity("WeightlessGravityGeneratorDummy", entityManager.GetComponent(human).Coordinates);
});
@@ -66,7 +68,7 @@ await server.WaitAssertion(() =>
await server.WaitAssertion(() =>
{
- Assert.That(alertsSystem.IsShowingAlert(human, AlertType.Weightless), Is.False);
+ Assert.That(alertsSystem.IsShowingAlert(human, weightlessAlert), Is.False);
// This should kill gravity
entityManager.DeleteEntity(generatorUid);
@@ -76,7 +78,7 @@ await server.WaitAssertion(() =>
await server.WaitAssertion(() =>
{
- Assert.That(alertsSystem.IsShowingAlert(human, AlertType.Weightless));
+ Assert.That(alertsSystem.IsShowingAlert(human, weightlessAlert));
});
await pair.RunTicksSync(10);
diff --git a/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs b/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs
index 317aa10400b..2b844d34f0c 100644
--- a/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs
+++ b/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs
@@ -4,6 +4,7 @@
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
+using Content.Shared.Interaction.Components;
using Content.Shared.Item;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
@@ -64,6 +65,7 @@ await server.WaitAssertion(() =>
{
user = sEntities.SpawnEntity(null, coords);
sEntities.EnsureComponent(user);
+ sEntities.EnsureComponent(user);
handSys.AddHand(user, "hand", HandLocation.Left);
target = sEntities.SpawnEntity(null, coords);
item = sEntities.SpawnEntity(null, coords);
@@ -205,6 +207,7 @@ await server.WaitAssertion(() =>
{
user = sEntities.SpawnEntity(null, coords);
sEntities.EnsureComponent(user);
+ sEntities.EnsureComponent(user);
handSys.AddHand(user, "hand", HandLocation.Left);
target = sEntities.SpawnEntity(null, new MapCoordinates(new Vector2(SharedInteractionSystem.InteractionRange - 0.1f, 0), mapId));
item = sEntities.SpawnEntity(null, coords);
@@ -347,6 +350,7 @@ await server.WaitAssertion(() =>
{
user = sEntities.SpawnEntity(null, coords);
sEntities.EnsureComponent(user);
+ sEntities.EnsureComponent(user);
handSys.AddHand(user, "hand", HandLocation.Left);
target = sEntities.SpawnEntity(null, coords);
item = sEntities.SpawnEntity(null, coords);
@@ -407,7 +411,6 @@ await server.WaitAssertion(() =>
await pair.CleanReturnAsync();
}
- [Reflect(false)]
public sealed class TestInteractionSystem : EntitySystem
{
public EntityEventHandler? InteractUsingEvent;
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
index 42f64b344cd..e5f794feaa0 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
@@ -137,6 +137,7 @@ public abstract partial class InteractionTest
prototype: Aghost
- type: DoAfter
- type: Hands
+ - type: ComplexInteraction
- type: MindContainer
- type: Stripping
- type: Tag
diff --git a/Content.IntegrationTests/Tests/Minds/GhostTests.cs b/Content.IntegrationTests/Tests/Minds/GhostTests.cs
index ad9d53a70db..7a156e71e41 100644
--- a/Content.IntegrationTests/Tests/Minds/GhostTests.cs
+++ b/Content.IntegrationTests/Tests/Minds/GhostTests.cs
@@ -156,4 +156,20 @@ public async Task TestGridGhostOnQueueDelete()
await data.Pair.CleanReturnAsync();
}
+ [Test]
+ public async Task TestGhostGridNotTerminating()
+ {
+ var data = await SetupData();
+
+ Assert.DoesNotThrowAsync(async () =>
+ {
+ // Delete the grid
+ await data.Server.WaitPost(() => data.SEntMan.DeleteEntity(data.MapData.Grid.Owner));
+ });
+
+ await data.Pair.RunTicksSync(5);
+
+ await data.Pair.CleanReturnAsync();
+ }
+
}
diff --git a/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs b/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs
index d5c2a9124dd..40457f54883 100644
--- a/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs
+++ b/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs
@@ -9,7 +9,6 @@ namespace Content.IntegrationTests.Tests
[TestOf(typeof(RoundRestartCleanupEvent))]
public sealed class ResettingEntitySystemTests
{
- [Reflect(false)]
public sealed class TestRoundRestartCleanupEvent : EntitySystem
{
public bool HasBeenReset { get; set; }
@@ -49,8 +48,6 @@ await server.WaitAssertion(() =>
system.HasBeenReset = false;
- Assert.That(system.HasBeenReset, Is.False);
-
gameTicker.RestartRound();
Assert.That(system.HasBeenReset);
diff --git a/Content.IntegrationTests/Tests/VendingMachineRestockTest.cs b/Content.IntegrationTests/Tests/VendingMachineRestockTest.cs
index 99481db70e7..3cceaefbdc9 100644
--- a/Content.IntegrationTests/Tests/VendingMachineRestockTest.cs
+++ b/Content.IntegrationTests/Tests/VendingMachineRestockTest.cs
@@ -27,6 +27,7 @@ public sealed class VendingMachineRestockTest : EntitySystem
id: HumanVendingDummy
components:
- type: Hands
+ - type: ComplexInteraction
- type: Body
prototype: Human
diff --git a/Content.MapRenderer/Program.cs b/Content.MapRenderer/Program.cs
index 43dcff2c020..73141191089 100644
--- a/Content.MapRenderer/Program.cs
+++ b/Content.MapRenderer/Program.cs
@@ -29,7 +29,7 @@ internal static async Task Main(string[] args)
if (!CommandLineArguments.TryParse(args, out var arguments))
return;
- PoolManager.Startup(null);
+ PoolManager.Startup();
if (arguments.Maps.Count == 0)
{
Console.WriteLine("Didn't specify any maps to paint! Loading the map list...");
diff --git a/Content.Server.Database/Migrations/Postgres/20240531011555_RoleWhitelist.Designer.cs b/Content.Server.Database/Migrations/Postgres/20240531011555_RoleWhitelist.Designer.cs
new file mode 100644
index 00000000000..5032281dfec
--- /dev/null
+++ b/Content.Server.Database/Migrations/Postgres/20240531011555_RoleWhitelist.Designer.cs
@@ -0,0 +1,1913 @@
+//
+using System;
+using System.Net;
+using System.Text.Json;
+using Content.Server.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using NpgsqlTypes;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Postgres
+{
+ [DbContext(typeof(PostgresServerDbContext))]
+ [Migration("20240531011555_RoleWhitelist")]
+ partial class RoleWhitelist
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Content.Server.Database.Admin", b =>
+ {
+ b.Property("UserId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("AdminRankId")
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_id");
+
+ b.Property("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.HasKey("UserId")
+ .HasName("PK_admin");
+
+ b.HasIndex("AdminRankId")
+ .HasDatabaseName("IX_admin_admin_rank_id");
+
+ b.ToTable("admin", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_flag_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AdminId")
+ .HasColumnType("uuid")
+ .HasColumnName("admin_id");
+
+ b.Property("Flag")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("flag");
+
+ b.Property("Negative")
+ .HasColumnType("boolean")
+ .HasColumnName("negative");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_flag");
+
+ b.HasIndex("AdminId")
+ .HasDatabaseName("IX_admin_flag_admin_id");
+
+ b.HasIndex("Flag", "AdminId")
+ .IsUnique();
+
+ b.ToTable("admin_flag", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+ {
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("Id")
+ .HasColumnType("integer")
+ .HasColumnName("admin_log_id");
+
+ b.Property("Date")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("date");
+
+ b.Property("Impact")
+ .HasColumnType("smallint")
+ .HasColumnName("impact");
+
+ b.Property("Json")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("json");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("message");
+
+ b.Property("Type")
+ .HasColumnType("integer")
+ .HasColumnName("type");
+
+ b.HasKey("RoundId", "Id")
+ .HasName("PK_admin_log");
+
+ b.HasIndex("Date");
+
+ b.HasIndex("Message")
+ .HasAnnotation("Npgsql:TsVectorConfig", "english");
+
+ NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Message"), "GIN");
+
+ b.HasIndex("Type")
+ .HasDatabaseName("IX_admin_log_type");
+
+ b.ToTable("admin_log", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
+ {
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("LogId")
+ .HasColumnType("integer")
+ .HasColumnName("log_id");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.HasKey("RoundId", "LogId", "PlayerUserId")
+ .HasName("PK_admin_log_player");
+
+ b.HasIndex("PlayerUserId")
+ .HasDatabaseName("IX_admin_log_player_player_user_id");
+
+ b.ToTable("admin_log_player", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminMessage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_messages_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property("Dismissed")
+ .HasColumnType("boolean")
+ .HasColumnName("dismissed");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("LastEditedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_edited_at");
+
+ b.Property("LastEditedById")
+ .HasColumnType("uuid")
+ .HasColumnName("last_edited_by_id");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("message");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("Seen")
+ .HasColumnType("boolean")
+ .HasColumnName("seen");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_messages");
+
+ b.HasIndex("CreatedById");
+
+ b.HasIndex("DeletedById");
+
+ b.HasIndex("LastEditedById");
+
+ b.HasIndex("PlayerUserId")
+ .HasDatabaseName("IX_admin_messages_player_user_id");
+
+ b.HasIndex("RoundId")
+ .HasDatabaseName("IX_admin_messages_round_id");
+
+ b.ToTable("admin_messages", null, t =>
+ {
+ t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen");
+ });
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_notes_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("LastEditedAt")
+ .IsRequired()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_edited_at");
+
+ b.Property("LastEditedById")
+ .HasColumnType("uuid")
+ .HasColumnName("last_edited_by_id");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("message");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("Secret")
+ .HasColumnType("boolean")
+ .HasColumnName("secret");
+
+ b.Property("Severity")
+ .HasColumnType("integer")
+ .HasColumnName("severity");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_notes");
+
+ b.HasIndex("CreatedById");
+
+ b.HasIndex("DeletedById");
+
+ b.HasIndex("LastEditedById");
+
+ b.HasIndex("PlayerUserId")
+ .HasDatabaseName("IX_admin_notes_player_user_id");
+
+ b.HasIndex("RoundId")
+ .HasDatabaseName("IX_admin_notes_round_id");
+
+ b.ToTable("admin_notes", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_rank");
+
+ b.ToTable("admin_rank", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_flag_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AdminRankId")
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_id");
+
+ b.Property("Flag")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("flag");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_rank_flag");
+
+ b.HasIndex("AdminRankId");
+
+ b.HasIndex("Flag", "AdminRankId")
+ .IsUnique();
+
+ b.ToTable("admin_rank_flag", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_watchlists_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("LastEditedAt")
+ .IsRequired()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_edited_at");
+
+ b.Property("LastEditedById")
+ .HasColumnType("uuid")
+ .HasColumnName("last_edited_by_id");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("message");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_watchlists");
+
+ b.HasIndex("CreatedById");
+
+ b.HasIndex("DeletedById");
+
+ b.HasIndex("LastEditedById");
+
+ b.HasIndex("PlayerUserId")
+ .HasDatabaseName("IX_admin_watchlists_player_user_id");
+
+ b.HasIndex("RoundId")
+ .HasDatabaseName("IX_admin_watchlists_round_id");
+
+ b.ToTable("admin_watchlists", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Antag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("antag_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AntagName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("antag_name");
+
+ b.Property("ProfileId")
+ .HasColumnType("integer")
+ .HasColumnName("profile_id");
+
+ b.HasKey("Id")
+ .HasName("PK_antag");
+
+ b.HasIndex("ProfileId", "AntagName")
+ .IsUnique();
+
+ b.ToTable("antag", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AssignedUserId", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("assigned_user_id_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("UserName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("user_name");
+
+ b.HasKey("Id")
+ .HasName("PK_assigned_user_id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.HasIndex("UserName")
+ .IsUnique();
+
+ b.ToTable("assigned_user_id", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("connection_log_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .IsRequired()
+ .HasColumnType("inet")
+ .HasColumnName("address");
+
+ b.Property("Denied")
+ .HasColumnType("smallint")
+ .HasColumnName("denied");
+
+ b.Property("HWId")
+ .HasColumnType("bytea")
+ .HasColumnName("hwid");
+
+ b.Property("ServerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0)
+ .HasColumnName("server_id");
+
+ b.Property("Time")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("time");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property