Merge pull request #1278 from NullWanderer/2024/06/01-upstream-merge

2024/06/01 upstream merge
This commit is contained in:
Null
2024-06-01 18:15:18 +02:00
committed by GitHub
743 changed files with 13253 additions and 5693 deletions
@@ -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;
/// <summary>
/// Benchmarks for comparing the speed of various component fetching/lookup related methods, including directed event
/// subscriptions
/// </summary>
[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<ItemComponent> _itemQuery;
private EntityQuery<ClothingComponent> _clothingQuery;
private EntityQuery<MapComponent> _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<IEntityManager>();
_itemQuery = _entMan.GetEntityQuery<ItemComponent>();
_clothingQuery = _entMan.GetEntityQuery<ClothingComponent>();
_mapQuery = _entMan.GetEntityQuery<MapComponent>();
_pair.Server.ResolveDependency<IRobustRandom>().SetSeed(42);
_pair.Server.WaitPost(() =>
{
var success = _entMan.System<MapLoaderSystem>().TryLoad(_mapId, Map, out _);
if (!success)
throw new Exception("Map load failed");
_pair.Server.MapMan.DoMapInitialize(_mapId);
}).GetAwaiter().GetResult();
_items = new EntityUid[_entMan.Count<ItemComponent>()];
var i = 0;
var enumerator = _entMan.AllEntityQueryEnumerator<ItemComponent>();
while (enumerator.MoveNext(out var uid, out _))
{
_items[i++] = uid;
}
}
[GlobalCleanup]
public async Task Cleanup()
{
await _pair.DisposeAsync();
PoolManager.Shutdown();
}
#region TryComp
/// <summary>
/// Baseline TryComp benchmark. When the benchmark was created, around 40% of the items were clothing.
/// </summary>
[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;
}
/// <summary>
/// Variant of <see cref="TryComp"/> that is meant to always fail to get a component.
/// </summary>
[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;
}
/// <summary>
/// Variant of <see cref="TryComp"/> that is meant to always succeed getting a component.
/// </summary>
[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;
}
/// <summary>
/// Variant of <see cref="TryComp"/> that uses `Resolve()` to try get the component.
/// </summary>
[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<ItemComponent>();
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<ClothingComponent, ItemComponent>();
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<ClothingComponent, ItemComponent, TransformComponent>();
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<AirlockComponent>();
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<AirlockComponent, DoorComponent>();
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<AirlockComponent, DoorComponent, TransformComponent>();
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<ClothingComponent, QueryBenchEvent>(OnEvent);
}
private void OnEvent(EntityUid uid, ClothingComponent component, ref QueryBenchEvent args)
{
args.HashCode = HashCode.Combine(args.HashCode, component.GetHashCode());
}
}
-137
View File
@@ -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<ClothingComponent> _clothingQuery;
[GlobalSetup]
public void Setup()
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup(null);
_pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
_entMan = _pair.Server.ResolveDependency<IEntityManager>();
_pair.Server.ResolveDependency<IRobustRandom>().SetSeed(42);
_pair.Server.WaitPost(() =>
{
var success = _entMan.System<MapLoaderSystem>().TryLoad(_mapId, Map, out _);
if (!success)
throw new Exception("Map load failed");
_pair.Server.MapMan.DoMapInitialize(_mapId);
}).GetAwaiter().GetResult();
_clothingQuery = _entMan.GetEntityQuery<ClothingComponent>();
// Apparently ~40% of entities are items, and 1 in 6 of those are clothing.
/*
var entCount = _entMan.EntityCount;
var itemCount = _entMan.Count<ItemComponent>();
var clothingCount = _entMan.Count<ClothingComponent>();
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<ItemComponent>();
while (enumerator.MoveNext(out var uid, out var _))
{
if (_entMan.HasComponent<ClothingComponent>(uid))
hashCode = HashCode.Combine(hashCode, uid.Id);
}
return hashCode;
}
[Benchmark]
public int HasComponentQuery()
{
var hashCode = 0;
var enumerator = _entMan.AllEntityQueryEnumerator<ItemComponent>();
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<ItemComponent>();
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<ItemComponent>();
while (enumerator.MoveNext(out var uid, out var _))
{
if (_clothingQuery.TryGetComponent(uid, out var clothing))
hashCode = HashCode.Combine(hashCode, clothing.GetHashCode());
}
return hashCode;
}
/// <summary>
/// Enumerate all entities with both an item and clothing component.
/// </summary>
[Benchmark]
public int Enumerator()
{
var hashCode = 0;
var enumerator = _entMan.AllEntityQueryEnumerator<ItemComponent, ClothingComponent>();
while (enumerator.MoveNext(out var _, out var clothing))
{
hashCode = HashCode.Combine(hashCode, clothing.GetHashCode());
}
return hashCode;
}
}
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -49,7 +49,7 @@ public class PvsBenchmark
#if !DEBUG
ProgramShared.PathOffset = "../../../../";
#endif
PoolManager.Startup(null);
PoolManager.Startup();
_pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
_entMan = _pair.Server.ResolveDependency<IEntityManager>();
@@ -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;
@@ -16,14 +16,17 @@ namespace Content.Client.Administration.UI.Bwoink
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;
}
if (sel.OverallPlaytime != null)
{
Title += $" | {Loc.GetString("generic-playtime-title")}: {sel.PlaytimeString}";
}
Title = $"{sel.CharacterName} / {sel.Username}";
if (sel.OverallPlaytime != null)
{
Title += $" | {Loc.GetString("generic-playtime-title")}: {sel.PlaytimeString}";
}
};
@@ -20,7 +20,7 @@ namespace Content.Client.Administration.UI.CustomControls
private List<PlayerInfo> _playerList = new();
private readonly List<PlayerInfo> _sortedPlayerList = new();
public event Action<PlayerInfo>? OnSelectionChanged;
public event Action<PlayerInfo?>? OnSelectionChanged;
public IReadOnlyList<PlayerInfo> PlayerInfo => _playerList;
public Func<PlayerInfo, string, string>? OverrideText;
@@ -41,12 +41,19 @@ namespace Content.Client.Administration.UI.CustomControls
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})
@@ -1,21 +1,19 @@
<Control xmlns="https://spacestation14.io"
xmlns:pt="clr-namespace:Content.Client.Administration.UI.Tabs.PlayerTab"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls">
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:co="clr-namespace:Content.Client.UserInterface.Controls">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Label Name="PlayerCount" HorizontalExpand="True" SizeFlagsStretchRatio="0.50"
Text="{Loc Player Count}" />
<Button Name="ShowDisconnectedButton" HorizontalExpand="True" SizeFlagsStretchRatio="0.25"
Text="{Loc player-tab-show-disconnected}" ToggleMode="True"/>
<Button Name="OverlayButton" HorizontalExpand="True" SizeFlagsStretchRatio="0.25"
Text="{Loc player-tab-overlay}" ToggleMode="True"/>
<Label Name="PlayerCount" HorizontalExpand="True" Text="{Loc Player Count}" />
<LineEdit Name="SearchLineEdit" HorizontalExpand="True"
PlaceHolder="{Loc player-tab-filter-line-edit-placeholder}" />
<Button Name="ShowDisconnectedButton" HorizontalExpand="True"
Text="{Loc player-tab-show-disconnected}" ToggleMode="True" />
<Button Name="OverlayButton" HorizontalExpand="True" Text="{Loc player-tab-overlay}" ToggleMode="True" />
</BoxContainer>
<Control MinSize="0 5" />
<ScrollContainer HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Orientation="Vertical" Name="PlayerList">
<pt:PlayerTabHeader Name="ListHeader" />
<cc:HSeparator />
</BoxContainer>
</ScrollContainer>
<Control MinSize="0 5"/>
<pt:PlayerTabHeader Name="ListHeader"/>
<cc:HSeparator/>
<co:SearchListContainer Name="SearchList" Access="Public" VerticalExpand="True"/>
</BoxContainer>
</Control>
@@ -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 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
private bool _ascending = true;
private bool _showDisconnected;
public event Action<PlayerTabEntry, GUIBoundKeyEventArgs>? OnEntryKeyBindDown;
public event Action<GUIBoundKeyEventArgs, ListData>? OnEntryKeyBindDown;
public PlayerTab()
{
IoCManager.InjectDependencies(this);
_adminSystem = _entManager.System<AdminSystem>();
RobustXamlLoader.Load(this);
RefreshPlayerList(_adminSystem.PlayerList);
_adminSystem = _entManager.System<AdminSystem>();
_adminSystem.PlayerListChanged += RefreshPlayerList;
_adminSystem.OverlayEnabled += OverlayEnabled;
_adminSystem.OverlayDisabled += OverlayDisabled;
@@ -46,8 +46,17 @@ namespace Content.Client.Administration.UI.Tabs.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 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
}
}
#endregion
private void ShowDisconnectedPressed(ButtonEventArgs args)
{
_showDisconnected = args.Button.Pressed;
@@ -92,14 +103,10 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
}
}
#region ListContainer
private void RefreshPlayerList(IReadOnlyList<PlayerInfo> 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 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
UpdateHeaderSymbols();
var useAltColor = false;
foreach (var player in sortedPlayers)
{
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;
}
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}";
}
/// <summary>
/// Determines whether <paramref name="filter"/> is contained in <paramref name="listData"/>.FilteringString.
/// If all characters are lowercase, the comparison ignores case.
/// If there is an uppercase character, the comparison is case sensitive.
/// </summary>
/// <param name="filter"></param>
/// <param name="listData"></param>
/// <returns>Whether <paramref name="filter"/> is contained in <paramref name="listData"/>.FilteringString.</returns>
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 (!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 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
RefreshPlayerList(_adminSystem.PlayerList);
}
#endregion
}
public record PlayerListData(PlayerInfo Info, string FilteringString) : ListData;
}
@@ -1,6 +1,6 @@
<ContainerButton xmlns="https://spacestation14.io"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls">
<PanelContainer Name="BackgroundColorPanel"/>
<PanelContainer xmlns="https://spacestation14.io"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
Name="BackgroundColorPanel">
<BoxContainer Orientation="Horizontal"
HorizontalExpand="True"
SeparationOverride="4">
@@ -15,17 +15,18 @@
ClipText="True"/>
<customControls:VSeparator/>
<Label Name="JobLabel"
SizeFlagsStretchRatio="3"
SizeFlagsStretchRatio="2"
HorizontalExpand="True"
ClipText="True"/>
<customControls:VSeparator/>
<Label Name="AntagonistLabel"
SizeFlagsStretchRatio="2"
SizeFlagsStretchRatio="1"
HorizontalExpand="True"
ClipText="True"/>
<customControls:VSeparator/>
<Label Name="OverallPlaytimeLabel"
SizeFlagsStretchRatio="2"
SizeFlagsStretchRatio="1"
HorizontalExpand="True"
ClipText="True"/>
</BoxContainer>
</ContainerButton>
</PanelContainer>
@@ -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 @@ using Robust.Client.UserInterface.XAML;
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;
}
}
@@ -19,23 +19,25 @@
MouseFilter="Pass"/>
<cc:VSeparator/>
<Label Name="JobLabel"
SizeFlagsStretchRatio="3"
SizeFlagsStretchRatio="2"
HorizontalExpand="True"
ClipText="True"
Text="{Loc player-tab-job}"
MouseFilter="Pass"/>
<cc:VSeparator/>
<Label Name="AntagonistLabel"
SizeFlagsStretchRatio="2"
SizeFlagsStretchRatio="1"
HorizontalExpand="True"
ClipText="True"
Text="{Loc player-tab-antagonist}"
MouseFilter="Pass"/>
<cc:VSeparator/>
<Label Name="PlaytimeLabel"
SizeFlagsStretchRatio="2"
SizeFlagsStretchRatio="1"
HorizontalExpand="True"
ClipText="True"
Text="{Loc player-tab-playtime}"
MouseFilter="Pass"/>
MouseFilter="Pass"
ToolTip="{Loc player-tab-entry-tooltip}"/>
</BoxContainer>
</Control>
+1 -1
View File
@@ -91,7 +91,7 @@ public sealed class ClientAlertsSystem : AlertsSystem
ClearAlerts?.Invoke(this, EventArgs.Empty);
}
public void AlertClicked(AlertType alertType)
public void AlertClicked(ProtoId<AlertPrototype> alertType)
{
RaiseNetworkEvent(new ClickAlertEvent(alertType));
}
@@ -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 @@ namespace Content.Client.Atmos.EntitySystems
private void OnHandleState(EntityUid gridUid, GasTileOverlayComponent comp, ref ComponentHandleState args)
{
if (args.Current is not GasTileOverlayState state)
return;
Dictionary<Vector2i, GasOverlayChunk> 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;
}
@@ -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<SpriteComponent>(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
}
+24 -15
View File
@@ -56,34 +56,43 @@ namespace Content.Client.Decals
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<Vector2i, DecalChunk> 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)
+2 -1
View File
@@ -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!;
+3 -3
View File
@@ -239,8 +239,8 @@ namespace Content.Client.Examine
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 @@ namespace Content.Client.Examine
else
{
var label = new RichTextLabel();
label.SetMessage(FormattedMessage.FromMarkup("[bold]???[/bold]"));
label.SetMessage(FormattedMessage.FromMarkupOrThrow("[bold]???[/bold]"));
hBox.AddChild(label);
}
+1 -1
View File
@@ -17,7 +17,7 @@ namespace Content.Client.FlavorText
var loc = IoCManager.Resolve<ILocalizationManager>();
CFlavorTextInput.Placeholder = new Rope.Leaf(loc.GetString("flavor-text-placeholder"));
CFlavorTextInput.OnKeyBindDown += _ => FlavorTextChanged();
CFlavorTextInput.OnTextChanged += _ => FlavorTextChanged();
}
public void FlavorTextChanged()
@@ -25,7 +25,7 @@ public sealed partial class GravitySystem
{
var localPlayer = _playerManager.LocalEntity;
if (!TryComp<TransformComponent>(localPlayer, out var xform) ||
if (!TryComp(localPlayer, out TransformComponent? xform) ||
xform.GridUid != uid && xform.MapUid != uid)
{
return;
@@ -46,7 +46,7 @@ public sealed partial class GravitySystem
var localPlayer = _playerManager.LocalEntity;
if (!TryComp<TransformComponent>(localPlayer, out var xform))
if (!TryComp(localPlayer, out TransformComponent? xform))
return;
if (xform.GridUid != uid ||
@@ -47,6 +47,17 @@
</CollapsibleBody>
</Collapsible>
</BoxContainer>
<BoxContainer Name="PlantMetabolismsContainer" HorizontalExpand="True">
<Collapsible>
<CollapsibleHeading Title="{Loc 'guidebook-reagent-plant-metabolisms-header'}"/>
<CollapsibleBody>
<BoxContainer Name="PlantMetabolismsDescriptionContainer"
Orientation="Vertical"
Margin="10 0 10 0"
HorizontalExpand="True"/>
</CollapsibleBody>
</Collapsible>
</BoxContainer>
<BoxContainer Margin="10 5 10 10" HorizontalExpand="True">
<!-- The troublemaker !-->
<RichTextLabel Name="ReagentDescription" HorizontalAlignment="Left"/>
@@ -157,6 +157,39 @@ public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISea
}
#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();
+1 -1
View File
@@ -148,7 +148,7 @@ public sealed class GuidebookSystem : EntitySystem
public void FakeClientActivateInWorld(EntityUid activated)
{
var activateMsg = new ActivateInWorldEvent(GetGuidebookUser(), activated);
var activateMsg = new ActivateInWorldEvent(GetGuidebookUser(), activated, true);
RaiseLocalEvent(activated, activateMsg);
}
@@ -37,14 +37,8 @@ namespace Content.Client.Instruments.UI
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()
@@ -4,24 +4,6 @@ using Robust.Shared.Containers;
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<StorageComponent>(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;
}
@@ -73,11 +73,6 @@ public sealed class DragDropHelper<T>
_cfg.OnValueChanged(CCVars.DragDropDeadZone, SetDeadZone, true);
}
~DragDropHelper()
{
_cfg.UnsubValueChanged(CCVars.DragDropDeadZone, SetDeadZone);
}
/// <summary>
/// Tell the helper that the mouse button was pressed down on
/// a target, thus a drag has the possibility to begin for this target.
@@ -21,7 +21,6 @@ using Robust.Client.UserInterface;
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 @@ namespace Content.Client.Inventory
_inv = EntMan.System<InventorySystem>();
_cuffable = EntMan.System<SharedCuffableSystem>();
// 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);
}
+10 -7
View File
@@ -22,7 +22,6 @@ using Robust.Client.UserInterface.Controllers;
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 sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
_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 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
PreviewPanel.SetSummaryText(humanoid.Summary);
}
private void RefreshProfileEditor()
{
_profileEditor?.RefreshAntags();
_profileEditor?.RefreshJobs();
_profileEditor?.RefreshLoadouts();
}
private void SaveProfile()
{
DebugTools.Assert(EditedProfile != null);
@@ -4,10 +4,11 @@
<BoxContainer Orientation="Vertical">
<Label Text="{Loc 'observe-warning-1'}"/>
<Label Text="{Loc 'observe-warning-2'}"/>
<BoxContainer Orientation="Horizontal" >
<BoxContainer Orientation="Horizontal">
<Button Name="NevermindButton" Text="{Loc 'observe-nevermind'}" SizeFlagsStretchRatio="1"/>
<Control HorizontalExpand="True" SizeFlagsStretchRatio="2" />
<cc:CommandButton Command="observe" Name="ObserveButton" StyleClasses="Caution" Text="{Loc 'observe-confirm'}" SizeFlagsStretchRatio="1"/>
<cc:CommandButton Command="observe" Name="ObserveButton" StyleClasses="Caution" Text="{Loc 'observe-confirm'}" SizeFlagsStretchRatio="1"/>
<cc:CommandButton Command="observe admin" Name="ObserveAsAdminButton" Text="{Loc 'observe-as-admin'}" SizeFlagsStretchRatio="1" Visible="False"/>
</BoxContainer>
</BoxContainer>
</DefaultWindow>
@@ -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(); };
+2 -2
View File
@@ -61,7 +61,7 @@ public sealed class GridDraggingSystem : SharedGridDraggingSystem
{
if (_dragging == null) return;
if (_lastMousePosition != null && TryComp<TransformComponent>(_dragging.Value, out var xform) &&
if (_lastMousePosition != null && TryComp(_dragging.Value, out TransformComponent? xform) &&
TryComp<PhysicsComponent>(_dragging.Value, out var body) &&
xform.MapID == _lastMousePosition.Value.MapId)
{
@@ -104,7 +104,7 @@ public sealed class GridDraggingSystem : SharedGridDraggingSystem
StartDragging(gridUid, Transform(gridUid).InvWorldMatrix.Transform(mousePos.Position));
}
if (!TryComp<TransformComponent>(_dragging, out var xform))
if (!TryComp(_dragging, out TransformComponent? xform))
{
StopDragging();
return;
@@ -76,7 +76,7 @@ public sealed partial class ArticleEditorPanel : Control
TextEditPanel.Visible = !_preview;
PreviewPanel.Visible = _preview;
PreviewLabel.SetMarkup(Rope.Collapse(ContentField.TextRope));
PreviewLabel.SetMarkupPermissive(Rope.Collapse(ContentField.TextRope));
}
private void OnCancel(BaseButton.ButtonEventArgs eventArgs)
@@ -5,9 +5,27 @@ namespace Content.Client.Message;
public static class RichTextLabelExt
{
/// <summary>
/// Sets the labels markup.
/// </summary>
/// <remarks>
/// Invalid markup will cause exceptions to be thrown. Don't use this for user input!
/// </remarks>
public static RichTextLabel SetMarkup(this RichTextLabel label, string markup)
{
label.SetMessage(FormattedMessage.FromMarkup(markup));
return label;
}
/// <summary>
/// Sets the labels markup.<br/>
/// Uses <c>FormatedMessage.FromMarkupPermissive</c> which treats invalid markup as text.
/// </summary>
public static RichTextLabel SetMarkupPermissive(this RichTextLabel label, string markup)
{
label.SetMessage(FormattedMessage.FromMarkupPermissive(markup));
return label;
}
}
@@ -10,51 +10,56 @@ public sealed class FloorOcclusionSystem : SharedFloorOcclusionSystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
private EntityQuery<SpriteComponent> _spriteQuery;
public override void Initialize()
{
base.Initialize();
_spriteQuery = GetEntityQuery<SpriteComponent>();
SubscribeLocalEvent<FloorOcclusionComponent, ComponentStartup>(OnOcclusionStartup);
SubscribeLocalEvent<FloorOcclusionComponent, ComponentShutdown>(OnOcclusionShutdown);
SubscribeLocalEvent<FloorOcclusionComponent, AfterAutoHandleStateEvent>(OnOcclusionAuto);
}
private void OnOcclusionAuto(EntityUid uid, FloorOcclusionComponent component, ref AfterAutoHandleStateEvent args)
private void OnOcclusionAuto(Entity<FloorOcclusionComponent> 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<FloorOcclusionComponent> ent, ref ComponentStartup args)
{
if (component.Enabled && TryComp<SpriteComponent>(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<FloorOcclusionComponent> ent, ref ComponentShutdown args)
{
if (component.Enabled == enabled)
SetShader(ent.Owner, false);
}
protected override void SetEnabled(Entity<FloorOcclusionComponent> entity)
{
SetShader(entity.Owner, entity.Comp.Enabled);
}
private void SetShader(Entity<SpriteComponent?> sprite, bool enabled)
{
if (!_spriteQuery.Resolve(sprite.Owner, ref sprite.Comp, false))
return;
base.SetEnabled(uid, component, enabled);
if (!TryComp<SpriteComponent>(uid, out var sprite))
return;
SetShader(sprite, enabled);
}
private void SetShader(SpriteComponent sprite, bool enabled)
{
var shader = _proto.Index<ShaderPrototype>("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;
}
}
}
@@ -8,7 +8,6 @@
<tabs:GraphicsTab Name="GraphicsTab" />
<tabs:KeyRebindTab Name="KeyRebindTab" />
<tabs:AudioTab Name="AudioTab" />
<tabs:NetworkTab Name="NetworkTab" />
<dtabs:DeltaTab Name="DeltaTab" />
</TabContainer>
</DefaultWindow>
@@ -19,8 +19,7 @@ namespace Content.Client.Options.UI
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();
}
@@ -1,102 +0,0 @@
<Control xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Content.Client.Options.UI.Tabs.NetworkTab">
<BoxContainer Orientation="Vertical" >
<BoxContainer Orientation="Vertical" Margin="8 8 8 8" VerticalExpand="True">
<BoxContainer Orientation="Horizontal" Margin="4 10 4 0">
<CheckBox Name="NetPredictCheckbox" Text="{Loc 'ui-options-net-predict'}" />
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="4 10 4 0">
<Label Text="{Loc 'ui-options-net-interp-ratio'}" />
<Control MinSize="8 0" />
<Slider Name="NetInterpRatioSlider"
ToolTip="{Loc 'ui-options-net-interp-ratio-tooltip'}"
MaxValue="8"
HorizontalExpand="True"
MinSize="80 0"
Rounded="True" />
<Control MinSize="8 0" />
<Label Name="NetInterpRatioLabel" MinSize="48 0" Align="Right" />
<Control MinSize="4 0"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="4 10 4 0">
<Label Text="{Loc 'ui-options-net-predict-tick-bias'}" />
<Control MinSize="8 0" />
<Slider Name="NetPredictTickBiasSlider"
ToolTip="{Loc 'ui-options-net-predict-tick-bias-tooltip'}"
MaxValue="6"
MinValue="0"
HorizontalExpand="True"
MinSize="80 0"
Rounded="True" />
<Control MinSize="8 0" />
<Label Name="NetPredictTickBiasLabel" MinSize="48 0" Align="Right" />
<Control MinSize="4 0"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="4 10 4 0">
<Label Text="{Loc 'ui-options-net-pvs-spawn'}" />
<Control MinSize="8 0" />
<Slider Name="NetPvsSpawnSlider"
ToolTip="{Loc 'ui-options-net-pvs-spawn-tooltip'}"
MaxValue="150"
MinValue="20"
HorizontalExpand="True"
MinSize="80 0"
Rounded="True" />
<Control MinSize="8 0" />
<Label Name="NetPvsSpawnLabel" MinSize="48 0" Align="Right" />
<Control MinSize="4 0"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="4 10 4 0">
<Label Text="{Loc 'ui-options-net-pvs-entry'}" />
<Control MinSize="8 0" />
<Slider Name="NetPvsEntrySlider"
ToolTip="{Loc 'ui-options-net-pvs-entry-tooltip'}"
MaxValue="500"
MinValue="20"
HorizontalExpand="True"
MinSize="80 0"
Rounded="True" />
<Control MinSize="8 0" />
<Label Name="NetPvsEntryLabel" MinSize="48 0" Align="Right" />
<Control MinSize="4 0"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="4 10 4 10">
<Label Text="{Loc 'ui-options-net-pvs-leave'}" />
<Control MinSize="8 0" />
<Slider Name="NetPvsLeaveSlider"
ToolTip="{Loc 'ui-options-net-pvs-leave-tooltip'}"
MaxValue="300"
MinValue="20"
HorizontalExpand="True"
MinSize="80 0"
Rounded="True" />
<Control MinSize="8 0" />
<Label Name="NetPvsLeaveLabel" MinSize="48 0" Align="Right" />
<Control MinSize="4 0"/>
</BoxContainer>
</BoxContainer>
<controls:StripeBack HasBottomEdge="False" HasMargins="False">
<BoxContainer Orientation="Horizontal"
Align="End"
HorizontalExpand="True"
VerticalExpand="True">
<Button Name="ResetButton"
Text="{Loc 'ui-options-reset-all'}"
StyleClasses="Caution"
HorizontalExpand="True"
HorizontalAlignment="Right" />
<Button Name="DefaultButton"
Text="{Loc 'ui-options-default'}"
TextAlign="Center"
HorizontalAlignment="Right" />
<Control MinSize="2 0" />
<Button Name="ApplyButton"
Text="{Loc 'ui-options-apply'}"
TextAlign="Center"
HorizontalAlignment="Right" />
</BoxContainer>
</controls:StripeBack>
</BoxContainer>
</Control>
@@ -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
}
}
}
@@ -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 @@ public sealed class EntityHealthBarOverlay : Overlay
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 @@ public sealed class EntityHealthBarOverlay : Overlay
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 @@ public sealed class EntityHealthBarOverlay : Overlay
/// <summary>
/// Returns a ratio between 0 and 1, and whether the entity is in crit.
/// </summary>
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);
+27 -14
View File
@@ -14,27 +14,40 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
private void OnHandleState(EntityUid uid, NavMapComponent component, ref ComponentHandleState args)
{
if (args.Current is not NavMapComponentState state)
return;
Dictionary<Vector2i, int[]> modifiedChunks;
Dictionary<NetEntity, NavMapBeacon> 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 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
}
component.Beacons.Clear();
foreach (var (nuid, beacon) in state.Beacons)
foreach (var (nuid, beacon) in beacons)
{
component.Beacons[nuid] = beacon;
}
@@ -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<string, TimeSpan> _roles = new();
private readonly List<string> _roleBans = new();
private readonly List<string> _jobWhitelists = new();
private ISawmill _sawmill = default!;
@@ -36,6 +38,7 @@ public sealed partial class JobRequirementsManager : ISharedPlaytimeManager
// Yeah the client manager handles role bans and playtime but the server ones are separate DEAL.
_net.RegisterNetMessage<MsgRoleBans>(RxRoleBans);
_net.RegisterNetMessage<MsgPlayTime>(RxPlayTime);
_net.RegisterNetMessage<MsgJobWhitelist>(RxJobWhitelist);
_net.RegisterNetMessage<MsgWhitelist>(RxWhitelist);
_client.RunLevelChanged += ClientOnRunLevelChanged;
@@ -80,6 +83,13 @@ public sealed partial class JobRequirementsManager : ISharedPlaytimeManager
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 sealed partial class JobRequirementsManager : ISharedPlaytimeManager
return false;
}
if (!CheckWhitelist(job, out reason))
return false;
var player = _playerManager.LocalSession;
if (player == null)
return true;
@@ -117,6 +130,21 @@ public sealed partial class JobRequirementsManager : ISharedPlaytimeManager
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;
+76 -38
View File
@@ -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 @@ namespace Content.Client.Popups
[Dependency] private readonly ExamineSystemShared _examine = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public IReadOnlyList<WorldPopupLabel> WorldLabels => _aliveWorldLabels;
public IReadOnlyList<CursorPopupLabel> CursorLabels => _aliveCursorLabels;
public IReadOnlyCollection<WorldPopupLabel> WorldLabels => _aliveWorldLabels.Values;
public IReadOnlyCollection<CursorPopupLabel> CursorLabels => _aliveCursorLabels.Values;
private readonly List<WorldPopupLabel> _aliveWorldLabels = new();
private readonly List<CursorPopupLabel> _aliveCursorLabels = new();
private readonly Dictionary<WorldPopupData, WorldPopupLabel> _aliveWorldLabels = new();
private readonly Dictionary<CursorPopupData, CursorPopupLabel> _aliveCursorLabels = new();
public const float MinimumPopupLifetime = 0.7f;
public const float MaximumPopupLifetime = 5f;
@@ -65,6 +67,15 @@ namespace Content.Client.Popups
.RemoveOverlay<PopupOverlay>();
}
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 @@ namespace Content.Client.Popups
_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 @@ namespace Content.Client.Popups
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 @@ namespace Content.Client.Popups
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<WorldPopupData>();
foreach (var (data, label) in _aliveWorldLabels)
{
_aliveWorldLabels.RemoveSwap(i);
i--;
label.TotalTime += frameTime;
if (label.TotalTime > GetPopupLifetime(label) || Deleted(label.InitialPos.EntityId))
{
aliveWorldToRemove.Add(data);
}
}
foreach (var data in aliveWorldToRemove)
{
_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<CursorPopupData>();
foreach (var (data, label) in _aliveCursorLabels)
{
_aliveCursorLabels.RemoveSwap(i);
i--;
label.TotalTime += frameTime;
if (label.TotalTime > GetPopupLifetime(label))
{
aliveCursorToRemove.Add(data);
}
}
foreach (var data in aliveCursorToRemove)
{
_aliveCursorLabels.Remove(data);
}
}
}
@@ -279,29 +314,32 @@ namespace Content.Client.Popups
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
{
/// <summary>
/// The original EntityCoordinates of the label.
/// </summary>
public EntityCoordinates InitialPos;
public WorldPopupLabel(EntityCoordinates coordinates)
{
InitialPos = coordinates;
}
public EntityCoordinates 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);
}
}
@@ -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()
{
base.Initialize();
[Dependency] private readonly SharedPopupSystem _popup = default!;
SubscribeLocalEvent<ActivatableUIRequiresPowerComponent, ActivatableUIOpenAttemptEvent>(OnActivate);
}
private void OnActivate(EntityUid uid, ActivatableUIRequiresPowerComponent component, ActivatableUIOpenAttemptEvent args)
protected override void OnActivate(Entity<ActivatableUIRequiresPowerComponent> ent, ref ActivatableUIOpenAttemptEvent args)
{
// Client can't predict the power properly at the moment so rely upon the server to do it.
if (args.Cancelled || this.IsPowered(ent.Owner, EntityManager))
{
return;
}
if (TryComp<WiresPanelComponent>(ent.Owner, out var panel) && panel.Open)
return;
_popup.PopupClient(Loc.GetString("base-computer-ui-component-not-powered", ("machine", ent.Owner)), args.User, args.User);
args.Cancel();
}
}
@@ -0,0 +1,8 @@
using Content.Shared.Power.Components;
namespace Content.Client.Power.Components;
[RegisterComponent]
public sealed partial class ApcPowerReceiverComponent : SharedApcPowerReceiverComponent
{
}
@@ -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<ApcPowerReceiverComponent, ComponentHandleState>(OnHandleState);
}
private void OnHandleState(EntityUid uid, ApcPowerReceiverComponent component, ref ComponentHandleState args)
{
if (args.Current is not ApcPowerReceiverComponentState state)
return;
component.Powered = state.Powered;
}
}
@@ -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;
}
}
+1 -2
View File
@@ -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 @@ public sealed class RevenantSystem : EntitySystem
private void OnUpdateAlert(Entity<RevenantComponent> ent, ref UpdateAlertSpriteEvent args)
{
if (args.Alert.AlertType != AlertType.Essence)
if (args.Alert.ID != ent.Comp.EssenceAlert)
return;
var sprite = args.SpriteViewEnt.Comp;
+1 -1
View File
@@ -38,7 +38,7 @@ public sealed class SalvageSystem : SharedSalvageSystem
var player = _playerManager.LocalEntity;
if (!TryComp<TransformComponent>(player, out var xform) ||
if (!TryComp(player, out TransformComponent? xform) ||
!TryComp<SalvageExpeditionComponent>(xform.MapUid, out var expedition) ||
expedition.Stage < ExpeditionStage.MusicCountdown)
{
+1 -1
View File
@@ -45,7 +45,7 @@ public sealed class SpriteFadeSystem : EntitySystem
var spriteQuery = GetEntityQuery<SpriteComponent>();
var change = ChangeRate * frameTime;
if (TryComp<TransformComponent>(player, out var playerXform) &&
if (TryComp(player, out TransformComponent? playerXform) &&
_stateManager.CurrentState is GameplayState state &&
spriteQuery.TryGetComponent(player, out var playerSprite))
{
@@ -31,7 +31,7 @@ public sealed class SurveillanceCameraSetupBoundUi : BoundUserInterface
_window.OpenCentered();
_window.OnNameConfirm += SendDeviceName;
_window.OnNetworkConfirm += SendSelectedNetwork;
_window.OnClose += Close;
}
private void SendSelectedNetwork(int idx)
@@ -63,7 +63,8 @@ public sealed class SurveillanceCameraSetupBoundUi : BoundUserInterface
if (disposing)
{
_window!.Dispose();
_window?.Dispose();
_window = null;
}
}
}
+1 -1
View File
@@ -175,7 +175,7 @@ public sealed class TippyUIController : UIController
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;
@@ -8,7 +8,8 @@ using Robust.Shared.Map;
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 sealed class ListContainer : Control
set => _buttonGroup = value ? new ButtonGroup() : null;
}
public bool Toggle { get; set; }
/// <summary>
/// Called when creating a button on the UI.
/// The provided <see cref="ListContainerButton"/> is the generated button that Controls should be parented to.
/// </summary>
public Action<ListData, ListContainerButton>? GenerateItem;
public Action<BaseButton.ButtonEventArgs?, ListData?>? ItemPressed;
public Action<GUIBoundKeyEventArgs, ListData?>? ItemKeyBindDown;
/// <inheritdoc cref="BaseButton.OnPressed"/>
public Action<BaseButton.ButtonEventArgs, ListData>? ItemPressed;
/// <summary>
/// Invoked when a KeyBind is pressed on a ListContainerButton.
/// </summary>
public Action<GUIBoundKeyEventArgs, ListData>? ItemKeyBindDown;
/// <summary>
/// Invoked when the selected item does not exist in the new data when PopulateList is called.
/// </summary>
public Action? NoItemSelected;
public IReadOnlyList<ListData> Data => _data;
private const int DefaultSeparation = 3;
@@ -72,11 +90,11 @@ public sealed class ListContainer : Control
_vScrollBar.OnValueChanged += ScrollValueChanged;
}
public void PopulateList(IReadOnlyList<ListData> data)
public virtual void PopulateList(IReadOnlyList<ListData> 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 sealed class ListContainer : Control
if (_selected != null && !data.Contains(_selected))
{
_selected = null;
ItemPressed?.Invoke(null, null);
NoItemSelected?.Invoke();
}
}
@@ -116,7 +134,7 @@ public sealed class ListContainer : Control
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 @@ public sealed class ListContainer : Control
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 @@ public sealed class ListContainer : Control
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,
@@ -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<ListData> _unfilteredData = new();
/// <summary>
/// The <see cref="LineEdit"/> that is used to filter the list data.
/// </summary>
public LineEdit? SearchBar
{
get => _searchBar;
set
{
if (_searchBar is not null)
_searchBar.OnTextChanged -= FilterList;
_searchBar = value;
if (_searchBar is null)
return;
_searchBar.OnTextChanged += FilterList;
}
}
/// <summary>
/// Runs over the ListData to determine if it should pass the filter.
/// </summary>
public Func<string, ListData, bool>? DataFilterCondition = null;
public override void PopulateList(IReadOnlyList<ListData> 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<ListData>();
foreach (var data in _unfilteredData)
{
if (!DataFilterCondition(filterText, data))
continue;
filteredData.Add(data);
}
base.PopulateList(filteredData);
}
}
@@ -119,9 +119,4 @@ public class ActionButtonContainer : GridContainer
yield return button;
}
}
~ActionButtonContainer()
{
UserInterfaceManager.GetUIController<ActionUIController>().RemoveActionContainer();
}
}
@@ -177,12 +177,15 @@ public sealed class AdminUIController : UIController,
}
}
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)
@@ -7,6 +7,7 @@ using Robust.Client.GameObjects;
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 @@ public sealed class AlertsUIController : UIController, IOnStateEntered<GameplayS
SyncAlerts();
}
private void OnAlertPressed(object? sender, AlertType e)
private void OnAlertPressed(object? sender, ProtoId<AlertPrototype> e)
{
_alertsSystem?.AlertClicked(e);
}
@@ -4,6 +4,7 @@ using Robust.Client.AutoGenerated;
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 sealed partial class AlertsUI : UIWidget
RobustXamlLoader.Load(this);
}
public void SyncControls(AlertsSystem alertsSystem, AlertOrderPrototype? alertOrderPrototype,
IReadOnlyDictionary<AlertKey, AlertState> alertStates)
public void SyncControls(AlertsSystem alertsSystem,
AlertOrderPrototype? alertOrderPrototype,
IReadOnlyDictionary<AlertKey,
AlertState> alertStates)
{
// remove any controls with keys no longer present
if (SyncRemoveControls(alertStates))
@@ -46,7 +49,7 @@ public sealed partial class AlertsUI : UIWidget
_alertControls.Clear();
}
public event EventHandler<AlertType>? AlertPressed;
public event EventHandler<ProtoId<AlertPrototype>>? AlertPressed;
private bool SyncRemoveControls(IReadOnlyDictionary<AlertKey, AlertState> alertStates)
{
@@ -88,7 +91,7 @@ public sealed partial class AlertsUI : UIWidget
}
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 @@ public sealed partial class AlertsUI : UIWidget
if (args.Event.Function != EngineKeyFunctions.UIClick)
return;
AlertPressed?.Invoke(this, control.Alert.AlertType);
AlertPressed?.Invoke(this, control.Alert.ID);
}
}
@@ -22,9 +22,4 @@ public sealed class ItemSlotButtonContainer : ItemSlotUIContainer<SlotControl>
{
_inventoryController = UserInterfaceManager.GetUIController<InventoryUIController>();
}
~ItemSlotButtonContainer()
{
_inventoryController.RemoveSlotGroup(SlotGroup);
}
}
@@ -9,7 +9,7 @@ using Robust.Client.UserInterface.CustomControls;
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 sealed class ItemGridPiece : Control
var actualSize = new Vector2(boxSize.X + 1, boxSize.Y + 1);
return actualSize * new Vector2i(8, 8);
}
public EntityUid? UiEntity => Entity;
}
public enum ItemGridPieceMarks
@@ -8,6 +8,7 @@ using Content.Shared.Verbs;
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 @@ namespace Content.Client.Verbs.UI
private void FillVerbPopup(ContextMenuPopup popup)
{
HashSet<string> listedCategories = new();
var extras = new ValueList<string>(ExtraCategories.Count);
foreach (var cat in ExtraCategories)
{
extras.Add(cat.Text);
}
foreach (var verb in CurrentVerbs)
{
if (verb.Category == null)
@@ -122,17 +130,15 @@ namespace Content.Client.Verbs.UI
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();
@@ -82,7 +82,7 @@ public sealed class TetherGunSystem : SharedTetherGunSystem
const float BufferDistance = 0.1f;
if (TryComp<TransformComponent>(gun.TetherEntity, out var tetherXform) &&
if (TryComp(gun.TetherEntity, out TransformComponent? tetherXform) &&
tetherXform.Coordinates.TryDistance(EntityManager, TransformSystem, coords, out var distance) &&
distance < BufferDistance)
{
@@ -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<string, object> _modifiedClientCvars = new();
private readonly Dictionary<string, object> _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();
}
/// <summary>
/// Reverts any cvars that were modified during a test back to their original values.
/// </summary>
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();
}
}
@@ -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 sealed partial class TestPair
return list;
}
/// <summary>
/// Helper method for enabling or disabling a antag role
/// </summary>
public async Task SetAntagPref(ProtoId<AntagPrototype> id, bool value)
{
var prefMan = Server.ResolveDependency<IServerPreferencesManager>();
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));
}
}
@@ -40,6 +40,8 @@ public sealed partial class TestPair : IAsyncDisposable
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 sealed partial class TestPair : IAsyncDisposable
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());
@@ -4,6 +4,7 @@ using System.IO;
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 sealed partial class TestPair
(Server, ServerLogHandler) = await PoolManager.GenerateServer(settings, testOut);
ActivateContext(testOut);
Client.CfgMan.OnCVarValueChanged += OnClientCvarChanged;
Server.CfgMan.OnCVarValueChanged += OnServerCvarChanged;
if (!settings.NoLoadTestPrototypes)
await LoadPrototypes(testPrototypes!);
@@ -21,6 +21,7 @@ public static partial class PoolManager
(CCVars.NPCMaxUpdates.Name, "999999"),
(CVars.ThreadParallelCount.Name, "1"),
(CCVars.GameRoleTimers.Name, "false"),
(CCVars.GameRoleWhitelist.Name, "false"),
(CCVars.GridFill.Name, "false"),
(CCVars.PreloadGrids.Name, "false"),
(CCVars.ArrivalsShuttles.Name, "false"),
@@ -15,11 +15,8 @@ public static partial class PoolManager
| BindingFlags.Public
| BindingFlags.DeclaredOnly;
private static void DiscoverTestPrototypes(Assembly? assembly = null)
private static void DiscoverTestPrototypes(Assembly assembly)
{
assembly ??= typeof(PoolManager).Assembly;
_testPrototypes.Clear();
foreach (var type in assembly.GetTypes())
{
foreach (var field in type.GetFields(Flags))
+24 -12
View File
@@ -42,6 +42,8 @@ public static partial class PoolManager
private static bool _dead;
private static Exception? _poolFailureReason;
private static HashSet<Assembly> _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<IEntitySystemManager>();
entSysMan.LoadExtraSystemType<ResettingEntitySystemTests.TestRoundRestartCleanupEvent>();
entSysMan.LoadExtraSystemType<InteractionSystemTests.TestInteractionSystem>();
entSysMan.LoadExtraSystemType<DeviceNetworkTestSystem>();
entSysMan.LoadExtraSystemType<TestDestructibleListenerSystem>();
IoCManager.Resolve<ILogManager>().GetSawmill("loc").Level = LogLevel.Error;
IoCManager.Resolve<IConfigurationManager>()
.OnValueChanged(RTCVars.FailureLogLevel, value => logHandler.FailureLevel = value, true);
@@ -140,7 +137,7 @@ public static partial class PoolManager
{
typeof(Shared.Entry.EntryPoint).Assembly,
typeof(Client.Entry.EntryPoint).Assembly,
typeof(PoolManager).Assembly
typeof(PoolManager).Assembly,
}
};
@@ -273,6 +270,8 @@ public static partial class PoolManager
$"{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 @@ we are just going to end this here to save a lot of time. This is the exception
/// <summary>
/// Initialize the pool manager.
/// </summary>
/// <param name="assembly">Assembly to search for to discover extra test prototypes.</param>
public static void Startup(Assembly? assembly)
/// <param name="extraAssemblies">Assemblies to search for to discover extra prototypes and systems.</param>
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);
}
}
}
@@ -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(_ =>
{
@@ -29,6 +29,7 @@ namespace Content.IntegrationTests.Tests.Buckle
components:
- type: Buckle
- type: Hands
- type: ComplexInteraction
- type: InputMover
- type: Body
prototype: Human
@@ -350,8 +350,12 @@ namespace Content.IntegrationTests.Tests
"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<IEntityManager>();
@@ -24,6 +24,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking
components:
- type: Cuffable
- type: Hands
- type: ComplexInteraction
- type: Body
prototype: Human
@@ -5,7 +5,6 @@ using Content.Shared.Alert;
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 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs
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 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs
// 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 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs
// 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));
});
@@ -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<GameTicker>();
var sys = server.System<AntagSelectionSystem>();
// 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<AntagSelectionComponent>(uid, server.EntMan.GetComponent<AntagSelectionComponent>(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<ICommonSession>{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<IRobustRandom>(), 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();
}
}
@@ -50,10 +50,6 @@ public sealed class NukeOpsTest
var invSys = server.System<InventorySystem>();
var factionSys = server.System<NpcFactionSystem>();
// 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 sealed class NukeOpsTest
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<MapComponent>(), Is.Zero);
Assert.That(entMan.Count<MapGridComponent>(), Is.Zero);
@@ -75,8 +74,6 @@ public sealed class NukeOpsTest
Assert.That(entMan.Count<NukeOpsShuttleComponent>(), Is.Zero);
Assert.That(entMan.Count<NukeOperativeSpawnerComponent>(), 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 sealed class NukeOpsTest
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();
}
}
@@ -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 @@ namespace Content.IntegrationTests.Tests.Gravity
var entityManager = server.ResolveDependency<IEntityManager>();
var alertsSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<AlertsSystem>();
var weightlessAlert = SharedGravitySystem.WeightlessAlert;
EntityUid human = default;
@@ -56,7 +58,7 @@ namespace Content.IntegrationTests.Tests.Gravity
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<TransformComponent>(human).Coordinates);
});
@@ -66,7 +68,7 @@ namespace Content.IntegrationTests.Tests.Gravity
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 @@ namespace Content.IntegrationTests.Tests.Gravity
await server.WaitAssertion(() =>
{
Assert.That(alertsSystem.IsShowingAlert(human, AlertType.Weightless));
Assert.That(alertsSystem.IsShowingAlert(human, weightlessAlert));
});
await pair.RunTicksSync(10);
@@ -4,6 +4,7 @@ using Content.Server.Interaction;
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 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
{
user = sEntities.SpawnEntity(null, coords);
sEntities.EnsureComponent<HandsComponent>(user);
sEntities.EnsureComponent<ComplexInteractionComponent>(user);
handSys.AddHand(user, "hand", HandLocation.Left);
target = sEntities.SpawnEntity(null, coords);
item = sEntities.SpawnEntity(null, coords);
@@ -205,6 +207,7 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
{
user = sEntities.SpawnEntity(null, coords);
sEntities.EnsureComponent<HandsComponent>(user);
sEntities.EnsureComponent<ComplexInteractionComponent>(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 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
{
user = sEntities.SpawnEntity(null, coords);
sEntities.EnsureComponent<HandsComponent>(user);
sEntities.EnsureComponent<ComplexInteractionComponent>(user);
handSys.AddHand(user, "hand", HandLocation.Left);
target = sEntities.SpawnEntity(null, coords);
item = sEntities.SpawnEntity(null, coords);
@@ -407,7 +411,6 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
await pair.CleanReturnAsync();
}
[Reflect(false)]
public sealed class TestInteractionSystem : EntitySystem
{
public EntityEventHandler<InteractUsingEvent>? InteractUsingEvent;
@@ -137,6 +137,7 @@ public abstract partial class InteractionTest
prototype: Aghost
- type: DoAfter
- type: Hands
- type: ComplexInteraction
- type: MindContainer
- type: Stripping
- type: Tag
@@ -156,4 +156,20 @@ public sealed class GhostTests
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();
}
}
@@ -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 @@ namespace Content.IntegrationTests.Tests
system.HasBeenReset = false;
Assert.That(system.HasBeenReset, Is.False);
gameTicker.RestartRound();
Assert.That(system.HasBeenReset);
@@ -27,6 +27,7 @@ namespace Content.IntegrationTests.Tests
id: HumanVendingDummy
components:
- type: Hands
- type: ComplexInteraction
- type: Body
prototype: Human
+1 -1
View File
@@ -29,7 +29,7 @@ namespace Content.MapRenderer
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...");
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,40 @@
#nullable disable
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class RoleWhitelist : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "role_whitelists",
columns: table => new
{
player_user_id = table.Column<Guid>(type: "uuid", nullable: false),
role_id = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_role_whitelists", x => new { x.player_user_id, x.role_id });
table.ForeignKey(
name: "FK_role_whitelists_player_player_user_id",
column: x => x.player_user_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "role_whitelists");
}
}
}
@@ -900,6 +900,22 @@ namespace Content.Server.Database.Migrations.Postgres
b.ToTable("profile_role_loadout", (string)null);
});
modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
{
b.Property<Guid>("PlayerUserId")
.HasColumnType("uuid")
.HasColumnName("player_user_id");
b.Property<string>("RoleId")
.HasColumnType("text")
.HasColumnName("role_id");
b.HasKey("PlayerUserId", "RoleId")
.HasName("PK_role_whitelists");
b.ToTable("role_whitelists", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Round", b =>
{
b.Property<int>("Id")
@@ -1623,6 +1639,19 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("Profile");
});
modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
{
b.HasOne("Content.Server.Database.Player", "Player")
.WithMany("JobWhitelists")
.HasForeignKey("PlayerUserId")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_role_whitelists_player_player_user_id");
b.Navigation("Player");
});
modelBuilder.Entity("Content.Server.Database.Round", b =>
{
b.HasOne("Content.Server.Database.Server", "Server")
@@ -1822,6 +1851,8 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("AdminWatchlistsLastEdited");
b.Navigation("AdminWatchlistsReceived");
b.Navigation("JobWhitelists");
});
modelBuilder.Entity("Content.Server.Database.Preference", b =>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,40 @@
#nullable disable
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class RoleWhitelist : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "role_whitelists",
columns: table => new
{
player_user_id = table.Column<Guid>(type: "TEXT", nullable: false),
role_id = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_role_whitelists", x => new { x.player_user_id, x.role_id });
table.ForeignKey(
name: "FK_role_whitelists_player_player_user_id",
column: x => x.player_user_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "role_whitelists");
}
}
}
@@ -847,6 +847,22 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("profile_role_loadout", (string)null);
});
modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
{
b.Property<Guid>("PlayerUserId")
.HasColumnType("TEXT")
.HasColumnName("player_user_id");
b.Property<string>("RoleId")
.HasColumnType("TEXT")
.HasColumnName("role_id");
b.HasKey("PlayerUserId", "RoleId")
.HasName("PK_role_whitelists");
b.ToTable("role_whitelists", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Round", b =>
{
b.Property<int>("Id")
@@ -1548,6 +1564,19 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("Profile");
});
modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
{
b.HasOne("Content.Server.Database.Player", "Player")
.WithMany("JobWhitelists")
.HasForeignKey("PlayerUserId")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_role_whitelists_player_player_user_id");
b.Navigation("Player");
});
modelBuilder.Entity("Content.Server.Database.Round", b =>
{
b.HasOne("Content.Server.Database.Server", "Server")
@@ -1747,6 +1776,8 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("AdminWatchlistsLastEdited");
b.Navigation("AdminWatchlistsReceived");
b.Navigation("JobWhitelists");
});
modelBuilder.Entity("Content.Server.Database.Preference", b =>
+20
View File
@@ -40,6 +40,7 @@ namespace Content.Server.Database
public DbSet<AdminNote> AdminNotes { get; set; } = null!;
public DbSet<AdminWatchlist> AdminWatchlists { get; set; } = null!;
public DbSet<AdminMessage> AdminMessages { get; set; } = null!;
public DbSet<RoleWhitelist> RoleWhitelists { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -314,6 +315,13 @@ namespace Content.Server.Database
.HasForeignKey(ban => ban.LastEditedById)
.HasPrincipalKey(author => author.UserId)
.OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<RoleWhitelist>()
.HasOne(w => w.Player)
.WithMany(p => p.JobWhitelists)
.HasForeignKey(w => w.PlayerUserId)
.HasPrincipalKey(p => p.UserId)
.OnDelete(DeleteBehavior.Cascade);
}
public virtual IQueryable<AdminLog> SearchLogs(IQueryable<AdminLog> query, string searchText)
@@ -530,6 +538,7 @@ namespace Content.Server.Database
public List<ServerBan> AdminServerBansLastEdited { get; set; } = null!;
public List<ServerRoleBan> AdminServerRoleBansCreated { get; set; } = null!;
public List<ServerRoleBan> AdminServerRoleBansLastEdited { get; set; } = null!;
public List<RoleWhitelist> JobWhitelists { get; set; } = null!;
}
[Table("whitelist")]
@@ -1099,4 +1108,15 @@ namespace Content.Server.Database
/// </summary>
public bool Dismissed { get; set; }
}
[PrimaryKey(nameof(PlayerUserId), nameof(RoleId))]
public class RoleWhitelist
{
[Required, ForeignKey("Player")]
public Guid PlayerUserId { get; set; }
public Player Player { get; set; } = default!;
[Required]
public string RoleId { get; set; } = default!;
}
}
@@ -1,3 +1,4 @@
using Content.Shared.Alert;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
@@ -47,5 +48,12 @@ namespace Content.Server.Abilities.Mime
/// </summary>
[DataField("vowCooldown")]
public TimeSpan VowCooldown = TimeSpan.FromMinutes(5);
[DataField]
public ProtoId<AlertPrototype> VowAlert = "VowOfSilence";
[DataField]
public ProtoId<AlertPrototype> VowBrokenAlert = "VowBroken";
}
}
@@ -1,5 +1,4 @@
using Content.Server.Popups;
using Content.Server.Speech.Muting;
using Content.Shared.Actions;
using Content.Shared.Actions.Events;
using Content.Shared.Alert;
@@ -56,7 +55,7 @@ namespace Content.Server.Abilities.Mime
private void OnComponentInit(EntityUid uid, MimePowersComponent component, ComponentInit args)
{
EnsureComp<MutedComponent>(uid);
_alertsSystem.ShowAlert(uid, AlertType.VowOfSilence);
_alertsSystem.ShowAlert(uid, component.VowAlert);
_actionsSystem.AddAction(uid, ref component.InvisibleWallActionEntity, component.InvisibleWallAction, uid);
//Nyano - Summary: Add Psionic Ability to Mime.
if (TryComp<PsionicComponent>(uid, out var psionic) && psionic.PsionicAbility == null)
@@ -123,8 +122,8 @@ namespace Content.Server.Abilities.Mime
mimePowers.VowBroken = true;
mimePowers.VowRepentTime = _timing.CurTime + mimePowers.VowCooldown;
RemComp<MutedComponent>(uid);
_alertsSystem.ClearAlert(uid, AlertType.VowOfSilence);
_alertsSystem.ShowAlert(uid, AlertType.VowBroken);
_alertsSystem.ClearAlert(uid, mimePowers.VowAlert);
_alertsSystem.ShowAlert(uid, mimePowers.VowBrokenAlert);
_actionsSystem.RemoveAction(uid, mimePowers.InvisibleWallActionEntity);
}
@@ -146,8 +145,8 @@ namespace Content.Server.Abilities.Mime
mimePowers.ReadyToRepent = false;
mimePowers.VowBroken = false;
AddComp<MutedComponent>(uid);
_alertsSystem.ClearAlert(uid, AlertType.VowBroken);
_alertsSystem.ShowAlert(uid, AlertType.VowOfSilence);
_alertsSystem.ClearAlert(uid, mimePowers.VowAlert);
_alertsSystem.ShowAlert(uid, mimePowers.VowBrokenAlert);
_actionsSystem.AddAction(uid, ref mimePowers.InvisibleWallActionEntity, mimePowers.InvisibleWallAction, uid);
}
}
@@ -135,6 +135,8 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
_idCard.TryChangeJobDepartment(targetId, job);
}
UpdateStationRecord(uid, targetId, newFullName, newJobTitle, job);
if (!newAccessList.TrueForAll(x => component.AccessLevels.Contains(x)))
{
_sawmill.Warning($"User {ToPrettyString(uid)} tried to write unknown access tag.");
@@ -168,8 +170,6 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
This current implementation is pretty shit as it logs 27 entries (27 lines) if someone decides to give themselves AA*/
_adminLogger.Add(LogType.Action, LogImpact.Medium,
$"{ToPrettyString(player):player} has modified {ToPrettyString(targetId):entity} with the following accesses: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]");
UpdateStationRecord(uid, targetId, newFullName, newJobTitle, job);
}
/// <summary>
@@ -39,7 +39,7 @@ public sealed class ActionOnInteractSystem : EntitySystem
private void OnActivate(EntityUid uid, ActionOnInteractComponent component, ActivateInWorldEvent args)
{
if (args.Handled)
if (args.Handled || !args.Complex)
return;
if (component.ActionEntities is not {} actionEnts)
@@ -65,13 +65,23 @@ public sealed class BanListEui : BaseEui
unban = new SharedServerUnban(unbanningAdmin, ban.Unban.UnbanTime.UtcDateTime);
}
(string, int cidrMask)? ip = ("*Hidden*", 0);
var hwid = "*Hidden*";
if (_admins.HasAdminFlag(Player, AdminFlags.Pii))
{
ip = ban.Address is { } address
? (address.address.ToString(), address.cidrMask)
: null;
hwid = ban.HWId == null ? null : Convert.ToBase64String(ban.HWId.Value.AsSpan());
}
Bans.Add(new SharedServerBan(
ban.Id,
ban.UserId,
ban.Address is { } address
? (address.address.ToString(), address.cidrMask)
: null,
ban.HWId == null ? null : Convert.ToBase64String(ban.HWId.Value.AsSpan()),
ip,
hwid,
ban.BanTime.UtcDateTime,
ban.ExpirationTime?.UtcDateTime,
ban.Reason,
@@ -96,13 +106,22 @@ public sealed class BanListEui : BaseEui
unban = new SharedServerUnban(unbanningAdmin, ban.Unban.UnbanTime.UtcDateTime);
}
(string, int cidrMask)? ip = ("*Hidden*", 0);
var hwid = "*Hidden*";
if (_admins.HasAdminFlag(Player, AdminFlags.Pii))
{
ip = ban.Address is { } address
? (address.address.ToString(), address.cidrMask)
: null;
hwid = ban.HWId == null ? null : Convert.ToBase64String(ban.HWId.Value.AsSpan());
}
RoleBans.Add(new SharedServerRoleBan(
ban.Id,
ban.UserId,
ban.Address is { } address
? (address.address.ToString(), address.cidrMask)
: null,
ban.HWId == null ? null : Convert.ToBase64String(ban.HWId.Value.AsSpan()),
ip,
hwid,
ban.BanTime.UtcDateTime,
ban.ExpirationTime?.UtcDateTime,
ban.Reason,
@@ -7,7 +7,7 @@ using Robust.Shared.Utility;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Admin)]
[AdminCommand(AdminFlags.AdminWho)]
public sealed class AdminWhoCommand : IConsoleCommand
{
public string Command => "adminwho";
@@ -5,7 +5,7 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Admin)]
[AdminCommand(AdminFlags.Moderator)]
public sealed class AnnounceUiCommand : IConsoleCommand
{
public string Command => "announceui";
@@ -6,7 +6,7 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Admin)]
[AdminCommand(AdminFlags.Ban)]
public sealed class BanExemptionUpdateCommand : LocalizedCommands
{
[Dependency] private readonly IServerDbManager _dbManager = default!;
@@ -61,7 +61,7 @@ public sealed class BanExemptionUpdateCommand : LocalizedCommands
}
}
[AdminCommand(AdminFlags.Admin)]
[AdminCommand(AdminFlags.Ban)]
public sealed class BanExemptionGetCommand : LocalizedCommands
{
[Dependency] private readonly IServerDbManager _dbManager = default!;
@@ -4,7 +4,7 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Admin)]
[AdminCommand(AdminFlags.Moderator)]
sealed class DSay : IConsoleCommand
{
[Dependency] private readonly IEntityManager _e = default!;
@@ -5,7 +5,7 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Admin)]
[AdminCommand(AdminFlags.Fun)]
public sealed class FaxUiCommand : IConsoleCommand
{
public string Command => "faxui";
@@ -27,4 +27,3 @@ public sealed class FaxUiCommand : IConsoleCommand
eui.OpenEui(ui, player);
}
}
@@ -0,0 +1,214 @@
using System.Linq;
using Content.Server.Database;
using Content.Server.Players.JobWhitelist;
using Content.Shared.Administration;
using Content.Shared.Roles;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.Prototypes;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Ban)]
public sealed class JobWhitelistAddCommand : LocalizedCommands
{
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly JobWhitelistManager _jobWhitelist = default!;
[Dependency] private readonly IPlayerLocator _playerLocator = default!;
[Dependency] private readonly IPlayerManager _players = default!;
[Dependency] private readonly IPrototypeManager _prototypes = default!;
public override string Command => "jobwhitelistadd";
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 2)
{
shell.WriteError(Loc.GetString("shell-wrong-arguments-number-need-specific",
("properAmount", 2),
("currentAmount", args.Length)));
shell.WriteLine(Help);
return;
}
var player = args[0].Trim();
var job = new ProtoId<JobPrototype>(args[1].Trim());
if (!_prototypes.TryIndex(job, out var jobPrototype))
{
shell.WriteError(Loc.GetString("cmd-jobwhitelist-job-does-not-exist", ("job", job.Id)));
shell.WriteLine(Help);
return;
}
var data = await _playerLocator.LookupIdByNameAsync(player);
if (data != null)
{
var guid = data.UserId;
var isWhitelisted = await _db.IsJobWhitelisted(guid, job);
if (isWhitelisted)
{
shell.WriteLine(Loc.GetString("cmd-jobwhitelist-already-whitelisted",
("player", player),
("jobId", job.Id),
("jobName", jobPrototype.LocalizedName)));
return;
}
_jobWhitelist.AddWhitelist(guid, job);
shell.WriteLine(Loc.GetString("cmd-jobwhitelistadd-added",
("player", player),
("jobId", job.Id),
("jobName", jobPrototype.LocalizedName)));
return;
}
shell.WriteError(Loc.GetString("cmd-jobwhitelist-player-not-found", ("player", player)));
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromHintOptions(
_players.Sessions.Select(s => s.Name),
Loc.GetString("cmd-jobwhitelist-hint-player"));
}
if (args.Length == 2)
{
return CompletionResult.FromHintOptions(
_prototypes.EnumeratePrototypes<JobPrototype>().Select(p => p.ID),
Loc.GetString("cmd-jobwhitelist-hint-job"));
}
return CompletionResult.Empty;
}
}
[AdminCommand(AdminFlags.Ban)]
public sealed class GetJobWhitelistCommand : LocalizedCommands
{
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IPlayerLocator _playerLocator = default!;
[Dependency] private readonly IPlayerManager _players = default!;
public override string Command => "jobwhitelistget";
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length == 0)
{
shell.WriteError("This command needs at least one argument.");
shell.WriteLine(Help);
return;
}
var player = string.Join(' ', args).Trim();
var data = await _playerLocator.LookupIdByNameAsync(player);
if (data != null)
{
var guid = data.UserId;
var whitelists = await _db.GetJobWhitelists(guid);
if (whitelists.Count == 0)
{
shell.WriteLine(Loc.GetString("cmd-jobwhitelistget-whitelisted-none", ("player", player)));
return;
}
shell.WriteLine(Loc.GetString("cmd-jobwhitelistget-whitelisted-for",
("player", player),
("jobs", string.Join(", ", whitelists))));
return;
}
shell.WriteError(Loc.GetString("cmd-jobwhitelist-player-not-found", ("player", player)));
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromHintOptions(
_players.Sessions.Select(s => s.Name),
Loc.GetString("cmd-jobwhitelist-hint-player"));
}
return CompletionResult.Empty;
}
}
[AdminCommand(AdminFlags.Ban)]
public sealed class RemoveJobWhitelistCommand : LocalizedCommands
{
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly JobWhitelistManager _jobWhitelist = default!;
[Dependency] private readonly IPlayerLocator _playerLocator = default!;
[Dependency] private readonly IPlayerManager _players = default!;
[Dependency] private readonly IPrototypeManager _prototypes = default!;
public override string Command => "jobwhitelistremove";
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 2)
{
shell.WriteError(Loc.GetString("shell-wrong-arguments-number-need-specific",
("properAmount", 2),
("currentAmount", args.Length)));
shell.WriteLine(Help);
return;
}
var player = args[0].Trim();
var job = new ProtoId<JobPrototype>(args[1].Trim());
if (!_prototypes.TryIndex(job, out var jobPrototype))
{
shell.WriteError(Loc.GetString("cmd-jobwhitelist-job-does-not-exist", ("job", job)));
shell.WriteLine(Help);
return;
}
var data = await _playerLocator.LookupIdByNameAsync(player);
if (data != null)
{
var guid = data.UserId;
var isWhitelisted = await _db.IsJobWhitelisted(guid, job);
if (!isWhitelisted)
{
shell.WriteError(Loc.GetString("cmd-jobwhitelistremove-was-not-whitelisted",
("player", player),
("jobId", job.Id),
("jobName", jobPrototype.LocalizedName)));
return;
}
_jobWhitelist.RemoveWhitelist(guid, job);
shell.WriteLine(Loc.GetString("cmd-jobwhitelistremove-removed",
("player", player),
("jobId", job.Id),
("jobName", jobPrototype.LocalizedName)));
return;
}
shell.WriteError(Loc.GetString("cmd-jobwhitelist-player-not-found", ("player", player)));
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromHintOptions(
_players.Sessions.Select(s => s.Name),
Loc.GetString("cmd-jobwhitelist-hint-player"));
}
if (args.Length == 2)
{
return CompletionResult.FromHintOptions(
_prototypes.EnumeratePrototypes<JobPrototype>().Select(p => p.ID),
Loc.GetString("cmd-jobwhitelist-hint-job"));
}
return CompletionResult.Empty;
}
}

Some files were not shown because too many files have changed in this diff Show More