diff --git a/Content.Benchmarks/DeltaPressureBenchmark.cs b/Content.Benchmarks/DeltaPressureBenchmark.cs new file mode 100644 index 00000000000..b31b3ed1a24 --- /dev/null +++ b/Content.Benchmarks/DeltaPressureBenchmark.cs @@ -0,0 +1,174 @@ +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Diagnosers; +using Content.IntegrationTests; +using Content.IntegrationTests.Pair; +using Content.Server.Atmos.Components; +using Content.Server.Atmos.EntitySystems; +using Content.Shared.Atmos.Components; +using Content.Shared.CCVar; +using Robust.Shared; +using Robust.Shared.Analyzers; +using Robust.Shared.Configuration; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Benchmarks; + +/// +/// Spawns N number of entities with a and +/// simulates them for a number of ticks M. +/// +[Virtual] +[GcServer(true)] +//[MemoryDiagnoser] +//[ThreadingDiagnoser] +public class DeltaPressureBenchmark +{ + /// + /// Number of entities (windows, really) to spawn with a . + /// + [Params(1, 10, 100, 1000, 5000, 10000, 50000, 100000)] + public int EntityCount; + + /// + /// Number of entities that each parallel processing job will handle. + /// + // [Params(1, 10, 100, 1000, 5000, 10000)] For testing how multithreading parameters affect performance (THESE TESTS TAKE 16+ HOURS TO RUN) + [Params(10)] + public int BatchSize; + + /// + /// Number of entities to process per iteration in the DeltaPressure + /// processing loop. + /// + // [Params(100, 1000, 5000, 10000, 50000)] + [Params(1000)] + public int EntitiesPerIteration; + + private readonly EntProtoId _windowProtoId = "Window"; + private readonly EntProtoId _wallProtoId = "WallPlastitaniumIndestructible"; + + private TestPair _pair = default!; + private IEntityManager _entMan = default!; + private SharedMapSystem _map = default!; + private IRobustRandom _random = default!; + private IConfigurationManager _cvar = default!; + private ITileDefinitionManager _tileDefMan = default!; + private AtmosphereSystem _atmospereSystem = default!; + + private Entity + _testEnt; + + [GlobalSetup] + public async Task SetupAsync() + { + ProgramShared.PathOffset = "../../../../"; + PoolManager.Startup(); + _pair = await PoolManager.GetServerClient(); + var server = _pair.Server; + + var mapdata = await _pair.CreateTestMap(); + + _entMan = server.ResolveDependency(); + _map = _entMan.System(); + _random = server.ResolveDependency(); + _cvar = server.ResolveDependency(); + _tileDefMan = server.ResolveDependency(); + _atmospereSystem = _entMan.System(); + + _random.SetSeed(69420); // Randomness needs to be deterministic for benchmarking. + + _cvar.SetCVar(CCVars.DeltaPressureParallelToProcessPerIteration, EntitiesPerIteration); + _cvar.SetCVar(CCVars.DeltaPressureParallelBatchSize, BatchSize); + + var plating = _tileDefMan["Plating"].TileId; + + /* + Basically, we want to have a 5-wide grid of tiles. + Edges are walled, and the length of the grid is determined by N + 2. + Windows should only touch the top and bottom walls, and each other. + */ + + var length = EntityCount + 2; // ensures we can spawn exactly N windows between side walls + const int height = 5; + + await server.WaitPost(() => + { + // Fill required tiles (extend grid) with plating + for (var x = 0; x < length; x++) + { + for (var y = 0; y < height; y++) + { + _map.SetTile(mapdata.Grid, mapdata.Grid, new Vector2i(x, y), new Tile(plating)); + } + } + + // Spawn perimeter walls and windows row in the middle (y = 2) + const int midY = height / 2; + for (var x = 0; x < length; x++) + { + for (var y = 0; y < height; y++) + { + var coords = new EntityCoordinates(mapdata.Grid, x + 0.5f, y + 0.5f); + + var isPerimeter = x == 0 || x == length - 1 || y == 0 || y == height - 1; + if (isPerimeter) + { + _entMan.SpawnEntity(_wallProtoId, coords); + continue; + } + + // Spawn windows only on the middle row, spanning interior (excluding side walls) + if (y == midY) + { + _entMan.SpawnEntity(_windowProtoId, coords); + } + } + } + }); + + // Next we run the fixgridatmos command to ensure that we have some air on our grid. + // Wait a little bit as well. + // TODO: Unhardcode command magic string when fixgridatmos is an actual command we can ref and not just + // a stamp-on in AtmosphereSystem. + await _pair.WaitCommand("fixgridatmos " + mapdata.Grid.Owner, 1); + + var uid = mapdata.Grid.Owner; + _testEnt = new Entity( + uid, + _entMan.GetComponent(uid), + _entMan.GetComponent(uid), + _entMan.GetComponent(uid), + _entMan.GetComponent(uid)); + } + + [Benchmark] + public async Task PerformFullProcess() + { + await _pair.Server.WaitPost(() => + { + while (!_atmospereSystem.RunProcessingStage(_testEnt, AtmosphereProcessingState.DeltaPressure)) { } + }); + } + + [Benchmark] + public async Task PerformSingleRunProcess() + { + await _pair.Server.WaitPost(() => + { + _atmospereSystem.RunProcessingStage(_testEnt, AtmosphereProcessingState.DeltaPressure); + }); + } + + [GlobalCleanup] + public async Task CleanupAsync() + { + await _pair.DisposeAsync(); + PoolManager.Shutdown(); + } +} diff --git a/Content.Benchmarks/MapLoadBenchmark.cs b/Content.Benchmarks/MapLoadBenchmark.cs index f763ee321e6..126ac99774f 100644 --- a/Content.Benchmarks/MapLoadBenchmark.cs +++ b/Content.Benchmarks/MapLoadBenchmark.cs @@ -47,7 +47,7 @@ public class MapLoadBenchmark PoolManager.Shutdown(); } - public static readonly string[] MapsSource = { "Empty", "Box", "Aspid", "Bagel", "Dev", "CentComm", "Atlas", "Core", "TestTeg", "Saltern", "Packed", "Omega", "Cluster", "Gemini", "Reach", "Origin", "Meta", "Marathon", "Europa", "MeteorArena", "Fland", "Barratry" }; + public static string[] MapsSource { get; } = { "Empty", "Saltern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Convex"}; [ParamsSource(nameof(MapsSource))] public string Map; diff --git a/Content.Benchmarks/PvsBenchmark.cs b/Content.Benchmarks/PvsBenchmark.cs index 1edbcb64487..51a013539e0 100644 --- a/Content.Benchmarks/PvsBenchmark.cs +++ b/Content.Benchmarks/PvsBenchmark.cs @@ -6,7 +6,6 @@ using BenchmarkDotNet.Attributes; using Content.IntegrationTests; using Content.IntegrationTests.Pair; using Content.Server.Mind; -using Content.Server.Warps; using Content.Shared.Warps; using Robust.Shared; using Robust.Shared.Analyzers; diff --git a/Content.Client/Actions/UI/ActionAlertTooltip.cs b/Content.Client/Actions/UI/ActionAlertTooltip.cs index 2425cdefb91..664a67b406b 100644 --- a/Content.Client/Actions/UI/ActionAlertTooltip.cs +++ b/Content.Client/Actions/UI/ActionAlertTooltip.cs @@ -21,7 +21,7 @@ namespace Content.Client.Actions.UI /// public (TimeSpan Start, TimeSpan End)? Cooldown { get; set; } - public ActionAlertTooltip(FormattedMessage name, FormattedMessage? desc, string? requires = null, FormattedMessage? charges = null) + public ActionAlertTooltip(FormattedMessage name, FormattedMessage? desc, string? requires = null) { _gameTiming = IoCManager.Resolve(); @@ -52,17 +52,6 @@ namespace Content.Client.Actions.UI vbox.AddChild(description); } - if (charges != null && !string.IsNullOrWhiteSpace(charges.ToString())) - { - var chargesLabel = new RichTextLabel - { - MaxWidth = TooltipTextMaxWidth, - StyleClasses = { StyleNano.StyleClassTooltipActionCharges } - }; - chargesLabel.SetMessage(charges); - vbox.AddChild(chargesLabel); - } - vbox.AddChild(_cooldownLabel = new RichTextLabel { MaxWidth = TooltipTextMaxWidth, diff --git a/Content.Client/Administration/Components/HeadstandComponent.cs b/Content.Client/Administration/Components/HeadstandComponent.cs deleted file mode 100644 index a4e3bfc5aaf..00000000000 --- a/Content.Client/Administration/Components/HeadstandComponent.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Content.Shared.Administration.Components; -using Robust.Shared.GameStates; - -namespace Content.Client.Administration.Components; - -[RegisterComponent] -public sealed partial class HeadstandComponent : SharedHeadstandComponent -{ - -} diff --git a/Content.Client/Administration/Managers/ClientAdminManager.cs b/Content.Client/Administration/Managers/ClientAdminManager.cs index 0f740c81045..3f072691de6 100644 --- a/Content.Client/Administration/Managers/ClientAdminManager.cs +++ b/Content.Client/Administration/Managers/ClientAdminManager.cs @@ -15,6 +15,7 @@ namespace Content.Client.Administration.Managers [Dependency] private readonly IPlayerManager _player = default!; [Dependency] private readonly IClientNetManager _netMgr = default!; [Dependency] private readonly IClientConGroupController _conGroup = default!; + [Dependency] private readonly IClientConsoleHost _host = default!; [Dependency] private readonly IResourceManager _res = default!; [Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly IUserInterfaceManager _userInterface = default!; @@ -86,12 +87,12 @@ namespace Content.Client.Administration.Managers private void UpdateMessageRx(MsgUpdateAdminStatus message) { _availableCommands.Clear(); - var host = IoCManager.Resolve(); // Anything marked as Any we'll just add even if the server doesn't know about it. - foreach (var (command, instance) in host.AvailableCommands) + foreach (var (command, instance) in _host.AvailableCommands) { - if (Attribute.GetCustomAttribute(instance.GetType(), typeof(AnyCommandAttribute)) == null) continue; + if (Attribute.GetCustomAttribute(instance.GetType(), typeof(AnyCommandAttribute)) == null) + continue; _availableCommands.Add(command); } diff --git a/Content.Client/Administration/Systems/AdminFrozenSystem.cs b/Content.Client/Administration/Systems/AdminFrozenSystem.cs deleted file mode 100644 index 885585f985c..00000000000 --- a/Content.Client/Administration/Systems/AdminFrozenSystem.cs +++ /dev/null @@ -1,7 +0,0 @@ -using Content.Shared.Administration; - -namespace Content.Client.Administration.Systems; - -public sealed class AdminFrozenSystem : SharedAdminFrozenSystem -{ -} diff --git a/Content.Client/Administration/Systems/HeadstandSystem.cs b/Content.Client/Administration/Systems/HeadstandSystem.cs index d0634e4ddd6..8ab337f3ccf 100644 --- a/Content.Client/Administration/Systems/HeadstandSystem.cs +++ b/Content.Client/Administration/Systems/HeadstandSystem.cs @@ -1,4 +1,4 @@ -using Content.Client.Administration.Components; +using Content.Shared.Administration.Components; using Robust.Client.GameObjects; namespace Content.Client.Administration.Systems; diff --git a/Content.Client/Administration/UI/CustomControls/AdminLogLabel.cs b/Content.Client/Administration/UI/CustomControls/AdminLogLabel.cs deleted file mode 100644 index 0de38ce234f..00000000000 --- a/Content.Client/Administration/UI/CustomControls/AdminLogLabel.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Content.Shared.Administration.Logs; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; - -namespace Content.Client.Administration.UI.CustomControls; - -public sealed class AdminLogLabel : RichTextLabel -{ - public AdminLogLabel(ref SharedAdminLog log, HSeparator separator) - { - Log = log; - Separator = separator; - - SetMessage($"{log.Date:HH:mm:ss}: {log.Message}"); - OnVisibilityChanged += VisibilityChanged; - } - - public SharedAdminLog Log { get; } - - public HSeparator Separator { get; } - - private void VisibilityChanged(Control control) - { - Separator.Visible = Visible; - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - OnVisibilityChanged -= VisibilityChanged; - } -} diff --git a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs index c7fbf6c2dc0..8027a00c540 100644 --- a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs +++ b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs @@ -1,16 +1,15 @@ using System.Linq; +using System.Text.RegularExpressions; using Content.Client.Administration.Systems; using Content.Client.UserInterface.Controls; using Content.Client.Verbs.UI; using Content.Shared.Administration; using Robust.Client.AutoGenerated; -using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Input; -using Robust.Shared.Utility; namespace Content.Client.Administration.UI.CustomControls; @@ -96,13 +95,26 @@ public sealed partial class PlayerListControl : BoxContainer private void FilterList() { _sortedPlayerList.Clear(); + + Regex filterRegex; + // There is no neat way to handle invalid regex being submitted other than + // catching and ignoring the exception which gets thrown when it's invalid. + try + { + filterRegex = new Regex(FilterLineEdit.Text, RegexOptions.IgnoreCase); + } + catch (ArgumentException) + { + return; + } + foreach (var info in _playerList) { var displayName = $"{info.CharacterName} ({info.Username})"; if (info.IdentityName != info.CharacterName) displayName += $" [{info.IdentityName}]"; if (!string.IsNullOrEmpty(FilterLineEdit.Text) - && !displayName.ToLowerInvariant().Contains(FilterLineEdit.Text.Trim().ToLowerInvariant())) + && !filterRegex.IsMatch(displayName)) continue; _sortedPlayerList.Add(info); } diff --git a/Content.Client/Administration/UI/CustomControls/PlayerListEntry.xaml.cs b/Content.Client/Administration/UI/CustomControls/PlayerListEntry.xaml.cs index cd6a56ea71e..f62a6c71e49 100644 --- a/Content.Client/Administration/UI/CustomControls/PlayerListEntry.xaml.cs +++ b/Content.Client/Administration/UI/CustomControls/PlayerListEntry.xaml.cs @@ -1,10 +1,8 @@ using Content.Client.Stylesheets; using Content.Shared.Administration; using Robust.Client.AutoGenerated; -using Robust.Client.GameObjects; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; -using Robust.Shared.Utility; namespace Content.Client.Administration.UI.CustomControls; diff --git a/Content.Client/Administration/UI/Logs/AdminLogsControl.xaml b/Content.Client/Administration/UI/Logs/AdminLogsControl.xaml index 145a224ddec..9b5da778017 100644 --- a/Content.Client/Administration/UI/Logs/AdminLogsControl.xaml +++ b/Content.Client/Administration/UI/Logs/AdminLogsControl.xaml @@ -1,5 +1,6 @@  + xmlns:aui="clr-namespace:Content.Client.Administration.UI.CustomControls" + xmlns:ui="clr-namespace:Content.Client.Options.UI"> @@ -56,6 +57,13 @@ diff --git a/Content.Client/Lathe/UI/LatheMenu.xaml.cs b/Content.Client/Lathe/UI/LatheMenu.xaml.cs index 0475962321a..3b6a52c9499 100644 --- a/Content.Client/Lathe/UI/LatheMenu.xaml.cs +++ b/Content.Client/Lathe/UI/LatheMenu.xaml.cs @@ -33,6 +33,10 @@ public sealed partial class LatheMenu : DefaultWindow public event Action? OnServerListButtonPressed; public event Action? RecipeQueueAction; public event Action? OnClaimMiningPoints; // DeltaV + public event Action? QueueDeleteAction; + public event Action? QueueMoveUpAction; + public event Action? QueueMoveDownAction; + public event Action? DeleteFabricatingAction; public List> Recipes = new(); @@ -60,12 +64,21 @@ public sealed partial class LatheMenu : DefaultWindow }; AmountLineEdit.OnTextChanged += _ => { + if (int.TryParse(AmountLineEdit.Text, out var amount)) + { + if (amount > LatheSystem.MaxItemsPerRequest) + AmountLineEdit.Text = LatheSystem.MaxItemsPerRequest.ToString(); + else if (amount < 0) + AmountLineEdit.Text = "0"; + } + PopulateRecipes(); }; FilterOption.OnItemSelected += OnItemSelected; ServerListButton.OnPressed += a => OnServerListButtonPressed?.Invoke(a); + DeleteFabricating.OnPressed += _ => DeleteFabricatingAction?.Invoke(); } public void SetEntity(EntityUid uid) @@ -266,22 +279,27 @@ public sealed partial class LatheMenu : DefaultWindow /// Populates the build queue list with all queued items /// /// - public void PopulateQueueList(IReadOnlyCollection> queue) + public void PopulateQueueList(IReadOnlyCollection queue) { QueueList.DisposeAllChildren(); var idx = 1; - foreach (var recipeProto in queue) + foreach (var batch in queue) { - var recipe = _prototypeManager.Index(recipeProto); - var queuedRecipeBox = new BoxContainer(); - queuedRecipeBox.Orientation = BoxContainer.LayoutOrientation.Horizontal; + var recipe = _prototypeManager.Index(batch.Recipe); - queuedRecipeBox.AddChild(GetRecipeDisplayControl(recipe)); + var itemName = _lathe.GetRecipeName(batch.Recipe); + string displayText; + if (batch.ItemsRequested > 1) + displayText = Loc.GetString("lathe-menu-item-batch", ("index", idx), ("name", itemName), ("printed", batch.ItemsPrinted), ("total", batch.ItemsRequested)); + else + displayText = Loc.GetString("lathe-menu-item-single", ("index", idx), ("name", itemName)); + + var queuedRecipeBox = new QueuedRecipeControl(displayText, idx - 1, GetRecipeDisplayControl(recipe)); + queuedRecipeBox.OnDeletePressed += s => QueueDeleteAction?.Invoke(s); + queuedRecipeBox.OnMoveUpPressed += s => QueueMoveUpAction?.Invoke(s); + queuedRecipeBox.OnMoveDownPressed += s => QueueMoveDownAction?.Invoke(s); - var queuedRecipeLabel = new Label(); - queuedRecipeLabel.Text = $"{idx}. {_lathe.GetRecipeName(recipe)}"; - queuedRecipeBox.AddChild(queuedRecipeLabel); QueueList.AddChild(queuedRecipeBox); idx++; } diff --git a/Content.Client/Lathe/UI/QueuedRecipeControl.xaml b/Content.Client/Lathe/UI/QueuedRecipeControl.xaml new file mode 100644 index 00000000000..b1d4b496a14 --- /dev/null +++ b/Content.Client/Lathe/UI/QueuedRecipeControl.xaml @@ -0,0 +1,35 @@ + + + + + diff --git a/Content.Client/Lathe/UI/QueuedRecipeControl.xaml.cs b/Content.Client/Lathe/UI/QueuedRecipeControl.xaml.cs new file mode 100644 index 00000000000..c4ba9803b0d --- /dev/null +++ b/Content.Client/Lathe/UI/QueuedRecipeControl.xaml.cs @@ -0,0 +1,36 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.Lathe.UI; + +[GenerateTypedNameReferences] +public sealed partial class QueuedRecipeControl : Control +{ + public Action? OnDeletePressed; + public Action? OnMoveUpPressed; + public Action? OnMoveDownPressed; + + public QueuedRecipeControl(string displayText, int index, Control displayControl) + { + RobustXamlLoader.Load(this); + + RecipeName.Text = displayText; + RecipeDisplayContainer.AddChild(displayControl); + + MoveUp.OnPressed += (_) => + { + OnMoveUpPressed?.Invoke(index); + }; + + MoveDown.OnPressed += (_) => + { + OnMoveDownPressed?.Invoke(index); + }; + + Delete.OnPressed += (_) => + { + OnDeletePressed?.Invoke(index); + }; + } +} diff --git a/Content.Client/Light/EntitySystems/LightBulbSystem.cs b/Content.Client/Light/EntitySystems/LightBulbSystem.cs index c028cc64c6f..a3698fc199d 100644 --- a/Content.Client/Light/EntitySystems/LightBulbSystem.cs +++ b/Content.Client/Light/EntitySystems/LightBulbSystem.cs @@ -1,36 +1,46 @@ using Content.Shared.Light.Components; +using Content.Shared.Light.EntitySystems; using Robust.Client.GameObjects; -namespace Content.Client.Light.Visualizers; +namespace Content.Client.Light.EntitySystems; -public sealed class LightBulbSystem : VisualizerSystem +public sealed class LightBulbSystem : SharedLightBulbSystem { - protected override void OnAppearanceChange(EntityUid uid, LightBulbComponent comp, ref AppearanceChangeEvent args) + [Dependency] private readonly AppearanceSystem _appearance = default!; + [Dependency] private readonly SpriteSystem _sprite = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnAppearanceChange); + } + + private void OnAppearanceChange(EntityUid uid, LightBulbComponent comp, ref AppearanceChangeEvent args) { if (args.Sprite == null) return; // update sprite state - if (AppearanceSystem.TryGetData(uid, LightBulbVisuals.State, out var state, args.Component)) + if (_appearance.TryGetData(uid, LightBulbVisuals.State, out var state, args.Component)) { switch (state) { case LightBulbState.Normal: - SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.NormalSpriteState); + _sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.NormalSpriteState); break; case LightBulbState.Broken: - SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BrokenSpriteState); + _sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BrokenSpriteState); break; case LightBulbState.Burned: - SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BurnedSpriteState); + _sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BurnedSpriteState); break; } } // also update sprites color - if (AppearanceSystem.TryGetData(uid, LightBulbVisuals.Color, out var color, args.Component)) + if (_appearance.TryGetData(uid, LightBulbVisuals.Color, out var color, args.Component)) { - SpriteSystem.SetColor((uid, args.Sprite), color); + _sprite.SetColor((uid, args.Sprite), color); } } } diff --git a/Content.Client/Light/EntitySystems/PoweredLightSystem.cs b/Content.Client/Light/EntitySystems/PoweredLightSystem.cs new file mode 100644 index 00000000000..b8a6b16da4e --- /dev/null +++ b/Content.Client/Light/EntitySystems/PoweredLightSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Light.EntitySystems; + +namespace Content.Client.Light.EntitySystems; + +public sealed class PoweredLightSystem : SharedPoweredLightSystem; diff --git a/Content.Client/Lobby/LobbyUIController.cs b/Content.Client/Lobby/LobbyUIController.cs index 121e8dbe71f..ec052adea55 100644 --- a/Content.Client/Lobby/LobbyUIController.cs +++ b/Content.Client/Lobby/LobbyUIController.cs @@ -72,6 +72,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered RefreshProfileEditor()); + _configurationManager.OnValueChanged(CCVars.GameRoleLoadoutTimers, _ => RefreshProfileEditor()); _configurationManager.OnValueChanged(CCVars.GameRoleWhitelist, _ => RefreshProfileEditor()); } diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs index 590f9ceae87..dada2ab5ca9 100644 --- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs +++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs @@ -1093,7 +1093,7 @@ namespace Content.Client.Lobby.UI _loadoutWindow = new LoadoutWindow(Profile, roleLoadout, roleLoadoutProto, _playerManager.LocalSession, collection) { - Title = jobProto?.ID + "-loadout", + Title = Loc.GetString("loadout-window-title-loadout", ("job", $"{jobProto?.LocalizedName}")), }; // Refresh the buttons etc. diff --git a/Content.Client/Medical/Cryogenics/CryoPodSystem.cs b/Content.Client/Medical/Cryogenics/CryoPodSystem.cs index 13db5a84215..c1cbfc573ee 100644 --- a/Content.Client/Medical/Cryogenics/CryoPodSystem.cs +++ b/Content.Client/Medical/Cryogenics/CryoPodSystem.cs @@ -1,7 +1,5 @@ using System.Numerics; -using Content.Shared.Emag.Systems; using Content.Shared.Medical.Cryogenics; -using Content.Shared.Verbs; using Robust.Client.GameObjects; namespace Content.Client.Medical.Cryogenics; @@ -15,11 +13,6 @@ public sealed class CryoPodSystem : SharedCryoPodSystem { base.Initialize(); - SubscribeLocalEvent(OnComponentInit); - SubscribeLocalEvent>(AddAlternativeVerbs); - SubscribeLocalEvent(OnEmagged); - SubscribeLocalEvent(OnCryoPodPryFinished); - SubscribeLocalEvent(OnAppearanceChange); SubscribeLocalEvent(OnCryoPodInsertion); SubscribeLocalEvent(OnCryoPodRemoval); @@ -53,8 +46,8 @@ public sealed class CryoPodSystem : SharedCryoPodSystem return; } - if (!_appearance.TryGetData(uid, CryoPodComponent.CryoPodVisuals.ContainsEntity, out var isOpen, args.Component) - || !_appearance.TryGetData(uid, CryoPodComponent.CryoPodVisuals.IsOn, out var isOn, args.Component)) + if (!_appearance.TryGetData(uid, CryoPodVisuals.ContainsEntity, out var isOpen, args.Component) + || !_appearance.TryGetData(uid, CryoPodVisuals.IsOn, out var isOn, args.Component)) { return; } diff --git a/Content.Client/Medical/SuitSensors/SuitSensorSystem.cs b/Content.Client/Medical/SuitSensors/SuitSensorSystem.cs new file mode 100644 index 00000000000..75868e08d9b --- /dev/null +++ b/Content.Client/Medical/SuitSensors/SuitSensorSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Medical.SuitSensors; + +namespace Content.Client.Medical.SuitSensors; + +public sealed class SuitSensorSystem : SharedSuitSensorSystem; diff --git a/Content.Client/Morgue/CrematoriumSystem.cs b/Content.Client/Morgue/CrematoriumSystem.cs new file mode 100644 index 00000000000..66eac263c25 --- /dev/null +++ b/Content.Client/Morgue/CrematoriumSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Morgue; + +namespace Content.Client.Morgue; + +public sealed class CrematoriumSystem : SharedCrematoriumSystem; diff --git a/Content.Client/Morgue/MorgueSystem.cs b/Content.Client/Morgue/MorgueSystem.cs new file mode 100644 index 00000000000..b8d2f109fbe --- /dev/null +++ b/Content.Client/Morgue/MorgueSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Morgue; + +namespace Content.Client.Morgue; + +public sealed class MorgueSystem : SharedMorgueSystem; diff --git a/Content.Client/Movement/Systems/ContentEyeSystem.cs b/Content.Client/Movement/Systems/ContentEyeSystem.cs index 518a4a1bd40..a332d25f9a1 100644 --- a/Content.Client/Movement/Systems/ContentEyeSystem.cs +++ b/Content.Client/Movement/Systems/ContentEyeSystem.cs @@ -1,7 +1,6 @@ using System.Numerics; using Content.Shared.Movement.Components; using Content.Shared.Movement.Systems; -using Robust.Client.GameObjects; using Robust.Client.Player; namespace Content.Client.Movement.Systems; @@ -63,4 +62,15 @@ public sealed class ContentEyeSystem : SharedContentEyeSystem UpdateEyeOffset((entity, eyeComponent)); } } + + public override void Update(float frameTime) + { + base.Update(frameTime); + // TODO: Ideally we wouldn't want this to run in both FrameUpdate and Update, but we kind of have to since the visual update happens in FrameUpdate, but interaction update happens in Update. It's a workaround and a better solution should be found. + var eyeEntities = AllEntityQuery(); + while (eyeEntities.MoveNext(out var entity, out ContentEyeComponent? contentComponent, out EyeComponent? eyeComponent)) + { + UpdateEyeOffset((entity, eyeComponent)); + } + } } diff --git a/Content.Client/Movement/Systems/EyeCursorOffsetSystem.cs b/Content.Client/Movement/Systems/EyeCursorOffsetSystem.cs index 96a04710974..5227e892545 100644 --- a/Content.Client/Movement/Systems/EyeCursorOffsetSystem.cs +++ b/Content.Client/Movement/Systems/EyeCursorOffsetSystem.cs @@ -1,10 +1,10 @@ using System.Numerics; using Content.Shared.Movement.Components; // DeltaV - make EyeCursorOffsetComponent entirely Shared +using Content.Client.Viewport; using Content.Shared.Camera; using Robust.Client.Graphics; using Robust.Client.Input; using Robust.Shared.Map; -using Robust.Client.Player; namespace Content.Client.Movement.Systems; @@ -12,13 +12,10 @@ public sealed partial class EyeCursorOffsetSystem : EntitySystem { [Dependency] private readonly IEyeManager _eyeManager = default!; [Dependency] private readonly IInputManager _inputManager = default!; - [Dependency] private readonly IPlayerManager _player = default!; - [Dependency] private readonly SharedTransformSystem _transform = default!; - [Dependency] private readonly IClyde _clyde = default!; // This value is here to make sure the user doesn't have to move their mouse // all the way out to the edge of the screen to get the full offset. - static private float _edgeOffset = 0.9f; + private static float _edgeOffset = 0.8f; public override void Initialize() { @@ -46,25 +43,29 @@ public sealed partial class EyeCursorOffsetSystem : EntitySystem public Vector2? OffsetAfterMouse(EntityUid uid, EyeCursorOffsetComponent? component) { - var localPlayer = _player.LocalEntity; - var mousePos = _inputManager.MouseScreenPosition; - var screenSize = _clyde.MainWindow.Size; - var minValue = MathF.Min(screenSize.X / 2, screenSize.Y / 2) * _edgeOffset; - - var mouseNormalizedPos = new Vector2(-(mousePos.X - screenSize.X / 2) / minValue, (mousePos.Y - screenSize.Y / 2) / minValue); // X needs to be inverted here for some reason, otherwise it ends up flipped. - - if (localPlayer == null) + // We need the main viewport where the game content is displayed, as certain UI layouts (e.g. Separated HUD) can make it a different size to the game window. + if (_eyeManager.MainViewport is not ScalingViewport vp) return null; - var playerPos = _transform.GetWorldPosition(localPlayer.Value); + var mousePos = _inputManager.MouseScreenPosition.Position; // TODO: If we ever get a right-aligned Separated HUD setting, this might need to be adjusted for that. + + var viewportSize = vp.PixelSize; // The size of the game viewport, including black bars - does not include the chatbox in Separated HUD view. + var scalingViewportSize = vp.ViewportSize * vp.CurrentRenderScale; // The size of the viewport in which the game is rendered (i.e. not including black bars). Note! Can extend outside the game window with certain zoom settings! + var visibleViewportSize = Vector2.Min(viewportSize, scalingViewportSize); // The size of the game viewport that is "actually visible" to the player, cutting off over-extensions and not counting black bar padding. + + Matrix3x2.Invert(_eyeManager.MainViewport.GetLocalToScreenMatrix(), out var matrix); + var mouseCoords = Vector2.Transform(mousePos, matrix); // Gives the mouse position inside of the *scaling viewport*, i.e. 0,0 is inside the black bars. Note! 0,0 can be outside the game window with certain zoom settings! + + var boundedMousePos = Vector2.Clamp(Vector2.Min(mouseCoords, mousePos), Vector2.Zero, visibleViewportSize); // Mouse position inside the visible game viewport's bounds. + + var offsetRadius = MathF.Min(visibleViewportSize.X / 2f, visibleViewportSize.Y / 2f) * _edgeOffset; + var mouseNormalizedPos = new Vector2(-(boundedMousePos.X - visibleViewportSize.X / 2f) / offsetRadius, (boundedMousePos.Y - visibleViewportSize.Y / 2f) / offsetRadius); if (component == null) - { component = EnsureComp(uid); - } // Doesn't move the offset if the mouse has left the game window! - if (mousePos.Window != WindowId.Invalid) + if (_inputManager.MouseScreenPosition.Window != WindowId.Invalid) { // The offset must account for the in-world rotation. var eyeRotation = _eyeManager.CurrentEye.Rotation; @@ -85,7 +86,7 @@ public sealed partial class EyeCursorOffsetSystem : EntitySystem Vector2 vectorOffset = component.TargetPosition - component.CurrentPosition; if (vectorOffset.Length() > component.OffsetSpeed) { - vectorOffset = vectorOffset.Normalized() * component.OffsetSpeed; + vectorOffset = vectorOffset.Normalized() * component.OffsetSpeed; // TODO: Probably needs to properly account for time delta or something. } component.CurrentPosition += vectorOffset; } diff --git a/Content.Client/NPC/PathfindingSystem.cs b/Content.Client/NPC/PathfindingSystem.cs index 0c72a8f99ff..dc8fd984331 100644 --- a/Content.Client/NPC/PathfindingSystem.cs +++ b/Content.Client/NPC/PathfindingSystem.cs @@ -20,6 +20,7 @@ namespace Content.Client.NPC [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IInputManager _inputManager = default!; [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IOverlayManager _overlayManager = default!; [Dependency] private readonly IResourceCache _cache = default!; [Dependency] private readonly NPCSteeringSystem _steering = default!; [Dependency] private readonly MapSystem _mapSystem = default!; @@ -30,17 +31,15 @@ namespace Content.Client.NPC get => _modes; set { - var overlayManager = IoCManager.Resolve(); - if (value == PathfindingDebugMode.None) { Breadcrumbs.Clear(); Polys.Clear(); - overlayManager.RemoveOverlay(); + _overlayManager.RemoveOverlay(); } - else if (!overlayManager.HasOverlay()) + else if (!_overlayManager.HasOverlay()) { - overlayManager.AddOverlay(new PathfindingOverlay(EntityManager, _eyeManager, _inputManager, _mapManager, _cache, this, _mapSystem, _transformSystem)); + _overlayManager.AddOverlay(new PathfindingOverlay(EntityManager, _eyeManager, _inputManager, _mapManager, _cache, this, _mapSystem, _transformSystem)); } if ((value & PathfindingDebugMode.Steering) != 0x0) diff --git a/Content.Client/NetworkConfigurator/NetworkConfiguratorConfigurationMenu.xaml b/Content.Client/NetworkConfigurator/NetworkConfiguratorConfigurationMenu.xaml index 469faf209d3..da0b9f60d8b 100644 --- a/Content.Client/NetworkConfigurator/NetworkConfiguratorConfigurationMenu.xaml +++ b/Content.Client/NetworkConfigurator/NetworkConfiguratorConfigurationMenu.xaml @@ -5,15 +5,15 @@ -