diff --git a/.editorconfig b/.editorconfig index a8ce1d6b68..59ca35cc9a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -338,5 +338,8 @@ dotnet_naming_symbols.type_parameters_symbols.applicable_kinds = type_parameter resharper_braces_for_ifelse = required_for_multiline resharper_keep_existing_attribute_arrangement = true -[*.{csproj,xml,yml,dll.config,msbuildproj,targets}] +[*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets}] indent_size = 2 + +[{*.yaml,*.yml}] +ij_yaml_indent_sequence_value = false diff --git a/.github/workflows/build-docfx.yml b/.github/workflows/build-docfx.yml index 1c4b543743..ca1a6f0af1 100644 --- a/.github/workflows/build-docfx.yml +++ b/.github/workflows/build-docfx.yml @@ -21,7 +21,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v3.2.0 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: Install dependencies run: dotnet restore diff --git a/.github/workflows/build-map-renderer.yml b/.github/workflows/build-map-renderer.yml index e921bd2558..35aed1a7f7 100644 --- a/.github/workflows/build-map-renderer.yml +++ b/.github/workflows/build-map-renderer.yml @@ -36,7 +36,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v3.2.0 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: Install dependencies run: dotnet restore diff --git a/.github/workflows/build-test-debug.yml b/.github/workflows/build-test-debug.yml index 9abd4fbe17..47f9fd1a51 100644 --- a/.github/workflows/build-test-debug.yml +++ b/.github/workflows/build-test-debug.yml @@ -36,7 +36,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v3.2.0 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: Install dependencies run: dotnet restore diff --git a/.github/workflows/conflict-labeler.yml b/.github/workflows/conflict-labeler.yml index a78716bde6..1a9b91601d 100644 --- a/.github/workflows/conflict-labeler.yml +++ b/.github/workflows/conflict-labeler.yml @@ -14,6 +14,6 @@ jobs: - name: Check for Merge Conflicts uses: ike709/actions-label-merge-conflict@9eefdd17e10566023c46d2dc6dc04fcb8ec76142 with: - dirtyLabel: "Merge Conflict" + dirtyLabel: "Status: Merge Conflict" repoToken: "${{ secrets.GITHUB_TOKEN }}" commentOnDirty: "This pull request has conflicts, please resolve those before we can evaluate the pull request." diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7cff930c75..1ff4c49d90 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -22,17 +22,24 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v3.2.0 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: Get Engine Tag run: | cd RobustToolbox git fetch --depth=1 + - name: Install dependencies + run: dotnet restore + + - name: Build Packaging + run: dotnet build Content.Packaging --configuration Release --no-restore /m + + - name: Package server + run: dotnet run --project Content.Packaging server --platform win-x64 --platform linux-x64 --platform osx-x64 --platform linux-arm64 + - name: Package client - run: | - Tools/package_server_build.py -p win-x64 linux-x64 osx-x64 linux-arm64 - Tools/package_client_build.py + run: dotnet run --project Content.Packaging client --no-wipe-release - name: Update Build Info run: Tools/gen_build_info.py diff --git a/.github/workflows/test-packaging.yml b/.github/workflows/test-packaging.yml index 815b6a4adc..2dce502697 100644 --- a/.github/workflows/test-packaging.yml +++ b/.github/workflows/test-packaging.yml @@ -51,15 +51,19 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v3.2.0 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: Install dependencies run: dotnet restore + - name: Build Packaging + run: dotnet build Content.Packaging --configuration Release --no-restore /m + + - name: Package server + run: dotnet run --project Content.Packaging server --platform win-x64 --platform linux-x64 --platform osx-x64 --platform linux-arm64 + - name: Package client - run: | - Tools/package_server_build.py -p win-x64 linux-x64 osx-x64 linux-arm64 - Tools/package_client_build.py + run: dotnet run --project Content.Packaging client --no-wipe-release - name: Update Build Info run: Tools/gen_build_info.py diff --git a/.github/workflows/update-credits.yml b/.github/workflows/update-credits.yml index e0333096c4..fec053dc1b 100644 --- a/.github/workflows/update-credits.yml +++ b/.github/workflows/update-credits.yml @@ -23,10 +23,30 @@ jobs: # TODO #- name: Get this week's Patreons - # run: Tools/script2dumppatreons > Resources/Credits/Patrons.yml + # run: Tools/script2dumppatreons > Resources/Credits/Patrons.yml + + # MAKE SURE YOU ENABLED "Allow GitHub Actions to create and approve pull requests" IN YOUR ACTIONS, OTHERWISE IT WILL MOST LIKELY FAIL - - name: Commit new credit files - uses: stefanzweifel/git-auto-commit-action@v4 + + # For this you can use a pat token of an account with direct push access to the repo if you have protected branches. + # Uncomment this and comment the other line if you do this. + # https://github.com/stefanzweifel/git-auto-commit-action#push-to-protected-branches + + #- name: Commit new credit files + # uses: stefanzweifel/git-auto-commit-action@v4 + # with: + # commit_message: Update Credits + # commit_author: PJBot + + # This will make a PR + - name: Set current date as env variable + run: echo "NOW=$(date +'%Y-%m-%dT%H-%M-%S')" >> $GITHUB_ENV + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v5 with: - commit_message: Update Credits - commit_author: DeltaV-Bot + commit-message: Update Credits + title: Update Credits + body: This is an automated Pull Request. This PR updates the github contributors in the credits section. + author: DeltaV-Bot + branch: automated/credits-${{env.NOW}} diff --git a/.github/workflows/yaml-linter.yml b/.github/workflows/yaml-linter.yml index 254384acff..691eb29f1d 100644 --- a/.github/workflows/yaml-linter.yml +++ b/.github/workflows/yaml-linter.yml @@ -26,7 +26,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v3.2.0 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: Install dependencies run: dotnet restore - name: Build diff --git a/Content.Benchmarks/Content.Benchmarks.csproj b/Content.Benchmarks/Content.Benchmarks.csproj index a8b255c71b..049d6f5b6f 100644 --- a/Content.Benchmarks/Content.Benchmarks.csproj +++ b/Content.Benchmarks/Content.Benchmarks.csproj @@ -8,7 +8,7 @@ false Exe true - 11 + 12 diff --git a/Content.Benchmarks/EntityQueryBenchmark.cs b/Content.Benchmarks/EntityQueryBenchmark.cs new file mode 100644 index 0000000000..cef6a5e35c --- /dev/null +++ b/Content.Benchmarks/EntityQueryBenchmark.cs @@ -0,0 +1,137 @@ +#nullable enable +using System; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Content.IntegrationTests; +using Content.IntegrationTests.Pair; +using Content.Shared.Clothing.Components; +using Content.Shared.Item; +using Robust.Server.GameObjects; +using Robust.Shared; +using Robust.Shared.Analyzers; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Random; + +namespace Content.Benchmarks; + +[Virtual] +public class EntityQueryBenchmark +{ + public const string Map = "Maps/atlas.yml"; + + private TestPair _pair = default!; + private IEntityManager _entMan = default!; + private MapId _mapId = new MapId(10); + private EntityQuery _clothingQuery; + + [GlobalSetup] + public void Setup() + { + ProgramShared.PathOffset = "../../../../"; + PoolManager.Startup(null); + + _pair = PoolManager.GetServerClient().GetAwaiter().GetResult(); + _entMan = _pair.Server.ResolveDependency(); + + _pair.Server.ResolveDependency().SetSeed(42); + _pair.Server.WaitPost(() => + { + var success = _entMan.System().TryLoad(_mapId, Map, out _); + if (!success) + throw new Exception("Map load failed"); + _pair.Server.MapMan.DoMapInitialize(_mapId); + }).GetAwaiter().GetResult(); + + _clothingQuery = _entMan.GetEntityQuery(); + + // Apparently ~40% of entities are items, and 1 in 6 of those are clothing. + /* + var entCount = _entMan.EntityCount; + var itemCount = _entMan.Count(); + var clothingCount = _entMan.Count(); + var itemRatio = (float) itemCount / entCount; + var clothingRatio = (float) clothingCount / entCount; + Console.WriteLine($"Entities: {entCount}. Items: {itemRatio:P2}. Clothing: {clothingRatio:P2}."); + */ + } + + [GlobalCleanup] + public async Task Cleanup() + { + await _pair.DisposeAsync(); + PoolManager.Shutdown(); + } + + [Benchmark] + public int HasComponent() + { + var hashCode = 0; + var enumerator = _entMan.AllEntityQueryEnumerator(); + while (enumerator.MoveNext(out var uid, out var _)) + { + if (_entMan.HasComponent(uid)) + hashCode = HashCode.Combine(hashCode, uid.Id); + } + + return hashCode; + } + + [Benchmark] + public int HasComponentQuery() + { + var hashCode = 0; + var enumerator = _entMan.AllEntityQueryEnumerator(); + while (enumerator.MoveNext(out var uid, out var _)) + { + if (_clothingQuery.HasComponent(uid)) + hashCode = HashCode.Combine(hashCode, uid.Id); + } + + return hashCode; + } + + [Benchmark] + public int TryGetComponent() + { + var hashCode = 0; + var enumerator = _entMan.AllEntityQueryEnumerator(); + while (enumerator.MoveNext(out var uid, out var _)) + { + if (_entMan.TryGetComponent(uid, out ClothingComponent? clothing)) + hashCode = HashCode.Combine(hashCode, clothing.GetHashCode()); + } + + return hashCode; + } + + [Benchmark] + public int TryGetComponentQuery() + { + var hashCode = 0; + var enumerator = _entMan.AllEntityQueryEnumerator(); + while (enumerator.MoveNext(out var uid, out var _)) + { + if (_clothingQuery.TryGetComponent(uid, out var clothing)) + hashCode = HashCode.Combine(hashCode, clothing.GetHashCode()); + } + + return hashCode; + } + + /// + /// Enumerate all entities with both an item and clothing component. + /// + [Benchmark] + public int Enumerator() + { + var hashCode = 0; + var enumerator = _entMan.AllEntityQueryEnumerator(); + while (enumerator.MoveNext(out var _, out var clothing)) + { + hashCode = HashCode.Combine(hashCode, clothing.GetHashCode()); + } + + return hashCode; + } +} diff --git a/Content.Client/Actions/ActionsSystem.cs b/Content.Client/Actions/ActionsSystem.cs index 83d927c94b..508f3404ba 100644 --- a/Content.Client/Actions/ActionsSystem.cs +++ b/Content.Client/Actions/ActionsSystem.cs @@ -2,11 +2,11 @@ using System.Linq; using Content.Shared.Actions; using JetBrains.Annotations; -using Robust.Client.GameObjects; using Robust.Client.Player; using Robust.Shared.ContentPack; using Robust.Shared.GameStates; using Robust.Shared.Input.Binding; +using Robust.Shared.Player; using Robust.Shared.Serialization.Manager; using Robust.Shared.Serialization.Markdown; using Robust.Shared.Serialization.Markdown.Mapping; @@ -41,8 +41,8 @@ public sealed class ActionsSystem : SharedActionsSystem public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnPlayerAttached); - SubscribeLocalEvent(OnPlayerDetached); + SubscribeLocalEvent(OnPlayerAttached); + SubscribeLocalEvent(OnPlayerDetached); SubscribeLocalEvent(HandleComponentState); SubscribeLocalEvent(OnInstantHandleState); @@ -87,12 +87,15 @@ private void BaseHandleState(EntityUid uid, BaseActionComponent component, Ba component.Cooldown = state.Cooldown; component.UseDelay = state.UseDelay; component.Charges = state.Charges; + component.MaxCharges = state.MaxCharges; + component.RenewCharges = state.RenewCharges; component.Container = EnsureEntity(state.Container, uid); component.EntityIcon = EnsureEntity(state.EntityIcon, uid); component.CheckCanInteract = state.CheckCanInteract; component.ClientExclusive = state.ClientExclusive; component.Priority = state.Priority; component.AttachedEntity = EnsureEntity(state.AttachedEntity, uid); + component.RaiseOnUser = state.RaiseOnUser; component.AutoPopulate = state.AutoPopulate; component.Temporary = state.Temporary; component.ItemIconStyle = state.ItemIconStyle; @@ -196,12 +199,12 @@ protected override void ActionRemoved(EntityUid performer, EntityUid actionId, A return GetActions(user); } - private void OnPlayerAttached(EntityUid uid, ActionsComponent component, PlayerAttachedEvent args) + private void OnPlayerAttached(EntityUid uid, ActionsComponent component, LocalPlayerAttachedEvent args) { LinkAllActions(component); } - private void OnPlayerDetached(EntityUid uid, ActionsComponent component, PlayerDetachedEvent? args = null) + private void OnPlayerDetached(EntityUid uid, ActionsComponent component, LocalPlayerDetachedEvent? args = null) { UnlinkAllActions(); } diff --git a/Content.Client/Actions/UI/ActionAlertTooltip.cs b/Content.Client/Actions/UI/ActionAlertTooltip.cs index f48350d772..ddc498b6e9 100644 --- a/Content.Client/Actions/UI/ActionAlertTooltip.cs +++ b/Content.Client/Actions/UI/ActionAlertTooltip.cs @@ -21,7 +21,7 @@ public sealed class ActionAlertTooltip : PanelContainer /// public (TimeSpan Start, TimeSpan End)? Cooldown { get; set; } - public ActionAlertTooltip(FormattedMessage name, FormattedMessage? desc, string? requires = null) + public ActionAlertTooltip(FormattedMessage name, FormattedMessage? desc, string? requires = null, FormattedMessage? charges = null) { _gameTiming = IoCManager.Resolve(); @@ -52,6 +52,17 @@ public ActionAlertTooltip(FormattedMessage name, FormattedMessage? desc, string? 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/Managers/ClientAdminManager.cs b/Content.Client/Administration/Managers/ClientAdminManager.cs index 8978e2fd6d..1a1366c6f2 100644 --- a/Content.Client/Administration/Managers/ClientAdminManager.cs +++ b/Content.Client/Administration/Managers/ClientAdminManager.cs @@ -4,7 +4,7 @@ using Robust.Client.Player; using Robust.Shared.ContentPack; using Robust.Shared.Network; -using Robust.Shared.Players; +using Robust.Shared.Player; using Robust.Shared.Utility; namespace Content.Client.Administration.Managers diff --git a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs index 6142e3a831..41c3ac76f9 100644 --- a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs +++ b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs @@ -1,10 +1,8 @@ using System.Linq; using Content.Client.Administration.Systems; using Content.Client.UserInterface.Controls; -using Content.Client.Verbs; using Content.Client.Verbs.UI; using Content.Shared.Administration; -using Content.Shared.Input; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; using Robust.Client.UserInterface; @@ -22,7 +20,7 @@ public sealed partial class PlayerListControl : BoxContainer private List _playerList = new(); private readonly List _sortedPlayerList = new(); - public event Action? OnSelectionChanged; + public event Action? OnSelectionChanged; public IReadOnlyList PlayerInfo => _playerList; public Func? OverrideText; @@ -39,6 +37,7 @@ public PlayerListControl() RobustXamlLoader.Load(this); // Fill the Option data PlayerListContainer.ItemPressed += PlayerListItemPressed; + PlayerListContainer.ItemKeyBindDown += PlayerListItemKeyBindDown; PlayerListContainer.GenerateItem += GenerateButton; PopulateList(_adminSystem.PlayerList); FilterLineEdit.OnTextChanged += _ => FilterList(); @@ -46,22 +45,31 @@ public PlayerListControl() BackgroundPanel.PanelOverride = new StyleBoxFlat {BackgroundColor = new Color(32, 32, 40)}; } - private void PlayerListItemPressed(BaseButton.ButtonEventArgs args, ListData data) + private void PlayerListItemPressed(BaseButton.ButtonEventArgs? args, ListData? data) { - if (data is not PlayerListData {Info: var selectedPlayer}) + if (args == null || data is not PlayerListData {Info: var selectedPlayer}) return; - if (args.Event.Function == EngineKeyFunctions.UIClick) - { - OnSelectionChanged?.Invoke(selectedPlayer); - // update label text. Only required if there is some override (e.g. unread bwoink count). - if (OverrideText != null && args.Button.Children.FirstOrDefault()?.Children?.FirstOrDefault() is Label label) - label.Text = GetText(selectedPlayer); - } - else if (args.Event.Function == EngineKeyFunctions.UseSecondary && selectedPlayer.NetEntity != null) - { - _uiManager.GetUIController().OpenVerbMenu(_entManager.GetEntity(selectedPlayer.NetEntity.Value)); - } + if (args.Event.Function != EngineKeyFunctions.UIClick) + return; + + OnSelectionChanged?.Invoke(selectedPlayer); + + // update label text. Only required if there is some override (e.g. unread bwoink count). + if (OverrideText != null && args.Button.Children.FirstOrDefault()?.Children?.FirstOrDefault() is Label label) + label.Text = GetText(selectedPlayer); + } + + private void PlayerListItemKeyBindDown(GUIBoundKeyEventArgs? args, ListData? data) + { + if (args == null || data is not PlayerListData { Info: var selectedPlayer }) + return; + + if (args.Function != EngineKeyFunctions.UIRightClick || selectedPlayer.NetEntity == null) + return; + + _uiManager.GetUIController().OpenVerbMenu(selectedPlayer.NetEntity.Value, true); + args.Handle(); } public void StopFiltering() @@ -122,7 +130,7 @@ private void GenerateButton(ListData data, ListContainerButton button) } } }); - button.EnableAllKeybinds = true; + button.AddStyleClass(ListContainer.StyleClassListContainerButton); } } diff --git a/Content.Client/Administration/UI/Tabs/AdminTab/TeleportWindow.xaml.cs b/Content.Client/Administration/UI/Tabs/AdminTab/TeleportWindow.xaml.cs index c5a9bd036a..1978b5c3c0 100644 --- a/Content.Client/Administration/UI/Tabs/AdminTab/TeleportWindow.xaml.cs +++ b/Content.Client/Administration/UI/Tabs/AdminTab/TeleportWindow.xaml.cs @@ -5,7 +5,6 @@ using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Shared.IoC; -using Robust.Shared.Players; namespace Content.Client.Administration.UI.Tabs.AdminTab { diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs index 3f5df2cf00..dcb184b308 100644 --- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs +++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs @@ -1,7 +1,6 @@ using Content.Client.Station; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Map.Components; @@ -15,7 +14,7 @@ public sealed partial class ObjectsTab : Control private readonly List _objects = new(); private List _selections = new(); - public event Action? OnEntryPressed; + public event Action? OnEntryKeyBindDown; public ObjectsTab() { @@ -82,7 +81,7 @@ private void RefreshObjectList(ObjectsTabSelection selection) var ctrl = new ObjectsTabEntry(name, entity); _objects.Add(ctrl); ObjectList.AddChild(ctrl); - ctrl.OnPressed += args => OnEntryPressed?.Invoke(args); + ctrl.OnKeyBindDown += args => OnEntryKeyBindDown?.Invoke(ctrl, args); } } diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml index 92d5278ab5..0f6975e365 100644 --- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml +++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml @@ -1,6 +1,5 @@  + xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"> _players = new List(); @@ -26,11 +28,11 @@ public sealed partial class PlayerTab : Control private bool _ascending = true; private bool _showDisconnected; - public event Action? OnEntryPressed; + public event Action? OnEntryKeyBindDown; public PlayerTab() { - _entManager = IoCManager.Resolve(); + IoCManager.InjectDependencies(this); _adminSystem = _entManager.System(); RobustXamlLoader.Load(this); RefreshPlayerList(_adminSystem.PlayerList); @@ -95,13 +97,11 @@ private void RefreshPlayerList(IReadOnlyList players) foreach (var child in PlayerList.Children.ToArray()) { if (child is PlayerTabEntry) - child.Orphan(); + child.Dispose(); } _players = players; - - var playerManager = IoCManager.Resolve(); - PlayerCount.Text = $"Players: {playerManager.PlayerCount}"; + PlayerCount.Text = $"Players: {_playerMan.PlayerCount}"; var sortedPlayers = new List(players); sortedPlayers.Sort(Compare); @@ -123,7 +123,7 @@ private void RefreshPlayerList(IReadOnlyList players) player.Connected, player.PlaytimeString); entry.PlayerEntity = player.NetEntity; - entry.OnPressed += args => OnEntryPressed?.Invoke(args); + entry.OnKeyBindDown += args => OnEntryKeyBindDown?.Invoke(entry, args); entry.ToolTip = Loc.GetString("player-tab-entry-tooltip"); PlayerList.AddChild(entry); diff --git a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabEntry.xaml b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabEntry.xaml index 883681a28a..8ac90305ca 100644 --- a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabEntry.xaml +++ b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabEntry.xaml @@ -1,6 +1,5 @@  + xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"> + - + diff --git a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabHeader.xaml.cs b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabHeader.xaml.cs index 98de6dafa9..cf7ceff23c 100644 --- a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabHeader.xaml.cs +++ b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabHeader.xaml.cs @@ -7,7 +7,7 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab; [GenerateTypedNameReferences] -public sealed partial class PlayerTabHeader : ContainerButton +public sealed partial class PlayerTabHeader : Control { public event Action
? OnHeaderClicked; diff --git a/Content.Client/Alerts/ClientAlertsSystem.cs b/Content.Client/Alerts/ClientAlertsSystem.cs index bb6d2d4df4..83327ad77b 100644 --- a/Content.Client/Alerts/ClientAlertsSystem.cs +++ b/Content.Client/Alerts/ClientAlertsSystem.cs @@ -1,8 +1,8 @@ using System.Linq; using Content.Shared.Alert; using JetBrains.Annotations; -using Robust.Client.GameObjects; using Robust.Client.Player; +using Robust.Shared.Player; using Robust.Shared.Prototypes; namespace Content.Client.Alerts; @@ -22,8 +22,8 @@ public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnPlayerAttached); - SubscribeLocalEvent(OnPlayerDetached); + SubscribeLocalEvent(OnPlayerAttached); + SubscribeLocalEvent(OnPlayerDetached); SubscribeLocalEvent(ClientAlertsHandleState); } @@ -69,7 +69,7 @@ private void ClientAlertsHandleState(EntityUid uid, AlertsComponent component, r SyncAlerts?.Invoke(this, component.Alerts); } - private void OnPlayerAttached(EntityUid uid, AlertsComponent component, PlayerAttachedEvent args) + private void OnPlayerAttached(EntityUid uid, AlertsComponent component, LocalPlayerAttachedEvent args) { if (_playerManager.LocalPlayer?.ControlledEntity != uid) return; @@ -87,7 +87,7 @@ protected override void HandleComponentShutdown(EntityUid uid, AlertsComponent c ClearAlerts?.Invoke(this, EventArgs.Empty); } - private void OnPlayerDetached(EntityUid uid, AlertsComponent component, PlayerDetachedEvent args) + private void OnPlayerDetached(EntityUid uid, AlertsComponent component, LocalPlayerDetachedEvent args) { ClearAlerts?.Invoke(this, EventArgs.Empty); } diff --git a/Content.Client/Atmos/EntitySystems/AtmosDebugOverlaySystem.cs b/Content.Client/Atmos/EntitySystems/AtmosDebugOverlaySystem.cs index c849abf70e..b63d274bdc 100644 --- a/Content.Client/Atmos/EntitySystems/AtmosDebugOverlaySystem.cs +++ b/Content.Client/Atmos/EntitySystems/AtmosDebugOverlaySystem.cs @@ -1,22 +1,16 @@ -using System.Collections.Generic; using Content.Client.Atmos.Overlays; using Content.Shared.Atmos; using Content.Shared.Atmos.EntitySystems; using Content.Shared.GameTicking; using JetBrains.Annotations; using Robust.Client.Graphics; -using Robust.Shared.IoC; -using Robust.Shared.Map; -using Robust.Shared.Maths; namespace Content.Client.Atmos.EntitySystems { [UsedImplicitly] internal sealed class AtmosDebugOverlaySystem : SharedAtmosDebugOverlaySystem { - - private readonly Dictionary _tileData = - new(); + public readonly Dictionary TileData = new(); // Configuration set by debug commands and used by AtmosDebugOverlay { /// Value source for display @@ -48,20 +42,20 @@ public override void Initialize() private void OnGridRemoved(GridRemovalEvent ev) { - if (_tileData.ContainsKey(ev.EntityUid)) + if (TileData.ContainsKey(ev.EntityUid)) { - _tileData.Remove(ev.EntityUid); + TileData.Remove(ev.EntityUid); } } private void HandleAtmosDebugOverlayMessage(AtmosDebugOverlayMessage message) { - _tileData[GetEntity(message.GridId)] = message; + TileData[GetEntity(message.GridId)] = message; } private void HandleAtmosDebugOverlayDisableMessage(AtmosDebugOverlayDisableMessage ev) { - _tileData.Clear(); + TileData.Clear(); } public override void Shutdown() @@ -74,24 +68,12 @@ public override void Shutdown() public void Reset(RoundRestartCleanupEvent ev) { - _tileData.Clear(); + TileData.Clear(); } public bool HasData(EntityUid gridId) { - return _tileData.ContainsKey(gridId); - } - - public AtmosDebugOverlayData? GetData(EntityUid gridIndex, Vector2i indices) - { - if (!_tileData.TryGetValue(gridIndex, out var srcMsg)) - return null; - - var relative = indices - srcMsg.BaseIdx; - if (relative.X < 0 || relative.Y < 0 || relative.X >= LocalViewRange || relative.Y >= LocalViewRange) - return null; - - return srcMsg.OverlayData[relative.X + relative.Y * LocalViewRange]; + return TileData.ContainsKey(gridId); } } diff --git a/Content.Client/Atmos/Overlays/AtmosDebugOverlay.cs b/Content.Client/Atmos/Overlays/AtmosDebugOverlay.cs index 72adf276bf..fcf3b04e53 100644 --- a/Content.Client/Atmos/Overlays/AtmosDebugOverlay.cs +++ b/Content.Client/Atmos/Overlays/AtmosDebugOverlay.cs @@ -1,187 +1,272 @@ +using System.Linq; using System.Numerics; using Content.Client.Atmos.EntitySystems; +using Content.Client.Resources; using Content.Shared.Atmos; -using Content.Shared.Atmos.EntitySystems; using Robust.Client.Graphics; +using Robust.Client.Input; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.CustomControls; using Robust.Shared.Enums; using Robust.Shared.Map; using Robust.Shared.Map.Components; +using AtmosDebugOverlayData = Content.Shared.Atmos.EntitySystems.SharedAtmosDebugOverlaySystem.AtmosDebugOverlayData; +using DebugMessage = Content.Shared.Atmos.EntitySystems.SharedAtmosDebugOverlaySystem.AtmosDebugOverlayMessage; -namespace Content.Client.Atmos.Overlays +namespace Content.Client.Atmos.Overlays; + + +public sealed class AtmosDebugOverlay : Overlay { - public sealed class AtmosDebugOverlay : Overlay + [Dependency] private readonly IEntityManager _entManager = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IInputManager _input = default!; + [Dependency] private readonly IUserInterfaceManager _ui = default!; + [Dependency] private readonly IResourceCache _cache = default!; + private readonly SharedTransformSystem _transform; + private readonly AtmosDebugOverlaySystem _system; + private readonly SharedMapSystem _map; + private readonly Font _font; + private List<(Entity, DebugMessage)> _grids = new(); + + public override OverlaySpace Space => OverlaySpace.WorldSpace | OverlaySpace.ScreenSpace; + + internal AtmosDebugOverlay(AtmosDebugOverlaySystem system) { - private readonly AtmosDebugOverlaySystem _atmosDebugOverlaySystem; + IoCManager.InjectDependencies(this); + + _system = system; + _transform = _entManager.System(); + _map = _entManager.System(); + _font = _cache.GetFont("/Fonts/NotoSans/NotoSans-Regular.ttf", 12); + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (args.Space == OverlaySpace.ScreenSpace) + { + DrawTooltip(args); + return; + } - [Dependency] private readonly IEntityManager _entManager = default!; - [Dependency] private readonly IMapManager _mapManager = default!; + var handle = args.WorldHandle; + GetGrids(args.MapId, args.WorldBounds); - public override OverlaySpace Space => OverlaySpace.WorldSpace; - private List> _grids = new(); + // IF YOU ARE ABOUT TO INTRODUCE CHUNKING OR SOME OTHER OPTIMIZATION INTO THIS CODE: + // -- THINK! -- + // 1. "Is this going to make a critical atmos debugging tool harder to debug itself?" + // 2. "Is this going to do anything that could cause the atmos debugging tool to use resources, server-side or client-side, when nobody's using it?" + // 3. "Is this going to make it harder for atmos programmers to add data that may not be chunk-friendly into the atmos debugger?" + // Nanotrasen needs YOU! to avoid premature optimization in critical debugging tools - 20kdc - internal AtmosDebugOverlay(AtmosDebugOverlaySystem system) + foreach (var (grid, msg) in _grids) { - IoCManager.InjectDependencies(this); + handle.SetTransform(_transform.GetWorldMatrix(grid)); + DrawData(msg, handle); + } - _atmosDebugOverlaySystem = system; + handle.SetTransform(Matrix3.Identity); + } + + private void DrawData(DebugMessage msg, + DrawingHandleWorld handle) + { + foreach (var data in msg.OverlayData) + { + if (data != null) + DrawGridTile(data.Value, handle); } + } + + private void DrawGridTile(AtmosDebugOverlayData data, + DrawingHandleWorld handle) + { + DrawFill(data, handle); + DrawBlocked(data, handle); + } + + private void DrawFill(AtmosDebugOverlayData data, DrawingHandleWorld handle) + { + var tile = data.Indices; + var fill = GetFillData(data); + var interp = (fill - _system.CfgBase) / _system.CfgScale; - protected override void Draw(in OverlayDrawArgs args) + Color res; + if (_system.CfgCBM) + { + // Greyscale interpolation + res = Color.InterpolateBetween(Color.Black, Color.White, interp); + } + else { - var drawHandle = args.WorldHandle; + // Red-Green-Blue interpolation + if (interp < 0.5f) + { + res = Color.InterpolateBetween(Color.Red, Color.LimeGreen, interp * 2); + } + else + { + res = Color.InterpolateBetween(Color.LimeGreen, Color.Blue, (interp - 0.5f) * 2); + } + } - var mapId = args.Viewport.Eye!.Position.MapId; - var worldBounds = args.WorldBounds; + res = res.WithAlpha(0.75f); + handle.DrawRect(Box2.FromDimensions(new Vector2(tile.X, tile.Y), new Vector2(1, 1)), res); + } - // IF YOU ARE ABOUT TO INTRODUCE CHUNKING OR SOME OTHER OPTIMIZATION INTO THIS CODE: - // -- THINK! -- - // 1. "Is this going to make a critical atmos debugging tool harder to debug itself?" - // 2. "Is this going to do anything that could cause the atmos debugging tool to use resources, server-side or client-side, when nobody's using it?" - // 3. "Is this going to make it harder for atmos programmers to add data that may not be chunk-friendly into the atmos debugger?" - // Nanotrasen needs YOU! to avoid premature optimization in critical debugging tools - 20kdc + private float GetFillData(AtmosDebugOverlayData data) + { + if (data.Moles == null) + return 0; - _grids.Clear(); + switch (_system.CfgMode) + { + case AtmosDebugOverlayMode.TotalMoles: + var total = 0f; + foreach (var f in data.Moles) + { + total += f; + } - _mapManager.FindGridsIntersecting(mapId, worldBounds, ref _grids, (EntityUid uid, MapGridComponent grid, - ref List> state) => - { - state.Add((uid, grid)); - return true; - }); + return total; + case AtmosDebugOverlayMode.GasMoles: + return data.Moles[_system.CfgSpecificGas]; + default: + return data.Temperature; + } + } - foreach (var (uid, mapGrid) in _grids) - { - if (!_atmosDebugOverlaySystem.HasData(uid) || - !_entManager.TryGetComponent(uid, out var xform)) - continue; + private void DrawBlocked(AtmosDebugOverlayData data, DrawingHandleWorld handle) + { + var tile = data.Indices; + var tileCentre = tile + 0.5f * Vector2.One; + CheckAndShowBlockDir(data, handle, AtmosDirection.North, tileCentre); + CheckAndShowBlockDir(data, handle, AtmosDirection.South, tileCentre); + CheckAndShowBlockDir(data, handle, AtmosDirection.East, tileCentre); + CheckAndShowBlockDir(data, handle, AtmosDirection.West, tileCentre); + + // -- Pressure Direction -- + if (data.PressureDirection != AtmosDirection.Invalid) + { + DrawPressureDirection(handle, data.PressureDirection, tileCentre, Color.Blue); + } + else if (data.LastPressureDirection != AtmosDirection.Invalid) + { + DrawPressureDirection(handle, data.LastPressureDirection, tileCentre, Color.LightGray); + } + + // -- Excited Groups -- + if (data.InExcitedGroup is {} grp) + { + var basisA = tile; + var basisB = tile + new Vector2(1.0f, 1.0f); + var basisC = tile + new Vector2(0.0f, 1.0f); + var basisD = tile + new Vector2(1.0f, 0.0f); + var color = Color.White // Use first three nibbles for an unique color... Good enough? + .WithRed(grp & 0x000F) + .WithGreen((grp & 0x00F0) >> 4) + .WithBlue((grp & 0x0F00) >> 8); + handle.DrawLine(basisA, basisB, color); + handle.DrawLine(basisC, basisD, color); + } + + if (data.IsSpace) + handle.DrawCircle(tileCentre, 0.15f, Color.Yellow); + + if (data.MapAtmosphere) + handle.DrawCircle(tileCentre, 0.1f, Color.Orange); + + if (data.NoGrid) + handle.DrawCircle(tileCentre, 0.05f, Color.Black); + } + + private void CheckAndShowBlockDir(AtmosDebugOverlayData data, DrawingHandleWorld handle, AtmosDirection dir, + Vector2 tileCentre) + { + if (!data.BlockDirection.HasFlag(dir)) + return; + + // Account for South being 0. + var atmosAngle = dir.ToAngle() - Angle.FromDegrees(90); + var atmosAngleOfs = atmosAngle.ToVec() * 0.45f; + var atmosAngleOfsR90 = new Vector2(atmosAngleOfs.Y, -atmosAngleOfs.X); + var basisA = tileCentre + atmosAngleOfs - atmosAngleOfsR90; + var basisB = tileCentre + atmosAngleOfs + atmosAngleOfsR90; + handle.DrawLine(basisA, basisB, Color.Azure); + } - drawHandle.SetTransform(xform.WorldMatrix); + private void DrawPressureDirection( + DrawingHandleWorld handle, + AtmosDirection d, + Vector2 center, + Color color) + { + // Account for South being 0. + var atmosAngle = d.ToAngle() - Angle.FromDegrees(90); + var atmosAngleOfs = atmosAngle.ToVec() * 0.4f; + handle.DrawLine(center, center + atmosAngleOfs, color); + } + + private void DrawTooltip(in OverlayDrawArgs args) + { + var handle = args.ScreenHandle; + var mousePos = _input.MouseScreenPosition; + if (!mousePos.IsValid) + return; + + if (_ui.MouseGetControl(mousePos) is not IViewportControl viewport) + return; - for (var pass = 0; pass < 2; pass++) + var coords= viewport.PixelToMap(mousePos.Position); + var box = Box2.CenteredAround(coords.Position, 3 * Vector2.One); + GetGrids(coords.MapId, new Box2Rotated(box)); + + foreach (var (grid, msg) in _grids) + { + var index = _map.WorldToTile(grid, grid, coords.Position); + foreach (var data in msg.OverlayData) + { + if (data?.Indices == index) { - foreach (var tile in mapGrid.GetTilesIntersecting(worldBounds)) - { - var dataMaybeNull = _atmosDebugOverlaySystem.GetData(uid, tile.GridIndices); - if (dataMaybeNull != null) - { - var data = (SharedAtmosDebugOverlaySystem.AtmosDebugOverlayData) dataMaybeNull; - if (pass == 0) - { - // -- Mole Count -- - float total = 0; - switch (_atmosDebugOverlaySystem.CfgMode) - { - case AtmosDebugOverlayMode.TotalMoles: - foreach (var f in data.Moles) - { - total += f; - } - break; - case AtmosDebugOverlayMode.GasMoles: - total = data.Moles[_atmosDebugOverlaySystem.CfgSpecificGas]; - break; - case AtmosDebugOverlayMode.Temperature: - total = data.Temperature; - break; - } - var interp = (total - _atmosDebugOverlaySystem.CfgBase) / _atmosDebugOverlaySystem.CfgScale; - Color res; - if (_atmosDebugOverlaySystem.CfgCBM) - { - // Greyscale interpolation - res = Color.InterpolateBetween(Color.Black, Color.White, interp); - } - else - { - // Red-Green-Blue interpolation - if (interp < 0.5f) - { - res = Color.InterpolateBetween(Color.Red, Color.LimeGreen, interp * 2); - } - else - { - res = Color.InterpolateBetween(Color.LimeGreen, Color.Blue, (interp - 0.5f) * 2); - } - } - res = res.WithAlpha(0.75f); - drawHandle.DrawRect(Box2.FromDimensions(new Vector2(tile.X, tile.Y), new Vector2(1, 1)), res); - } - else if (pass == 1) - { - // -- Blocked Directions -- - void CheckAndShowBlockDir(AtmosDirection dir) - { - if (data.BlockDirection.HasFlag(dir)) - { - // Account for South being 0. - var atmosAngle = dir.ToAngle() - Angle.FromDegrees(90); - var atmosAngleOfs = atmosAngle.ToVec() * 0.45f; - var atmosAngleOfsR90 = new Vector2(atmosAngleOfs.Y, -atmosAngleOfs.X); - var tileCentre = new Vector2(tile.X + 0.5f, tile.Y + 0.5f); - var basisA = tileCentre + atmosAngleOfs - atmosAngleOfsR90; - var basisB = tileCentre + atmosAngleOfs + atmosAngleOfsR90; - drawHandle.DrawLine(basisA, basisB, Color.Azure); - } - } - CheckAndShowBlockDir(AtmosDirection.North); - CheckAndShowBlockDir(AtmosDirection.South); - CheckAndShowBlockDir(AtmosDirection.East); - CheckAndShowBlockDir(AtmosDirection.West); - - void DrawPressureDirection( - DrawingHandleWorld handle, - AtmosDirection d, - TileRef t, - Color color) - { - // Account for South being 0. - var atmosAngle = d.ToAngle() - Angle.FromDegrees(90); - var atmosAngleOfs = atmosAngle.ToVec() * 0.4f; - var tileCentre = new Vector2(t.X + 0.5f, t.Y + 0.5f); - var basisA = tileCentre; - var basisB = tileCentre + atmosAngleOfs; - handle.DrawLine(basisA, basisB, color); - } - - // -- Pressure Direction -- - if (data.PressureDirection != AtmosDirection.Invalid) - { - DrawPressureDirection(drawHandle, data.PressureDirection, tile, Color.Blue); - } - else if (data.LastPressureDirection != AtmosDirection.Invalid) - { - DrawPressureDirection(drawHandle, data.LastPressureDirection, tile, Color.LightGray); - } - - var tilePos = new Vector2(tile.X, tile.Y); - - // -- Excited Groups -- - if (data.InExcitedGroup != 0) - { - var basisA = tilePos; - var basisB = tilePos + new Vector2(1.0f, 1.0f); - var basisC = tilePos + new Vector2(0.0f, 1.0f); - var basisD = tilePos + new Vector2(1.0f, 0.0f); - var color = Color.White // Use first three nibbles for an unique color... Good enough? - .WithRed( data.InExcitedGroup & 0x000F) - .WithGreen((data.InExcitedGroup & 0x00F0) >>4) - .WithBlue( (data.InExcitedGroup & 0x0F00) >>8); - drawHandle.DrawLine(basisA, basisB, color); - drawHandle.DrawLine(basisC, basisD, color); - } - - // -- Space Tiles -- - if (data.IsSpace) - { - drawHandle.DrawCircle(tilePos + Vector2.One/2, 0.125f, Color.Orange); - } - } - } - } + DrawTooltip(handle, mousePos.Position, data.Value); + return; } } - - drawHandle.SetTransform(Matrix3.Identity); } } + + private void DrawTooltip(DrawingHandleScreen handle, Vector2 pos, AtmosDebugOverlayData data) + { + var lineHeight = _font.GetLineHeight(1f); + var offset = new Vector2(0, lineHeight); + + var moles = data.Moles == null + ? "No Air" + : data.Moles.Sum().ToString(); + + handle.DrawString(_font, pos, $"Moles: {moles}"); + pos += offset; + handle.DrawString(_font, pos, $"Temp: {data.Temperature}"); + pos += offset; + handle.DrawString(_font, pos, $"Excited: {data.InExcitedGroup?.ToString() ?? "None"}"); + pos += offset; + handle.DrawString(_font, pos, $"Space: {data.IsSpace}"); + pos += offset; + handle.DrawString(_font, pos, $"Map: {data.MapAtmosphere}"); + pos += offset; + handle.DrawString(_font, pos, $"NoGrid: {data.NoGrid}"); + } + + private void GetGrids(MapId mapId, Box2Rotated box) + { + _grids.Clear(); + _mapManager.FindGridsIntersecting(mapId, box, ref _grids, (EntityUid uid, MapGridComponent grid, + ref List<(Entity, DebugMessage)> state) => + { + if (_system.TileData.TryGetValue(uid, out var data)) + state.Add(((uid, grid), data)); + return true; + }); + } } diff --git a/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs b/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs index ccf9e370e3..b105e629cf 100644 --- a/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs +++ b/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs @@ -241,6 +241,10 @@ private void GenerateGasDisplay(GasMixEntry gasMix, Control parent) { Orientation = BoxContainer.LayoutOrientation.Vertical }; + var tablePercent = new BoxContainer + { + Orientation = BoxContainer.LayoutOrientation.Vertical + }; dataContainer.AddChild(new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Horizontal, @@ -252,7 +256,13 @@ private void GenerateGasDisplay(GasMixEntry gasMix, Control parent) MinSize = new Vector2(10, 0), HorizontalExpand = true }, - tableVal + tableVal, + new Control + { + MinSize = new Vector2(10, 0), + HorizontalExpand = true + }, + tablePercent } }); // This is the gas bar thingy @@ -260,6 +270,7 @@ private void GenerateGasDisplay(GasMixEntry gasMix, Control parent) var gasBar = new SplitBar { MinHeight = height, + MinBarSize = new Vector2(12, 0) }; // Separator dataContainer.AddChild(new Control @@ -274,6 +285,17 @@ private void GenerateGasDisplay(GasMixEntry gasMix, Control parent) totalGasAmount += gas.Amount; } + tableKey.AddChild(new Label + { Text = Loc.GetString("gas-analyzer-window-gas-column-name"), Align = Label.AlignMode.Center }); + tableVal.AddChild(new Label + { Text = Loc.GetString("gas-analyzer-window-molarity-column-name"), Align = Label.AlignMode.Center }); + tablePercent.AddChild(new Label + { Text = Loc.GetString("gas-analyzer-window-percentage-column-name"), Align = Label.AlignMode.Center }); + + tableKey.AddChild(new StripeBack()); + tableVal.AddChild(new StripeBack()); + tablePercent.AddChild(new StripeBack()); + for (var j = 0; j < gasMix.Gases.Length; j++) { var gas = gasMix.Gases[j]; @@ -286,10 +308,14 @@ private void GenerateGasDisplay(GasMixEntry gasMix, Control parent) tableVal.AddChild(new Label { Text = Loc.GetString("gas-analyzer-window-molarity-text", - ("mol", $"{gas.Amount:0.##}"), - ("percentage", $"{(gas.Amount / totalGasAmount * 100):0.#}")), + ("mol", $"{gas.Amount:0.00}")), Align = Label.AlignMode.Right, - HorizontalExpand = true + }); + tablePercent.AddChild(new Label + { + Text = Loc.GetString("gas-analyzer-window-percentage-text", + ("percentage", $"{(gas.Amount / totalGasAmount * 100):0.0}")), + Align = Label.AlignMode.Right }); // Add to the gas bar //TODO: highlight the currently hover one diff --git a/Content.Client/Audio/AmbientSoundSystem.cs b/Content.Client/Audio/AmbientSoundSystem.cs index aebacb94f6..d66ee434a2 100644 --- a/Content.Client/Audio/AmbientSoundSystem.cs +++ b/Content.Client/Audio/AmbientSoundSystem.cs @@ -1,16 +1,21 @@ -using System.Linq; -using System.Numerics; using Content.Shared.Audio; using Content.Shared.CCVar; -using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.Player; using Robust.Shared.Audio; +using Robust.Shared.Log; using Robust.Shared.Configuration; +using Robust.Shared.Map; using Robust.Shared.Physics; using Robust.Shared.Random; using Robust.Shared.Timing; using Robust.Shared.Utility; +using System.Linq; +using System.Numerics; +using Robust.Client.GameObjects; +using Robust.Shared.Audio.Effects; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Player; namespace Content.Client.Audio; //TODO: This is using a incomplete version of the whole "only play nearest sounds" algo, that breaks down a bit should the ambient sound cap get hit. @@ -41,14 +46,18 @@ protected override void QueueUpdate(EntityUid uid, AmbientSoundComponent ambienc private TimeSpan _targetTime = TimeSpan.Zero; private float _ambienceVolume = 0.0f; - private static AudioParams _params = AudioParams.Default.WithVariation(0.01f).WithLoop(true).WithAttenuation(Attenuation.LinearDistance); + private static AudioParams _params = AudioParams.Default + .WithVariation(0.01f) + .WithLoop(true) + .WithAttenuation(Attenuation.LinearDistance) + .WithMaxDistance(7f); /// /// How many times we can be playing 1 particular sound at once. /// private int MaxSingleSound => (int) (_maxAmbientCount / (16.0f / 6.0f)); - private readonly Dictionary, (IPlayingAudioStream? Stream, SoundSpecifier Sound, string Path)> _playingSounds = new(); + private readonly Dictionary _playingSounds = new(); private readonly Dictionary _playingCount = new(); public bool OverlayEnabled @@ -92,32 +101,32 @@ public override void Initialize() _cfg.OnValueChanged(CCVars.AmbientCooldown, SetCooldown, true); _cfg.OnValueChanged(CCVars.MaxAmbientSources, SetAmbientCount, true); _cfg.OnValueChanged(CCVars.AmbientRange, SetAmbientRange, true); - _cfg.OnValueChanged(CCVars.AmbienceVolume, SetAmbienceVolume, true); + _cfg.OnValueChanged(CCVars.AmbienceVolume, SetAmbienceGain, true); SubscribeLocalEvent(OnShutdown); } private void OnShutdown(EntityUid uid, AmbientSoundComponent component, ComponentShutdown args) { - if (!_playingSounds.Remove((uid, component), out var sound)) + if (!_playingSounds.Remove(component, out var sound)) return; - sound.Stream?.Stop(); + _audio.Stop(sound.Stream); _playingCount[sound.Path] -= 1; if (_playingCount[sound.Path] == 0) _playingCount.Remove(sound.Path); } - private void SetAmbienceVolume(float value) + private void SetAmbienceGain(float value) { - _ambienceVolume = value; + _ambienceVolume = SharedAudioSystem.GainToVolume(value); - foreach (var ((_, comp), values) in _playingSounds) + foreach (var (comp, values) in _playingSounds) { if (values.Stream == null) continue; - var stream = (AudioSystem.PlayingStream) values.Stream; - stream.Volume = _params.Volume + comp.Volume + _ambienceVolume; + var stream = values.Stream; + _audio.SetVolume(stream, _params.Volume + comp.Volume + _ambienceVolume); } } private void SetCooldown(float value) => _cooldown = value; @@ -132,7 +141,7 @@ public override void Shutdown() _cfg.UnsubValueChanged(CCVars.AmbientCooldown, SetCooldown); _cfg.UnsubValueChanged(CCVars.MaxAmbientSources, SetAmbientCount); _cfg.UnsubValueChanged(CCVars.AmbientRange, SetAmbientRange); - _cfg.UnsubValueChanged(CCVars.AmbienceVolume, SetAmbienceVolume); + _cfg.UnsubValueChanged(CCVars.AmbienceVolume, SetAmbienceGain); } private int PlayingCount(string countSound) @@ -177,7 +186,7 @@ private void ClearSounds() { foreach (var (stream, _, _) in _playingSounds.Values) { - stream?.Stop(); + _audio.Stop(stream); } _playingSounds.Clear(); @@ -186,7 +195,7 @@ private void ClearSounds() private readonly struct QueryState { - public readonly Dictionary)>> SourceDict = new(); + public readonly Dictionary> SourceDict = new(); public readonly Vector2 MapPos; public readonly TransformComponent Player; public readonly EntityQuery Query; @@ -224,7 +233,7 @@ private static bool Callback( // Prioritize far away & loud sounds. var importance = range * (ambientComp.Volume + 32); - state.SourceDict.GetOrNew(key).Add((importance, (ambientComp.Owner, ambientComp))); + state.SourceDict.GetOrNew(key).Add((importance, ambientComp)); return true; } @@ -238,10 +247,9 @@ private void ProcessNearbyAmbience(TransformComponent playerXform) var mapPos = playerXform.MapPosition; // Remove out-of-range ambiences - foreach (var (ent, sound) in _playingSounds) + foreach (var (comp, sound) in _playingSounds) { - var entity = ent.Owner; - var comp = ent.Comp; + var entity = comp.Owner; if (comp.Enabled && // Don't keep playing sounds that have changed since. @@ -258,8 +266,8 @@ private void ProcessNearbyAmbience(TransformComponent playerXform) continue; } - sound.Stream?.Stop(); - _playingSounds.Remove((entity, comp)); + _audio.Stop(sound.Stream); + _playingSounds.Remove(comp); _playingCount[sound.Path] -= 1; if (_playingCount[sound.Path] == 0) _playingCount.Remove(sound.Path); @@ -284,12 +292,11 @@ private void ProcessNearbyAmbience(TransformComponent playerXform) sources.Sort(static (a, b) => b.Importance.CompareTo(a.Importance)); - foreach (var (_, ent) in sources) + foreach (var (_, comp) in sources) { - var uid = ent.Owner; - var comp = ent.Comp; + var uid = comp.Owner; - if (_playingSounds.ContainsKey(ent) || + if (_playingSounds.ContainsKey(comp) || metaQuery.GetComponent(uid).EntityPaused) continue; @@ -299,11 +306,8 @@ private void ProcessNearbyAmbience(TransformComponent playerXform) .WithPlayOffset(_random.NextFloat(0.0f, 100.0f)) .WithMaxDistance(comp.Range); - var stream = _audio.PlayPvs(comp.Sound, uid, audioParams); - if (stream == null) - continue; - - _playingSounds[ent] = (stream, comp.Sound, key); + var stream = _audio.PlayEntity(comp.Sound, Filter.Local(), uid, false, audioParams); + _playingSounds[comp] = (stream.Value.Entity, comp.Sound, key); playingCount++; if (_playingSounds.Count >= _maxAmbientCount) diff --git a/Content.Client/Audio/BackgroundAudioSystem.cs b/Content.Client/Audio/BackgroundAudioSystem.cs index 0b31db2463..09ac1efcd6 100644 --- a/Content.Client/Audio/BackgroundAudioSystem.cs +++ b/Content.Client/Audio/BackgroundAudioSystem.cs @@ -5,6 +5,7 @@ using Robust.Client; using Robust.Client.State; using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; using Robust.Shared.Configuration; using Robust.Shared.Player; @@ -13,6 +14,9 @@ namespace Content.Client.Audio; [UsedImplicitly] public sealed class BackgroundAudioSystem : EntitySystem { + /* + * TODO: Nuke this system and merge into contentaudiosystem + */ [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly IBaseClient _client = default!; [Dependency] private readonly IConfigurationManager _configManager = default!; @@ -21,7 +25,7 @@ public sealed class BackgroundAudioSystem : EntitySystem private readonly AudioParams _lobbyParams = new(-5f, 1, "Master", 0, 0, 0, true, 0f); - private IPlayingAudioStream? _lobbyStream; + public EntityUid? LobbyStream; public override void Initialize() { @@ -108,7 +112,7 @@ public void RestartLobbyMusic() public void StartLobbyMusic() { - if (_lobbyStream != null || !_configManager.GetCVar(CCVars.LobbyMusicEnabled)) + if (LobbyStream != null || !_configManager.GetCVar(CCVars.LobbyMusicEnabled)) return; var file = _gameTicker.LobbySong; @@ -117,13 +121,12 @@ public void StartLobbyMusic() return; } - _lobbyStream = _audio.PlayGlobal(file, Filter.Local(), false, - _lobbyParams.WithVolume(_lobbyParams.Volume + _configManager.GetCVar(CCVars.LobbyMusicVolume))); + LobbyStream = _audio.PlayGlobal(file, Filter.Local(), false, + _lobbyParams.WithVolume(_lobbyParams.Volume + SharedAudioSystem.GainToVolume(_configManager.GetCVar(CCVars.LobbyMusicVolume))))?.Entity; } private void EndLobbyMusic() { - _lobbyStream?.Stop(); - _lobbyStream = null; + LobbyStream = _audio.Stop(LobbyStream); } } diff --git a/Content.Client/Audio/ClientGlobalSoundSystem.cs b/Content.Client/Audio/ClientGlobalSoundSystem.cs index 792f149d18..1d98564090 100644 --- a/Content.Client/Audio/ClientGlobalSoundSystem.cs +++ b/Content.Client/Audio/ClientGlobalSoundSystem.cs @@ -2,6 +2,7 @@ using Content.Shared.CCVar; using Content.Shared.GameTicking; using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; using Robust.Shared.Configuration; using Robust.Shared.Player; @@ -14,11 +15,11 @@ public sealed class ClientGlobalSoundSystem : SharedGlobalSoundSystem // Admin music private bool _adminAudioEnabled = true; - private List _adminAudio = new(1); + private List _adminAudio = new(1); // Event sounds (e.g. nuke timer) private bool _eventAudioEnabled = true; - private Dictionary _eventAudio = new(1); + private Dictionary _eventAudio = new(1); public override void Initialize() { @@ -49,13 +50,13 @@ private void ClearAudio() { foreach (var stream in _adminAudio) { - stream?.Stop(); + _audio.Stop(stream); } _adminAudio.Clear(); - foreach (var (_, stream) in _eventAudio) + foreach (var stream in _eventAudio.Values) { - stream?.Stop(); + _audio.Stop(stream); } _eventAudio.Clear(); @@ -66,7 +67,7 @@ private void PlayAdminSound(AdminSoundEvent soundEvent) if(!_adminAudioEnabled) return; var stream = _audio.PlayGlobal(soundEvent.Filename, Filter.Local(), false, soundEvent.AudioParams); - _adminAudio.Add(stream); + _adminAudio.Add(stream.Value.Entity); } private void PlayStationEventMusic(StationEventMusicEvent soundEvent) @@ -75,7 +76,7 @@ private void PlayStationEventMusic(StationEventMusicEvent soundEvent) if(!_eventAudioEnabled || _eventAudio.ContainsKey(soundEvent.Type)) return; var stream = _audio.PlayGlobal(soundEvent.Filename, Filter.Local(), false, soundEvent.AudioParams); - _eventAudio.Add(soundEvent.Type, stream); + _eventAudio.Add(soundEvent.Type, stream.Value.Entity); } private void PlayGameSound(GameGlobalSoundEvent soundEvent) @@ -85,8 +86,10 @@ private void PlayGameSound(GameGlobalSoundEvent soundEvent) private void StopStationEventMusic(StopStationEventMusic soundEvent) { - if (!_eventAudio.TryGetValue(soundEvent.Type, out var stream)) return; - stream?.Stop(); + if (!_eventAudio.TryGetValue(soundEvent.Type, out var stream)) + return; + + _audio.Stop(stream); _eventAudio.Remove(soundEvent.Type); } @@ -96,7 +99,7 @@ private void ToggleAdminSound(bool enabled) if (_adminAudioEnabled) return; foreach (var stream in _adminAudio) { - stream?.Stop(); + _audio.Stop(stream); } _adminAudio.Clear(); } @@ -107,7 +110,7 @@ private void ToggleStationEventMusic(bool enabled) if (_eventAudioEnabled) return; foreach (var stream in _eventAudio) { - stream.Value?.Stop(); + _audio.Stop(stream.Value); } _eventAudio.Clear(); } diff --git a/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs b/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs index 15fc53222e..aafd0ea630 100644 --- a/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs +++ b/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs @@ -9,6 +9,8 @@ using Robust.Client.ResourceManagement; using Robust.Client.State; using Robust.Shared.Audio; +using Robust.Shared.Audio.Components; +using Robust.Shared.Audio.Systems; using Robust.Shared.Configuration; using Robust.Shared.Player; using Robust.Shared.Prototypes; @@ -39,7 +41,7 @@ public sealed partial class ContentAudioSystem // Don't need to worry about this being serializable or pauseable as it doesn't affect the sim. private TimeSpan _nextAudio; - private AudioSystem.PlayingStream? _ambientMusicStream; + private EntityUid? _ambientMusicStream; private AmbientMusicPrototype? _musicProto; /// @@ -58,12 +60,6 @@ public sealed partial class ContentAudioSystem private void InitializeAmbientMusic() { - // TODO: Shitty preload - foreach (var audio in _proto.Index("AmbienceSpace").PickFiles) - { - _resource.GetResource(audio.ToString()); - } - _configManager.OnValueChanged(CCVars.AmbientMusicVolume, AmbienceCVarChanged, true); _sawmill = IoCManager.Resolve().GetSawmill("audio.ambience"); @@ -71,7 +67,7 @@ private void InitializeAmbientMusic() _nextAudio = TimeSpan.MaxValue; SetupAmbientSounds(); - _proto.PrototypesReloaded += OnProtoReload; + SubscribeLocalEvent(OnProtoReload); _state.OnStateChanged += OnStateChange; // On round end summary OR lobby cut audio. SubscribeNetworkEvent(OnRoundEndMessage); @@ -79,32 +75,25 @@ private void InitializeAmbientMusic() private void AmbienceCVarChanged(float obj) { - _volumeSlider = obj; + _volumeSlider = SharedAudioSystem.GainToVolume(obj); if (_ambientMusicStream != null && _musicProto != null) { - _ambientMusicStream.Volume = _musicProto.Sound.Params.Volume + _volumeSlider; + _audio.SetVolume(_ambientMusicStream, _musicProto.Sound.Params.Volume + _volumeSlider); } } private void ShutdownAmbientMusic() { _configManager.UnsubValueChanged(CCVars.AmbientMusicVolume, AmbienceCVarChanged); - _proto.PrototypesReloaded -= OnProtoReload; _state.OnStateChanged -= OnStateChange; - _ambientMusicStream?.Stop(); + _ambientMusicStream = _audio.Stop(_ambientMusicStream); } private void OnProtoReload(PrototypesReloadedEventArgs obj) { - if (!obj.ByType.ContainsKey(typeof(AmbientMusicPrototype)) && - !obj.ByType.ContainsKey(typeof(RulesPrototype))) - { - return; - } - - _ambientSounds.Clear(); - SetupAmbientSounds(); + if (obj.WasModified() || obj.WasModified()) + SetupAmbientSounds(); } private void OnStateChange(StateChangedEventArgs obj) @@ -118,6 +107,7 @@ private void OnStateChange(StateChangedEventArgs obj) private void SetupAmbientSounds() { + _ambientSounds.Clear(); foreach (var ambience in _proto.EnumeratePrototypes()) { var tracks = _ambientSounds.GetOrNew(ambience.ID); @@ -129,8 +119,7 @@ private void SetupAmbientSounds() private void OnRoundEndMessage(RoundEndMessageEvent ev) { // If scoreboard shows then just stop the music - _ambientMusicStream?.Stop(); - _ambientMusicStream = null; + _ambientMusicStream = _audio.Stop(_ambientMusicStream); _nextAudio = TimeSpan.FromMinutes(3); } @@ -164,21 +153,26 @@ private void UpdateAmbientMusic() // Update still runs in lobby so just ignore it. if (_state.CurrentState is not GameplayState) { - FadeOut(_ambientMusicStream); + Audio.Stop(_ambientMusicStream); _ambientMusicStream = null; _musicProto = null; return; } - var isDone = _ambientMusicStream?.Done; + bool? isDone = null; + + if (TryComp(_ambientMusicStream, out AudioComponent? audioComp)) + { + isDone = !audioComp.Playing; + } if (_interruptable) { - var player = _player.LocalPlayer?.ControlledEntity; + var player = _player.LocalSession?.AttachedEntity; if (player == null || _musicProto == null || !_rules.IsTrue(player.Value, _proto.Index(_musicProto.Rules))) { - FadeOut(_ambientMusicStream, AmbientMusicFadeTime); + FadeOut(_ambientMusicStream, duration: AmbientMusicFadeTime); _musicProto = null; _interruptable = false; isDone = true; @@ -221,14 +215,11 @@ private void UpdateAmbientMusic() false, AudioParams.Default.WithVolume(_musicProto.Sound.Params.Volume + _volumeSlider)); - if (strim != null) - { - _ambientMusicStream = (AudioSystem.PlayingStream) strim; + _ambientMusicStream = strim.Value.Entity; - if (_musicProto.FadeIn) - { - FadeIn(_ambientMusicStream, AmbientMusicFadeTime); - } + if (_musicProto.FadeIn) + { + FadeIn(_ambientMusicStream, strim.Value.Component, AmbientMusicFadeTime); } // Refresh the list diff --git a/Content.Client/Audio/ContentAudioSystem.cs b/Content.Client/Audio/ContentAudioSystem.cs index 696a5eb32d..603b1086d8 100644 --- a/Content.Client/Audio/ContentAudioSystem.cs +++ b/Content.Client/Audio/ContentAudioSystem.cs @@ -1,26 +1,61 @@ using Content.Shared.Audio; +using Content.Shared.CCVar; +using Content.Shared.GameTicking; using Robust.Client.GameObjects; +using Robust.Shared; +using Robust.Shared.Audio; +using AudioComponent = Robust.Shared.Audio.Components.AudioComponent; namespace Content.Client.Audio; public sealed partial class ContentAudioSystem : SharedContentAudioSystem { // Need how much volume to change per tick and just remove it when it drops below "0" - private readonly Dictionary _fadingOut = new(); + private readonly Dictionary _fadingOut = new(); // Need volume change per tick + target volume. - private readonly Dictionary _fadingIn = new(); + private readonly Dictionary _fadingIn = new(); - private readonly List _fadeToRemove = new(); + private readonly List _fadeToRemove = new(); private const float MinVolume = -32f; private const float DefaultDuration = 2f; + /* + * Gain multipliers for specific audio sliders. + * The float value will get multiplied by this when setting + * i.e. a gain of 0.5f x 3 will equal 1.5f which is supported in OpenAL. + */ + + public const float MasterVolumeMultiplier = 3f; + public const float MidiVolumeMultiplier = 0.25f; + public const float AmbienceMultiplier = 3f; + public const float AmbientMusicMultiplier = 3f; + public const float LobbyMultiplier = 3f; + public override void Initialize() { base.Initialize(); UpdatesOutsidePrediction = true; InitializeAmbientMusic(); + SubscribeNetworkEvent(OnRoundCleanup); + } + + private void OnRoundCleanup(RoundRestartCleanupEvent ev) + { + _fadingOut.Clear(); + + // Preserve lobby music but everything else should get dumped. + var lobbyStream = EntityManager.System().LobbyStream; + TryComp(lobbyStream, out AudioComponent? audioComp); + var oldGain = audioComp?.Gain; + + SilenceAudio(); + + if (oldGain != null) + { + Audio.SetGain(lobbyStream, oldGain.Value, audioComp); + } } public override void Shutdown() @@ -42,28 +77,28 @@ public override void Update(float frameTime) #region Fades - public void FadeOut(AudioSystem.PlayingStream? stream, float duration = DefaultDuration) + public void FadeOut(EntityUid? stream, AudioComponent? component = null, float duration = DefaultDuration) { - if (stream == null || duration <= 0f) + if (stream == null || duration <= 0f || !Resolve(stream.Value, ref component)) return; // Just in case // TODO: Maybe handle the removals by making it seamless? - _fadingIn.Remove(stream); - var diff = stream.Volume - MinVolume; - _fadingOut.Add(stream, diff / duration); + _fadingIn.Remove(stream.Value); + var diff = component.Volume - MinVolume; + _fadingOut.Add(stream.Value, diff / duration); } - public void FadeIn(AudioSystem.PlayingStream? stream, float duration = DefaultDuration) + public void FadeIn(EntityUid? stream, AudioComponent? component = null, float duration = DefaultDuration) { - if (stream == null || duration <= 0f || stream.Volume < MinVolume) + if (stream == null || duration <= 0f || !Resolve(stream.Value, ref component) || component.Volume < MinVolume) return; - _fadingOut.Remove(stream); - var curVolume = stream.Volume; + _fadingOut.Remove(stream.Value); + var curVolume = component.Volume; var change = (curVolume - MinVolume) / duration; - _fadingIn.Add(stream, (change, stream.Volume)); - stream.Volume = MinVolume; + _fadingIn.Add(stream.Value, (change, component.Volume)); + component.Volume = MinVolume; } private void UpdateFades(float frameTime) @@ -72,19 +107,18 @@ private void UpdateFades(float frameTime) foreach (var (stream, change) in _fadingOut) { - // Cancelled elsewhere - if (stream.Done) + if (!TryComp(stream, out AudioComponent? component)) { _fadeToRemove.Add(stream); continue; } - var volume = stream.Volume - change * frameTime; - stream.Volume = MathF.Max(MinVolume, volume); + var volume = component.Volume - change * frameTime; + component.Volume = MathF.Max(MinVolume, volume); - if (stream.Volume.Equals(MinVolume)) + if (component.Volume.Equals(MinVolume)) { - stream.Stop(); + _audio.Stop(stream); _fadeToRemove.Add(stream); } } @@ -99,16 +133,16 @@ private void UpdateFades(float frameTime) foreach (var (stream, (change, target)) in _fadingIn) { // Cancelled elsewhere - if (stream.Done) + if (!TryComp(stream, out AudioComponent? component)) { _fadeToRemove.Add(stream); continue; } - var volume = stream.Volume + change * frameTime; - stream.Volume = MathF.Min(target, volume); + var volume = component.Volume + change * frameTime; + component.Volume = MathF.Min(target, volume); - if (stream.Volume.Equals(target)) + if (component.Volume.Equals(target)) { _fadeToRemove.Add(stream); } diff --git a/Content.Client/Changelog/ChangelogTab.xaml.cs b/Content.Client/Changelog/ChangelogTab.xaml.cs index bb677db40e..b8f98c0d40 100644 --- a/Content.Client/Changelog/ChangelogTab.xaml.cs +++ b/Content.Client/Changelog/ChangelogTab.xaml.cs @@ -7,6 +7,7 @@ using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; +using Robust.Shared.ContentPack; using Robust.Shared.Utility; using static Content.Client.Changelog.ChangelogManager; using static Robust.Client.UserInterface.Controls.BoxContainer; diff --git a/Content.Client/CharacterInfo/CharacterInfoSystem.cs b/Content.Client/CharacterInfo/CharacterInfoSystem.cs index 93bd86d140..844a352a18 100644 --- a/Content.Client/CharacterInfo/CharacterInfoSystem.cs +++ b/Content.Client/CharacterInfo/CharacterInfoSystem.cs @@ -1,6 +1,5 @@ using Content.Shared.CharacterInfo; using Content.Shared.Objectives; -using Robust.Client.GameObjects; using Robust.Client.Player; using Robust.Client.UserInterface; @@ -11,14 +10,11 @@ public sealed class CharacterInfoSystem : EntitySystem [Dependency] private readonly IPlayerManager _players = default!; public event Action? OnCharacterUpdate; - public event Action? OnCharacterDetached; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnPlayerAttached); - SubscribeNetworkEvent(OnCharacterInfoEvent); } @@ -33,14 +29,6 @@ public void RequestCharacterInfo() RaiseNetworkEvent(new RequestCharacterInfoEvent(GetNetEntity(entity.Value))); } - private void OnPlayerAttached(PlayerAttachSysMessage msg) - { - if (msg.AttachedEntity == default) - { - OnCharacterDetached?.Invoke(); - } - } - private void OnCharacterInfoEvent(CharacterInfoEvent msg, EntitySessionEventArgs args) { var entity = GetEntity(msg.NetEntity); diff --git a/Content.Client/Chat/UI/SpeechBubble.cs b/Content.Client/Chat/UI/SpeechBubble.cs index b6b336f1dc..91e8e5a90f 100644 --- a/Content.Client/Chat/UI/SpeechBubble.cs +++ b/Content.Client/Chat/UI/SpeechBubble.cs @@ -1,19 +1,28 @@ using System.Numerics; using Content.Client.Chat.Managers; +using Content.Shared.CCVar; +using Content.Shared.Chat; using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; +using Robust.Shared.Configuration; using Robust.Shared.Timing; +using Robust.Shared.Utility; namespace Content.Client.Chat.UI { public abstract class SpeechBubble : Control { + [Dependency] private readonly IEyeManager _eyeManager = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] protected readonly IConfigurationManager ConfigManager = default!; + public enum SpeechType : byte { Emote, Say, - Whisper + Whisper, + Looc } /// @@ -32,10 +41,12 @@ public enum SpeechType : byte /// private const float EntityVerticalOffset = 0.5f; - private readonly IEyeManager _eyeManager; + /// + /// The default maximum width for speech bubbles. + /// + public const float SpeechMaxWidth = 256; + private readonly EntityUid _senderEntity; - private readonly IChatManager _chatManager; - private readonly IEntityManager _entityManager; private float _timeLeft = TotalTime; @@ -47,35 +58,36 @@ public enum SpeechType : byte // man down public event Action? OnDied; - public static SpeechBubble CreateSpeechBubble(SpeechType type, string text, EntityUid senderEntity, IEyeManager eyeManager, IChatManager chatManager, IEntityManager entityManager) + public static SpeechBubble CreateSpeechBubble(SpeechType type, ChatMessage message, EntityUid senderEntity) { switch (type) { case SpeechType.Emote: - return new TextSpeechBubble(text, senderEntity, eyeManager, chatManager, entityManager, "emoteBox"); + return new TextSpeechBubble(message, senderEntity, "emoteBox"); case SpeechType.Say: - return new TextSpeechBubble(text, senderEntity, eyeManager, chatManager, entityManager, "sayBox"); + return new FancyTextSpeechBubble(message, senderEntity, "sayBox"); case SpeechType.Whisper: - return new TextSpeechBubble(text, senderEntity, eyeManager, chatManager, entityManager, "whisperBox"); + return new FancyTextSpeechBubble(message, senderEntity, "whisperBox"); + + case SpeechType.Looc: + return new TextSpeechBubble(message, senderEntity, "emoteBox", Color.FromHex("#48d1cc")); default: throw new ArgumentOutOfRangeException(); } } - public SpeechBubble(string text, EntityUid senderEntity, IEyeManager eyeManager, IChatManager chatManager, IEntityManager entityManager, string speechStyleClass) + public SpeechBubble(ChatMessage message, EntityUid senderEntity, string speechStyleClass, Color? fontColor = null) { - _chatManager = chatManager; + IoCManager.InjectDependencies(this); _senderEntity = senderEntity; - _eyeManager = eyeManager; - _entityManager = entityManager; // Use text clipping so new messages don't overlap old ones being pushed up. RectClipContent = true; - var bubble = BuildBubble(text, speechStyleClass); + var bubble = BuildBubble(message, speechStyleClass, fontColor); AddChild(bubble); @@ -86,7 +98,7 @@ public SpeechBubble(string text, EntityUid senderEntity, IEyeManager eyeManager, _verticalOffsetAchieved = -ContentSize.Y; } - protected abstract Control BuildBubble(string text, string speechStyleClass); + protected abstract Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null); protected override void FrameUpdate(FrameEventArgs args) { @@ -160,22 +172,49 @@ public void FadeNow() _timeLeft = FadeTime; } } + + protected FormattedMessage FormatSpeech(string message, Color? fontColor = null) + { + var msg = new FormattedMessage(); + if (fontColor != null) + msg.PushColor(fontColor.Value); + msg.AddMarkup(message); + return msg; + } + + protected string ExtractSpeechSubstring(ChatMessage message, string tag) + { + var rawmsg = message.WrappedMessage; + var tagStart = rawmsg.IndexOf($"[{tag}]"); + var tagEnd = rawmsg.IndexOf($"[/{tag}]"); + if (tagStart < 0 || tagEnd < 0) //the above return -1 if the tag's not found, which in turn will cause the below to throw an exception. a blank speech bubble is far more noticeably broken than the bubble not appearing at all -bhijn + return ""; + tagStart += tag.Length + 2; + return rawmsg.Substring(tagStart, tagEnd - tagStart); + } + + protected FormattedMessage ExtractAndFormatSpeechSubstring(ChatMessage message, string tag, Color? fontColor = null) + { + return FormatSpeech(ExtractSpeechSubstring(message, tag), fontColor); + } + } public sealed class TextSpeechBubble : SpeechBubble { - public TextSpeechBubble(string text, EntityUid senderEntity, IEyeManager eyeManager, IChatManager chatManager, IEntityManager entityManager, string speechStyleClass) - : base(text, senderEntity, eyeManager, chatManager, entityManager, speechStyleClass) + public TextSpeechBubble(ChatMessage message, EntityUid senderEntity, string speechStyleClass, Color? fontColor = null) + : base(message, senderEntity, speechStyleClass, fontColor) { } - protected override Control BuildBubble(string text, string speechStyleClass) + protected override Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null) { var label = new RichTextLabel { - MaxWidth = 256, + MaxWidth = SpeechMaxWidth, }; - label.SetMessage(text); + + label.SetMessage(FormatSpeech(message.WrappedMessage, fontColor)); var panel = new PanelContainer { @@ -187,4 +226,76 @@ protected override Control BuildBubble(string text, string speechStyleClass) return panel; } } + + public sealed class FancyTextSpeechBubble : SpeechBubble + { + + public FancyTextSpeechBubble(ChatMessage message, EntityUid senderEntity, string speechStyleClass, Color? fontColor = null) + : base(message, senderEntity, speechStyleClass, fontColor) + { + } + + protected override Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null) + { + if (!ConfigManager.GetCVar(CCVars.ChatEnableFancyBubbles)) + { + var label = new RichTextLabel + { + MaxWidth = SpeechMaxWidth + }; + + label.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleContent", fontColor)); + + var unfanciedPanel = new PanelContainer + { + StyleClasses = { "speechBox", speechStyleClass }, + Children = { label }, + ModulateSelfOverride = Color.White.WithAlpha(0.75f) + }; + return unfanciedPanel; + } + + var bubbleHeader = new RichTextLabel + { + Margin = new Thickness(1, 1, 1, 1) + }; + + var bubbleContent = new RichTextLabel + { + MaxWidth = SpeechMaxWidth, + Margin = new Thickness(2, 6, 2, 2) + }; + + //We'll be honest. *Yes* this is hacky. Doing this in a cleaner way would require a bottom-up refactor of how saycode handles sending chat messages. -Myr + bubbleHeader.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleHeader", fontColor)); + bubbleContent.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleContent", fontColor)); + + //As for below: Some day this could probably be converted to xaml. But that is not today. -Myr + var mainPanel = new PanelContainer + { + StyleClasses = { "speechBox", speechStyleClass }, + Children = { bubbleContent }, + ModulateSelfOverride = Color.White.WithAlpha(0.75f), + HorizontalAlignment = HAlignment.Center, + VerticalAlignment = VAlignment.Bottom, + Margin = new Thickness(4, 14, 4, 2) + }; + + var headerPanel = new PanelContainer + { + StyleClasses = { "speechBox", speechStyleClass }, + Children = { bubbleHeader }, + ModulateSelfOverride = Color.White.WithAlpha(ConfigManager.GetCVar(CCVars.ChatFancyNameBackground) ? 0.75f : 0f), + HorizontalAlignment = HAlignment.Center, + VerticalAlignment = VAlignment.Top + }; + + var panel = new PanelContainer + { + Children = { mainPanel, headerPanel } + }; + + return panel; + } + } } diff --git a/Content.Client/Chemistry/EntitySystems/SolutionContainerMixerSystem.cs b/Content.Client/Chemistry/EntitySystems/SolutionContainerMixerSystem.cs new file mode 100644 index 0000000000..b6401c113d --- /dev/null +++ b/Content.Client/Chemistry/EntitySystems/SolutionContainerMixerSystem.cs @@ -0,0 +1,9 @@ +using Content.Shared.Chemistry.EntitySystems; + +namespace Content.Client.Chemistry.EntitySystems; + +/// +public sealed class SolutionContainerMixerSystem : SharedSolutionContainerMixerSystem +{ + +} diff --git a/Content.Client/Chemistry/Visualizers/FoamVisualizerSystem.cs b/Content.Client/Chemistry/Visualizers/FoamVisualizerSystem.cs index ec582ee094..2ee88956ff 100644 --- a/Content.Client/Chemistry/Visualizers/FoamVisualizerSystem.cs +++ b/Content.Client/Chemistry/Visualizers/FoamVisualizerSystem.cs @@ -1,8 +1,6 @@ -using Content.Shared.Smoking; -using Robust.Shared.Spawners; +using Content.Shared.Chemistry.Components; using Robust.Client.Animations; using Robust.Client.GameObjects; -using Robust.Shared.Network; using Robust.Shared.Timing; namespace Content.Client.Chemistry.Visualizers; @@ -18,6 +16,7 @@ public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnAnimationComplete); } public override void Update(float frameTime) @@ -27,11 +26,11 @@ public override void Update(float frameTime) if (!_timing.IsFirstTimePredicted) return; - var query = EntityQueryEnumerator(); + var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var comp, out var despawn)) + while (query.MoveNext(out var uid, out var comp, out var smoke)) { - if (despawn.Lifetime > 1f) + if (_timing.CurTime < comp.StartTime + TimeSpan.FromSeconds(smoke.Duration) - TimeSpan.FromSeconds(comp.AnimationTime)) continue; // Despawn animation. @@ -48,6 +47,7 @@ public override void Update(float frameTime) /// private void OnComponentInit(EntityUid uid, FoamVisualsComponent comp, ComponentInit args) { + comp.StartTime = _timing.CurTime; comp.Animation = new Animation { Length = TimeSpan.FromSeconds(comp.AnimationTime), @@ -58,12 +58,21 @@ private void OnComponentInit(EntityUid uid, FoamVisualsComponent comp, Component LayerKey = FoamVisualLayers.Base, KeyFrames = { - new AnimationTrackSpriteFlick.KeyFrame(comp.State, 0f) + new AnimationTrackSpriteFlick.KeyFrame(comp.AnimationState, 0f) } } } }; } + + private void OnAnimationComplete(EntityUid uid, FoamVisualsComponent component, AnimationCompletedEvent args) + { + if (args.Key != FoamVisualsComponent.AnimationKey) + return; + + if (TryComp(uid, out var sprite)) + sprite.Visible = false; + } } public enum FoamVisualLayers : byte diff --git a/Content.Client/Chemistry/Visualizers/FoamVisualsComponent.cs b/Content.Client/Chemistry/Visualizers/FoamVisualsComponent.cs index b09c74aa0f..8199efa42e 100644 --- a/Content.Client/Chemistry/Visualizers/FoamVisualsComponent.cs +++ b/Content.Client/Chemistry/Visualizers/FoamVisualsComponent.cs @@ -1,5 +1,6 @@ using Robust.Client.Animations; using Robust.Client.Graphics; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Client.Chemistry.Visualizers; @@ -15,18 +16,21 @@ public sealed partial class FoamVisualsComponent : Component /// public const string AnimationKey = "foamdissolve_animation"; + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan StartTime; + /// /// How long the foam visually dissolves for. /// - [DataField("animationTime")] - public float AnimationTime = 0.6f; + [DataField] + public float AnimationTime = 0.5f; /// /// The state of the entities base sprite RSI that is displayed when the foam dissolves. /// Cannot use because it does not have and I am not making an engine PR at this time. /// - [DataField("animationState")] - public string State = "foam-dissolve"; + [DataField] + public string AnimationState = "foam-dissolve"; /// /// The animation used while the foam dissolves. diff --git a/Content.Client/Clothing/ClientClothingSystem.cs b/Content.Client/Clothing/ClientClothingSystem.cs index b16e14d653..979f7430e1 100644 --- a/Content.Client/Clothing/ClientClothingSystem.cs +++ b/Content.Client/Clothing/ClientClothingSystem.cs @@ -202,17 +202,15 @@ private void OnDidUnequip(EntityUid uid, SpriteComponent component, DidUnequipEv revealedLayers.Clear(); } - public void InitClothing(EntityUid uid, InventoryComponent? component = null, SpriteComponent? sprite = null) + public void InitClothing(EntityUid uid, InventoryComponent component) { - if (!Resolve(uid, ref sprite, ref component) || !_inventorySystem.TryGetSlots(uid, out var slots, component)) + if (!TryComp(uid, out SpriteComponent? sprite)) return; - foreach (var slot in slots) + var enumerator = _inventorySystem.GetSlotEnumerator((uid, component)); + while (enumerator.NextItem(out var item, out var slot)) { - if (!_inventorySystem.TryGetSlotContainer(uid, slot.Name, out var containerSlot, out _, component) || - !containerSlot.ContainedEntity.HasValue) continue; - - RenderEquipment(uid, containerSlot.ContainedEntity.Value, slot.Name, component, sprite); + RenderEquipment(uid, item, slot.Name, component, sprite); } } diff --git a/Content.Client/Clothing/Systems/ChameleonClothingSystem.cs b/Content.Client/Clothing/Systems/ChameleonClothingSystem.cs index f067d91051..0ea9bbac09 100644 --- a/Content.Client/Clothing/Systems/ChameleonClothingSystem.cs +++ b/Content.Client/Clothing/Systems/ChameleonClothingSystem.cs @@ -29,18 +29,13 @@ public override void Initialize() SubscribeLocalEvent(HandleState); PrepareAllVariants(); - _proto.PrototypesReloaded += OnProtoReloaded; + SubscribeLocalEvent(OnProtoReloaded); } - public override void Shutdown() + private void OnProtoReloaded(PrototypesReloadedEventArgs args) { - base.Shutdown(); - _proto.PrototypesReloaded -= OnProtoReloaded; - } - - private void OnProtoReloaded(PrototypesReloadedEventArgs _) - { - PrepareAllVariants(); + if (args.WasModified()) + PrepareAllVariants(); } private void HandleState(EntityUid uid, ChameleonClothingComponent component, ref AfterAutoHandleStateEvent args) diff --git a/Content.Client/Construction/ConstructionSystem.cs b/Content.Client/Construction/ConstructionSystem.cs index 98d2dfd414..4035c68cc7 100644 --- a/Content.Client/Construction/ConstructionSystem.cs +++ b/Content.Client/Construction/ConstructionSystem.cs @@ -2,6 +2,7 @@ using Content.Client.Popups; using Content.Shared.Construction; using Content.Shared.Construction.Prototypes; +using Content.Shared.Construction.Steps; using Content.Shared.Examine; using Content.Shared.Input; using Content.Shared.Interaction; @@ -12,6 +13,7 @@ using Robust.Shared.Input; using Robust.Shared.Input.Binding; using Robust.Shared.Map; +using Robust.Shared.Player; using Robust.Shared.Prototypes; namespace Content.Client.Construction @@ -38,7 +40,7 @@ public override void Initialize() base.Initialize(); UpdatesOutsidePrediction = true; - SubscribeLocalEvent(HandlePlayerAttached); + SubscribeLocalEvent(HandlePlayerAttached); SubscribeNetworkEvent(HandleAckStructure); SubscribeNetworkEvent(OnConstructionGuideReceived); @@ -96,7 +98,11 @@ private void HandleConstructionGhostExamined(EntityUid uid, ConstructionGhostCom return; } - edge.Steps[0].DoExamine(args); + foreach (ConstructionGraphStep step in edge.Steps) + { + args.Message.PushNewline(); + step.DoExamine(args); + } } public event EventHandler? CraftingAvailabilityChanged; @@ -110,9 +116,9 @@ private void HandleAckStructure(AckStructureConstructionMessage msg) ClearGhost(msg.GhostId); } - private void HandlePlayerAttached(PlayerAttachSysMessage msg) + private void HandlePlayerAttached(LocalPlayerAttachedEvent msg) { - var available = IsCraftingAvailable(msg.AttachedEntity); + var available = IsCraftingAvailable(msg.Entity); UpdateCraftingAvailability(available); } @@ -197,7 +203,7 @@ public bool TrySpawnGhost( var comp = EntityManager.GetComponent(ghost.Value); comp.Prototype = prototype; EntityManager.GetComponent(ghost.Value).LocalRotation = dir.ToAngle(); - _ghosts.Add(ghost.Value.Id, ghost.Value); + _ghosts.Add(ghost.GetHashCode(), ghost.Value); var sprite = EntityManager.GetComponent(ghost.Value); sprite.Color = new Color(48, 255, 48, 128); @@ -264,7 +270,7 @@ public void TryStartConstruction(EntityUid ghostId, ConstructionGhostComponent? } var transform = EntityManager.GetComponent(ghostId); - var msg = new TryStartStructureConstructionMessage(GetNetCoordinates(transform.Coordinates), ghostComp.Prototype.ID, transform.LocalRotation, ghostId.Id); + var msg = new TryStartStructureConstructionMessage(GetNetCoordinates(transform.Coordinates), ghostComp.Prototype.ID, transform.LocalRotation, ghostId.GetHashCode()); RaiseNetworkEvent(msg); } diff --git a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs index cdc9044a40..28cf3ba16c 100644 --- a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs +++ b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs @@ -1,9 +1,11 @@ using System.Linq; using Content.Client.UserInterface.Systems.MenuBar.Widgets; using Content.Shared.Construction.Prototypes; +using Content.Shared.Tag; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.Placement; +using Robust.Client.Player; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.Utility; @@ -25,6 +27,7 @@ internal sealed class ConstructionMenuPresenter : IDisposable [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPlacementManager _placementManager = default!; [Dependency] private readonly IUserInterfaceManager _uiManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; private readonly IConstructionMenuView _constructionView; @@ -152,6 +155,11 @@ private void OnViewPopulateRecipes(object? sender, (string search, string catago if (recipe.Hide) continue; + if (_playerManager.LocalSession == null + || _playerManager.LocalEntity == null + || (recipe.EntityWhitelist != null && !recipe.EntityWhitelist.IsValid(_playerManager.LocalEntity.Value))) + continue; + if (!string.IsNullOrEmpty(search)) { if (!recipe.Name.ToLowerInvariant().Contains(search.Trim().ToLowerInvariant())) diff --git a/Content.Client/Credits/CreditsWindow.xaml.cs b/Content.Client/Credits/CreditsWindow.xaml.cs index 666ff2aa48..60ac579845 100644 --- a/Content.Client/Credits/CreditsWindow.xaml.cs +++ b/Content.Client/Credits/CreditsWindow.xaml.cs @@ -11,6 +11,7 @@ using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Configuration; +using Robust.Shared.ContentPack; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Maths; @@ -23,7 +24,7 @@ namespace Content.Client.Credits [GenerateTypedNameReferences] public sealed partial class CreditsWindow : DefaultWindow { - [Dependency] private readonly IResourceCache _resourceManager = default!; + [Dependency] private readonly IResourceManager _resourceManager = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; private static readonly Dictionary PatronTierPriority = new() @@ -49,7 +50,7 @@ public CreditsWindow() private void PopulateLicenses(BoxContainer licensesContainer) { - foreach (var entry in CreditsManager.GetLicenses().OrderBy(p => p.Name)) + foreach (var entry in CreditsManager.GetLicenses(_resourceManager).OrderBy(p => p.Name)) { licensesContainer.AddChild(new Label {StyleClasses = {StyleBase.StyleClassLabelHeading}, Text = entry.Name}); diff --git a/Content.Client/CrewManifest/CrewManifestSystem.cs b/Content.Client/CrewManifest/CrewManifestSystem.cs index 05f27620c4..d05acb5680 100644 --- a/Content.Client/CrewManifest/CrewManifestSystem.cs +++ b/Content.Client/CrewManifest/CrewManifestSystem.cs @@ -1,4 +1,3 @@ -using Content.Client.GameTicking.Managers; using Content.Shared.CrewManifest; using Content.Shared.Roles; using Robust.Shared.Prototypes; @@ -19,12 +18,7 @@ public override void Initialize() base.Initialize(); BuildDepartmentLookup(); - _prototypeManager.PrototypesReloaded += OnPrototypesReload; - } - - public override void Shutdown() - { - _prototypeManager.PrototypesReloaded -= OnPrototypesReload; + SubscribeLocalEvent(OnPrototypesReload); } /// @@ -36,16 +30,16 @@ public void RequestCrewManifest(NetEntity netEntity) RaiseNetworkEvent(new RequestCrewManifestMessage(netEntity)); } - private void OnPrototypesReload(PrototypesReloadedEventArgs _) + private void OnPrototypesReload(PrototypesReloadedEventArgs args) { - _jobDepartmentLookup.Clear(); - _departments.Clear(); - - BuildDepartmentLookup(); + if (args.WasModified()) + BuildDepartmentLookup(); } private void BuildDepartmentLookup() { + _jobDepartmentLookup.Clear(); + _departments.Clear(); foreach (var department in _prototypeManager.EnumeratePrototypes()) { _departments.Add(department.ID); diff --git a/Content.Client/Decals/DecalSystem.cs b/Content.Client/Decals/DecalSystem.cs index 66b30545da..be442ab8a0 100644 --- a/Content.Client/Decals/DecalSystem.cs +++ b/Content.Client/Decals/DecalSystem.cs @@ -15,6 +15,9 @@ public sealed class DecalSystem : SharedDecalSystem private DecalOverlay _overlay = default!; + private HashSet _removedUids = new(); + private readonly List _removedChunks = new(); + public override void Initialize() { base.Initialize(); @@ -65,13 +68,14 @@ private void OnHandleState(EntityUid gridUid, DecalGridComponent gridComp, ref C return; // is this a delta or full state? - var removedChunks = new List(); + _removedChunks.Clear(); + if (!state.FullState) { foreach (var key in gridComp.ChunkCollection.ChunkCollection.Keys) { if (!state.AllChunks!.Contains(key)) - removedChunks.Add(key); + _removedChunks.Add(key); } } else @@ -79,12 +83,12 @@ private void OnHandleState(EntityUid gridUid, DecalGridComponent gridComp, ref C foreach (var key in gridComp.ChunkCollection.ChunkCollection.Keys) { if (!state.Chunks.ContainsKey(key)) - removedChunks.Add(key); + _removedChunks.Add(key); } } - if (removedChunks.Count > 0) - RemoveChunks(gridUid, gridComp, removedChunks); + if (_removedChunks.Count > 0) + RemoveChunks(gridUid, gridComp, _removedChunks); if (state.Chunks.Count > 0) UpdateChunks(gridUid, gridComp, state.Chunks); @@ -137,9 +141,10 @@ private void UpdateChunks(EntityUid gridId, DecalGridComponent gridComp, Diction { if (chunkCollection.TryGetValue(indices, out var chunk)) { - var removedUids = new HashSet(chunk.Decals.Keys); - removedUids.ExceptWith(newChunkData.Decals.Keys); - foreach (var removedUid in removedUids) + _removedUids.Clear(); + _removedUids.UnionWith(chunk.Decals.Keys); + _removedUids.ExceptWith(newChunkData.Decals.Keys); + foreach (var removedUid in _removedUids) { OnDecalRemoved(gridId, removedUid, gridComp, indices, chunk); gridComp.DecalIndex.Remove(removedUid); @@ -166,7 +171,8 @@ private void RemoveChunks(EntityUid gridId, DecalGridComponent gridComp, IEnumer foreach (var index in chunks) { - if (!chunkCollection.TryGetValue(index, out var chunk)) continue; + if (!chunkCollection.TryGetValue(index, out var chunk)) + continue; foreach (var decalId in chunk.Decals.Keys) { diff --git a/Content.Client/Decals/Overlays/DecalOverlay.cs b/Content.Client/Decals/Overlays/DecalOverlay.cs index f2c11e2a68..6fcd48264b 100644 --- a/Content.Client/Decals/Overlays/DecalOverlay.cs +++ b/Content.Client/Decals/Overlays/DecalOverlay.cs @@ -30,21 +30,22 @@ protected override void Draw(in OverlayDrawArgs args) { // Shouldn't need to clear cached textures unless the prototypes get reloaded. var handle = args.WorldHandle; - var xformQuery = _entManager.GetEntityQuery(); var xformSystem = _entManager.System(); var eyeAngle = args.Viewport.Eye?.Rotation ?? Angle.Zero; - foreach (var (decalGrid, xform) in _entManager.EntityQuery(true)) + var gridQuery = _entManager.AllEntityQueryEnumerator(); + + while (gridQuery.MoveNext(out var decalGrid, out var xform)) { + if (xform.MapID != args.MapId) + continue; + var zIndexDictionary = decalGrid.DecalRenderIndex; if (zIndexDictionary.Count == 0) continue; - if (xform.MapID != args.MapId) - continue; - - var (_, worldRot, worldMatrix) = xformSystem.GetWorldPositionRotationMatrix(xform, xformQuery); + var (_, worldRot, worldMatrix) = xformSystem.GetWorldPositionRotationMatrix(xform); handle.SetTransform(worldMatrix); diff --git a/Content.Client/DeltaV/Biscuit/BiscuitSystem.cs b/Content.Client/DeltaV/Biscuit/BiscuitSystem.cs new file mode 100644 index 0000000000..9cc7258d76 --- /dev/null +++ b/Content.Client/DeltaV/Biscuit/BiscuitSystem.cs @@ -0,0 +1,26 @@ +using Content.Shared.DeltaV.Biscuit; +using Robust.Client.GameObjects; + +namespace Content.Client.DeltaV.Biscuit; + +public sealed class BiscuitSystem : VisualizerSystem +{ + [Dependency] private readonly AppearanceSystem _appearance = default!; + + protected override void OnAppearanceChange(EntityUid uid, BiscuitVisualsComponent component, + ref AppearanceChangeEvent args) + { + if (args.Sprite == null) + return; + + _appearance.TryGetData(uid, BiscuitStatus.Cracked, out bool cracked); + + args.Sprite.LayerSetVisible(BiscuitVisualLayers.Top, !cracked); + } +} + +public enum BiscuitVisualLayers : byte +{ + Base, + Top +} diff --git a/Content.Client/DeltaV/Biscuit/BiscuitVisualsComponent.cs b/Content.Client/DeltaV/Biscuit/BiscuitVisualsComponent.cs new file mode 100644 index 0000000000..42c745bcb4 --- /dev/null +++ b/Content.Client/DeltaV/Biscuit/BiscuitVisualsComponent.cs @@ -0,0 +1,5 @@ +namespace Content.Client.DeltaV.Biscuit; + +[RegisterComponent] +public sealed partial class BiscuitVisualsComponent : Component +{} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUi.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUi.cs new file mode 100644 index 0000000000..ea5aa3cf25 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUi.cs @@ -0,0 +1,35 @@ +using Robust.Client.UserInterface; +using Content.Client.UserInterface.Fragments; +using Content.Shared.DeltaV.CartridgeLoader.Cartridges; +using Content.Shared.CartridgeLoader; +using Robust.Shared.Prototypes; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +public sealed partial class CrimeAssistUi : UIFragment +{ + private CrimeAssistUiFragment? _fragment; + + public override Control GetUIFragmentRoot() + { + return _fragment!; + } + + public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner) + { + _fragment = new CrimeAssistUiFragment(); + + _fragment.OnSync += _ => SendSyncMessage(userInterface); + } + + private void SendSyncMessage(BoundUserInterface userInterface) + { + var syncMessage = new CrimeAssistSyncMessageEvent(); + var message = new CartridgeUiMessage(syncMessage); + userInterface.SendMessage(message); + } + + public override void UpdateState(BoundUserInterfaceState state) + { + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUiFragment.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUiFragment.xaml new file mode 100644 index 0000000000..8186986d8f --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUiFragment.xaml @@ -0,0 +1,16 @@ + + + + + + + + + + [Access(typeof(ExpendableLightSystem))] - public IPlayingAudioStream? PlayingStream; + public EntityUid? PlayingStream; } public enum ExpendableLightVisualLayers : byte diff --git a/Content.Client/Light/EntitySystems/ExpendableLightSystem.cs b/Content.Client/Light/EntitySystems/ExpendableLightSystem.cs index 6e9e546dfa..a2a7fb2531 100644 --- a/Content.Client/Light/EntitySystems/ExpendableLightSystem.cs +++ b/Content.Client/Light/EntitySystems/ExpendableLightSystem.cs @@ -2,6 +2,8 @@ using Content.Shared.Light.Components; using Robust.Client.GameObjects; using Robust.Client.Graphics; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; namespace Content.Client.Light.EntitySystems; @@ -19,7 +21,7 @@ public override void Initialize() private void OnLightShutdown(EntityUid uid, ExpendableLightComponent component, ComponentShutdown args) { - component.PlayingStream?.Stop(); + component.PlayingStream = _audioSystem.Stop(component.PlayingStream); } protected override void OnAppearanceChange(EntityUid uid, ExpendableLightComponent comp, ref AppearanceChangeEvent args) @@ -48,12 +50,10 @@ protected override void OnAppearanceChange(EntityUid uid, ExpendableLightCompone switch (state) { case ExpendableLightState.Lit: - comp.PlayingStream?.Stop(); + _audioSystem.Stop(comp.PlayingStream); comp.PlayingStream = _audioSystem.PlayPvs( - comp.LoopedSound, - uid, - SharedExpendableLightComponent.LoopedSoundParams - ); + comp.LoopedSound, uid, SharedExpendableLightComponent.LoopedSoundParams)?.Entity; + if (args.Sprite.LayerMapTryGet(ExpendableLightVisualLayers.Overlay, out var layerIdx, true)) { if (!string.IsNullOrWhiteSpace(comp.IconStateLit)) @@ -73,7 +73,7 @@ protected override void OnAppearanceChange(EntityUid uid, ExpendableLightCompone break; case ExpendableLightState.Dead: - comp.PlayingStream?.Stop(); + comp.PlayingStream = _audioSystem.Stop(comp.PlayingStream); if (args.Sprite.LayerMapTryGet(ExpendableLightVisualLayers.Overlay, out layerIdx, true)) { if (!string.IsNullOrWhiteSpace(comp.IconStateSpent)) diff --git a/Content.Client/Light/Visualizers/PoweredLightVisualizerSystem.cs b/Content.Client/Light/Visualizers/PoweredLightVisualizerSystem.cs index bf69053d9a..e7fcf7e219 100644 --- a/Content.Client/Light/Visualizers/PoweredLightVisualizerSystem.cs +++ b/Content.Client/Light/Visualizers/PoweredLightVisualizerSystem.cs @@ -2,6 +2,8 @@ using Robust.Client.Animations; using Robust.Client.GameObjects; using Robust.Shared.Animations; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; using Robust.Shared.Random; namespace Content.Client.Light.Visualizers; diff --git a/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.cs b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.cs index 31db2c9c53..86989cd561 100644 --- a/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.cs +++ b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.cs @@ -119,7 +119,7 @@ public void UpdateUI() OverrideDirection = Direction.South, Scale = new Vector2(4f, 4f), MaxSize = new Vector2(112, 112), - Stretch = SpriteView.StretchMode.None, + Stretch = SpriteView.StretchMode.Fill, }; spriteView.SetEntity(_previewDummy.Value); _viewBox.AddChild(spriteView); diff --git a/Content.Client/MagicMirror/MagicMirrorBoundUserInterface.cs b/Content.Client/MagicMirror/MagicMirrorBoundUserInterface.cs index ba4362a472..ebb6780853 100644 --- a/Content.Client/MagicMirror/MagicMirrorBoundUserInterface.cs +++ b/Content.Client/MagicMirror/MagicMirrorBoundUserInterface.cs @@ -64,5 +64,16 @@ protected override void ReceiveMessage(BoundUserInterfaceMessage message) _window.UpdateState(data); } + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!disposing) + return; + + if (_window != null) + _window.OnClose -= Close; + + _window?.Dispose(); + } } diff --git a/Content.Client/MainMenu/UI/MainMenuControl.xaml b/Content.Client/MainMenu/UI/MainMenuControl.xaml index 16a8cc267a..e1d0652b93 100644 --- a/Content.Client/MainMenu/UI/MainMenuControl.xaml +++ b/Content.Client/MainMenu/UI/MainMenuControl.xaml @@ -5,9 +5,11 @@ + StyleIdentifier="mainMenuVBox" + MinWidth="512"> + - - diff --git a/Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs b/Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs index 852a3c2866..1773b2abe5 100644 --- a/Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs @@ -101,8 +101,6 @@ public GraphicsTab() UpdateApplyButton(); }; - ShowHeldItemCheckBox.OnToggled += OnCheckBoxToggled; - ShowCombatModeIndicatorsCheckBox.OnToggled += OnCheckBoxToggled; IntegerScalingCheckBox.OnToggled += OnCheckBoxToggled; ViewportLowResCheckBox.OnToggled += OnCheckBoxToggled; ParallaxLowQualityCheckBox.OnToggled += OnCheckBoxToggled; @@ -119,8 +117,6 @@ public GraphicsTab() ViewportLowResCheckBox.Pressed = !_cfg.GetCVar(CCVars.ViewportScaleRender); ParallaxLowQualityCheckBox.Pressed = _cfg.GetCVar(CCVars.ParallaxLowQuality); FpsCounterCheckBox.Pressed = _cfg.GetCVar(CCVars.HudFpsCounterVisible); - ShowHeldItemCheckBox.Pressed = _cfg.GetCVar(CCVars.HudHeldItemShow); - ShowCombatModeIndicatorsCheckBox.Pressed = _cfg.GetCVar(CCVars.CombatModeIndicatorsPointShow); ViewportWidthSlider.Value = _cfg.GetCVar(CCVars.ViewportWidth); _cfg.OnValueChanged(CCVars.ViewportMinimumWidth, _ => UpdateViewportWidthRange()); @@ -166,8 +162,6 @@ private void OnApplyButtonPressed(BaseButton.ButtonEventArgs args) IntegerScalingCheckBox.Pressed ? CCVars.ViewportSnapToleranceMargin.DefaultValue : 0); _cfg.SetCVar(CCVars.ViewportScaleRender, !ViewportLowResCheckBox.Pressed); _cfg.SetCVar(CCVars.ParallaxLowQuality, ParallaxLowQualityCheckBox.Pressed); - _cfg.SetCVar(CCVars.HudHeldItemShow, ShowHeldItemCheckBox.Pressed); - _cfg.SetCVar(CCVars.CombatModeIndicatorsPointShow, ShowCombatModeIndicatorsCheckBox.Pressed); _cfg.SetCVar(CCVars.HudFpsCounterVisible, FpsCounterCheckBox.Pressed); _cfg.SetCVar(CCVars.ViewportWidth, (int) ViewportWidthSlider.Value); @@ -203,8 +197,6 @@ private void UpdateApplyButton() var isIntegerScalingSame = IntegerScalingCheckBox.Pressed == (_cfg.GetCVar(CCVars.ViewportSnapToleranceMargin) != 0); var isVPResSame = ViewportLowResCheckBox.Pressed == !_cfg.GetCVar(CCVars.ViewportScaleRender); var isPLQSame = ParallaxLowQualityCheckBox.Pressed == _cfg.GetCVar(CCVars.ParallaxLowQuality); - var isShowHeldItemSame = ShowHeldItemCheckBox.Pressed == _cfg.GetCVar(CCVars.HudHeldItemShow); - var isCombatModeIndicatorsSame = ShowCombatModeIndicatorsCheckBox.Pressed == _cfg.GetCVar(CCVars.CombatModeIndicatorsPointShow); var isFpsCounterVisibleSame = FpsCounterCheckBox.Pressed == _cfg.GetCVar(CCVars.HudFpsCounterVisible); var isWidthSame = (int) ViewportWidthSlider.Value == _cfg.GetCVar(CCVars.ViewportWidth); var isLayoutSame = HudLayoutOption.SelectedMetadata is string opt && opt == _cfg.GetCVar(CCVars.UILayout); @@ -219,8 +211,6 @@ private void UpdateApplyButton() isVPResSame && isPLQSame && isHudThemeSame && - isShowHeldItemSame && - isCombatModeIndicatorsSame && isFpsCounterVisibleSame && isWidthSame && isLayoutSame; diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs index c68e7f3af9..87b1f10352 100644 --- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs @@ -224,10 +224,6 @@ void AddCheckBox(string checkBoxName, bool currentState, Action + + + + + [Prototype("parallax")] -public sealed class ParallaxPrototype : IPrototype +public sealed partial class ParallaxPrototype : IPrototype { /// [IdDataField] diff --git a/Content.Client/Parallax/ParallaxSystem.cs b/Content.Client/Parallax/ParallaxSystem.cs index 8b96cbdc3d..720da72e80 100644 --- a/Content.Client/Parallax/ParallaxSystem.cs +++ b/Content.Client/Parallax/ParallaxSystem.cs @@ -1,3 +1,4 @@ +using System.Numerics; using Content.Client.Parallax.Data; using Content.Client.Parallax.Managers; using Content.Shared.Parallax; @@ -23,14 +24,13 @@ public override void Initialize() { base.Initialize(); _overlay.AddOverlay(new ParallaxOverlay()); - _protoManager.PrototypesReloaded += OnReload; - + SubscribeLocalEvent(OnReload); SubscribeLocalEvent(OnAfterAutoHandleState); } private void OnReload(PrototypesReloadedEventArgs obj) { - if (!obj.ByType.ContainsKey(typeof(ParallaxPrototype))) + if (!obj.WasModified()) return; _parallax.UnloadParallax(Fallback); @@ -47,7 +47,6 @@ public override void Shutdown() { base.Shutdown(); _overlay.RemoveOverlay(); - _protoManager.PrototypesReloaded -= OnReload; } private void OnAfterAutoHandleState(EntityUid uid, ParallaxComponent component, ref AfterAutoHandleStateEvent args) @@ -72,4 +71,56 @@ public string GetParallax(EntityUid mapUid) { return TryComp(mapUid, out var parallax) ? parallax.Parallax : Fallback; } + + /// + /// Draws a texture as parallax in the specified world handle. + /// + /// + /// WorldAABB to use + /// Sprite to draw + /// Current time, unused if scrolling not set + /// Current position of the parallax + /// How much to scroll the parallax texture per second + /// Scale of the texture + /// How slow the parallax moves compared to position + /// Color modulation applied to drawing the texture + public void DrawParallax( + DrawingHandleWorld worldHandle, + Box2 worldAABB, + Texture sprite, + TimeSpan curTime, + Vector2 position, + Vector2 scrolling, + float scale = 1f, + float slowness = 0f, + Color? modulate = null) + { + // Size of the texture in world units. + var size = sprite.Size / (float) EyeManager.PixelsPerMeter * scale; + var scrolled = scrolling * (float) curTime.TotalSeconds; + + // Origin - start with the parallax shift itself. + var originBL = position * slowness + scrolled; + + // Centre the image. + originBL -= size / 2; + + // Remove offset so we can floor. + var flooredBL = worldAABB.BottomLeft - originBL; + + // Floor to background size. + flooredBL = (flooredBL / size).Floored() * size; + + // Re-offset. + flooredBL += originBL; + + for (var x = flooredBL.X; x < worldAABB.Right; x += size.X) + { + for (var y = flooredBL.Y; y < worldAABB.Top; y += size.Y) + { + var box = Box2.FromDimensions(new Vector2(x, y), size); + worldHandle.DrawTextureRect(sprite, box, modulate); + } + } + } } diff --git a/Content.Client/Physics/Controllers/MoverController.cs b/Content.Client/Physics/Controllers/MoverController.cs index 54c5c3de15..763f7b0114 100644 --- a/Content.Client/Physics/Controllers/MoverController.cs +++ b/Content.Client/Physics/Controllers/MoverController.cs @@ -1,10 +1,10 @@ using Content.Shared.Movement.Components; using Content.Shared.Movement.Systems; using Content.Shared.Pulling.Components; -using Robust.Client.GameObjects; using Robust.Client.Physics; using Robust.Client.Player; using Robust.Shared.Physics.Components; +using Robust.Shared.Player; using Robust.Shared.Timing; namespace Content.Client.Physics.Controllers @@ -17,10 +17,10 @@ public sealed class MoverController : SharedMoverController public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnRelayPlayerAttached); - SubscribeLocalEvent(OnRelayPlayerDetached); - SubscribeLocalEvent(OnPlayerAttached); - SubscribeLocalEvent(OnPlayerDetached); + SubscribeLocalEvent(OnRelayPlayerAttached); + SubscribeLocalEvent(OnRelayPlayerDetached); + SubscribeLocalEvent(OnPlayerAttached); + SubscribeLocalEvent(OnPlayerDetached); SubscribeLocalEvent(OnUpdatePredicted); SubscribeLocalEvent(OnUpdateRelayTargetPredicted); @@ -54,7 +54,7 @@ private void OnUpdatePullablePredicted(EntityUid uid, SharedPullableComponent co // What if the entity is being pulled by a vehicle controlled by the player? } - private void OnRelayPlayerAttached(EntityUid uid, RelayInputMoverComponent component, PlayerAttachedEvent args) + private void OnRelayPlayerAttached(EntityUid uid, RelayInputMoverComponent component, LocalPlayerAttachedEvent args) { Physics.UpdateIsPredicted(uid); Physics.UpdateIsPredicted(component.RelayEntity); @@ -62,7 +62,7 @@ private void OnRelayPlayerAttached(EntityUid uid, RelayInputMoverComponent compo SetMoveInput(inputMover, MoveButtons.None); } - private void OnRelayPlayerDetached(EntityUid uid, RelayInputMoverComponent component, PlayerDetachedEvent args) + private void OnRelayPlayerDetached(EntityUid uid, RelayInputMoverComponent component, LocalPlayerDetachedEvent args) { Physics.UpdateIsPredicted(uid); Physics.UpdateIsPredicted(component.RelayEntity); @@ -70,12 +70,12 @@ private void OnRelayPlayerDetached(EntityUid uid, RelayInputMoverComponent compo SetMoveInput(inputMover, MoveButtons.None); } - private void OnPlayerAttached(EntityUid uid, InputMoverComponent component, PlayerAttachedEvent args) + private void OnPlayerAttached(EntityUid uid, InputMoverComponent component, LocalPlayerAttachedEvent args) { SetMoveInput(component, MoveButtons.None); } - private void OnPlayerDetached(EntityUid uid, InputMoverComponent component, PlayerDetachedEvent args) + private void OnPlayerDetached(EntityUid uid, InputMoverComponent component, LocalPlayerDetachedEvent args) { SetMoveInput(component, MoveButtons.None); } diff --git a/Content.Client/Pinpointer/UI/NavMapControl.cs b/Content.Client/Pinpointer/UI/NavMapControl.cs index 04d8cc76f9..438c06f7f2 100644 --- a/Content.Client/Pinpointer/UI/NavMapControl.cs +++ b/Content.Client/Pinpointer/UI/NavMapControl.cs @@ -1,40 +1,68 @@ -using System.Numerics; using Content.Client.Stylesheets; using Content.Client.UserInterface.Controls; using Content.Shared.Pinpointer; -using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.ResourceManagement; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; +using Robust.Shared.Collections; using Robust.Shared.Input; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Physics; using Robust.Shared.Physics.Collision.Shapes; using Robust.Shared.Physics.Components; +using Robust.Shared.Timing; +using System.Numerics; +using JetBrains.Annotations; namespace Content.Client.Pinpointer.UI; /// /// Displays the nav map data of the specified grid. /// -public sealed class NavMapControl : MapGridControl +[UsedImplicitly, Virtual] +public partial class NavMapControl : MapGridControl { [Dependency] private readonly IEntityManager _entManager = default!; - private SharedTransformSystem _transform; + private readonly SharedTransformSystem _transformSystem = default!; public EntityUid? MapUid; + // Actions + public event Action? TrackedEntitySelectedAction; + + // Tracked data public Dictionary TrackedCoordinates = new(); + public Dictionary TrackedEntities = new(); + public Dictionary> TileGrid = default!; + + // Default colors + public Color WallColor = new(102, 217, 102); + public Color TileColor = new(30, 67, 30); + + // Constants + protected float UpdateTime = 1.0f; + protected float MaxSelectableDistance = 10f; + protected float RecenterMinimum = 0.05f; + protected float MinDragDistance = 5f; + // Local variables private Vector2 _offset; private bool _draggin; + private Vector2 _startDragPosition = default!; private bool _recentering = false; - private readonly float _recenterMinimum = 0.05f; private readonly Font _font; - private static readonly Color TileColor = new(30, 67, 30); - private static readonly Color BeaconColor = Color.FromSrgb(TileColor.WithAlpha(0.8f)); + private float _updateTimer = 0.25f; + private Dictionary _sRGBLookUp = new Dictionary(); + private Color _beaconColor; + + // Components + private NavMapComponent? _navMap; + private MapGridComponent? _grid; + private TransformComponent? _xform; + private PhysicsComponent? _physics; + private FixturesComponent? _fixtures; // TODO: https://github.com/space-wizards/RobustToolbox/issues/3818 private readonly Label _zoom = new() @@ -45,20 +73,30 @@ public sealed class NavMapControl : MapGridControl private readonly Button _recenter = new() { - Text = "Recentre", + Text = Loc.GetString("navmap-recenter"), VerticalAlignment = VAlignment.Top, HorizontalAlignment = HAlignment.Right, Margin = new Thickness(8f, 4f), Disabled = true, }; + private readonly CheckBox _beacons = new() + { + Text = Loc.GetString("navmap-toggle-beacons"), + Margin = new Thickness(4f, 0f), + VerticalAlignment = VAlignment.Center, + HorizontalAlignment = HAlignment.Center, + Pressed = false, + }; + public NavMapControl() : base(8f, 128f, 48f) { IoCManager.InjectDependencies(this); - - _transform = _entManager.System(); var cache = IoCManager.Resolve(); - _font = new VectorFont(cache.GetResource("/EngineFonts/NotoSans/NotoSans-Regular.ttf"), 16); + + _transformSystem = _entManager.System(); + _font = new VectorFont(cache.GetResource("/EngineFonts/NotoSans/NotoSans-Regular.ttf"), 12); + _beaconColor = Color.FromSrgb(TileColor.WithAlpha(0.8f)); RectClipContent = true; HorizontalExpand = true; @@ -75,6 +113,7 @@ public NavMapControl() : base(8f, 128f, 48f) Children = { _zoom, + _beacons, _recenter, } }; @@ -101,14 +140,28 @@ public NavMapControl() : base(8f, 128f, 48f) { _recentering = true; }; + + ForceNavMapUpdate(); + } + + public void ForceRecenter() + { + _recentering = true; + } + + public void ForceNavMapUpdate() + { + _entManager.TryGetComponent(MapUid, out _navMap); + _entManager.TryGetComponent(MapUid, out _grid); + + UpdateNavMap(); } public void CenterToCoordinates(EntityCoordinates coordinates) { - if (_entManager.TryGetComponent(MapUid, out var physics)) - { - _offset = new Vector2(coordinates.X, coordinates.Y) - physics.LocalCenter; - } + if (_physics != null) + _offset = new Vector2(coordinates.X, coordinates.Y) - _physics.LocalCenter; + _recenter.Disabled = false; } @@ -118,6 +171,7 @@ protected override void KeyBindDown(GUIBoundKeyEventArgs args) if (args.Function == EngineKeyFunctions.Use) { + _startDragPosition = args.PointerLocation.Position; _draggin = true; } } @@ -127,8 +181,59 @@ protected override void KeyBindUp(GUIBoundKeyEventArgs args) base.KeyBindUp(args); if (args.Function == EngineKeyFunctions.Use) - { _draggin = false; + + if (TrackedEntitySelectedAction == null) + return; + + if (args.Function == EngineKeyFunctions.Use) + { + if (_xform == null || _physics == null || TrackedEntities.Count == 0) + return; + + // If the cursor has moved a significant distance, exit + if ((_startDragPosition - args.PointerLocation.Position).Length() > MinDragDistance) + return; + + // Get the clicked position + var offset = _offset + _physics.LocalCenter; + var localPosition = args.PointerLocation.Position - GlobalPixelPosition; + + // Convert to a world position + var unscaledPosition = (localPosition - MidpointVector) / MinimapScale; + var worldPosition = _transformSystem.GetWorldMatrix(_xform).Transform(new Vector2(unscaledPosition.X, -unscaledPosition.Y) + offset); + + // Find closest tracked entity in range + var closestEntity = NetEntity.Invalid; + var closestCoords = new EntityCoordinates(); + var closestDistance = float.PositiveInfinity; + + foreach ((var currentEntity, var blip) in TrackedEntities) + { + if (!blip.Selectable) + continue; + + var currentDistance = (blip.Coordinates.ToMapPos(_entManager, _transformSystem) - worldPosition).Length(); + + if (closestDistance < currentDistance || currentDistance * MinimapScale > MaxSelectableDistance) + continue; + + closestEntity = currentEntity; + closestCoords = blip.Coordinates; + closestDistance = currentDistance; + } + + if (closestDistance > MaxSelectableDistance || !closestEntity.IsValid()) + return; + + TrackedEntitySelectedAction.Invoke(closestEntity); + } + + else if (args.Function == EngineKeyFunctions.UIRightClick) + { + // Clear current selection with right click + if (TrackedEntitySelectedAction != null) + TrackedEntitySelectedAction.Invoke(null); } } @@ -143,25 +248,30 @@ protected override void MouseMove(GUIMouseMoveEventArgs args) _offset -= new Vector2(args.Relative.X, -args.Relative.Y) / MidPoint * WorldRange; if (_offset != Vector2.Zero) - { _recenter.Disabled = false; - } + else - { _recenter.Disabled = true; - } } protected override void Draw(DrawingHandleScreen handle) { base.Draw(handle); + // Get the components necessary for drawing the navmap + _entManager.TryGetComponent(MapUid, out _navMap); + _entManager.TryGetComponent(MapUid, out _grid); + _entManager.TryGetComponent(MapUid, out _xform); + _entManager.TryGetComponent(MapUid, out _physics); + _entManager.TryGetComponent(MapUid, out _fixtures); + + // Map re-centering if (_recentering) { var frameTime = Timing.FrameTime; var diff = _offset * (float) frameTime.TotalSeconds; - if (_offset.LengthSquared() < _recenterMinimum) + if (_offset.LengthSquared() < RecenterMinimum) { _offset = Vector2.Zero; _recentering = false; @@ -173,29 +283,22 @@ protected override void Draw(DrawingHandleScreen handle) } } - _zoom.Text = $"Zoom: {(WorldRange / WorldMaxRange * 100f):0.00}%"; + _zoom.Text = Loc.GetString("navmap-zoom", ("value", $"{(WorldRange / WorldMaxRange * 100f):0.00}")); - if (!_entManager.TryGetComponent(MapUid, out var navMap) || - !_entManager.TryGetComponent(MapUid, out var xform) || - !_entManager.TryGetComponent(MapUid, out var grid)) - { + if (_navMap == null || _xform == null) return; - } var offset = _offset; - var lineColor = new Color(102, 217, 102); - if (_entManager.TryGetComponent(MapUid, out var physics)) - { - offset += physics.LocalCenter; - } + if (_physics != null) + offset += _physics.LocalCenter; // Draw tiles - if (_entManager.TryGetComponent(MapUid, out var manager)) + if (_fixtures != null) { Span verts = new Vector2[8]; - foreach (var fixture in manager.Fixtures.Values) + foreach (var fixture in _fixtures.Fixtures.Values) { if (fixture.Shape is not PolygonShape poly) continue; @@ -211,113 +314,63 @@ protected override void Draw(DrawingHandleScreen handle) } } - // Draw the wall data var area = new Box2(-WorldRange, -WorldRange, WorldRange + 1f, WorldRange + 1f).Translated(offset); - var tileSize = new Vector2(grid.TileSize, -grid.TileSize); - for (var x = Math.Floor(area.Left); x <= Math.Ceiling(area.Right); x += SharedNavMapSystem.ChunkSize * grid.TileSize) + // Drawing lines can be rather expensive due to the number of neighbors that need to be checked in order + // to figure out where they should be drawn. However, we don't *need* to do check these every frame. + // Instead, lets periodically update where to draw each line and then store these points in a list. + // Then we can just run through the list each frame and draw the lines without any extra computation. + + // Draw walls + if (TileGrid != null && TileGrid.Count > 0) { - for (var y = Math.Floor(area.Bottom); y <= Math.Ceiling(area.Top); y += SharedNavMapSystem.ChunkSize * grid.TileSize) + var walls = new ValueList(); + + foreach ((var chunk, var chunkedLines) in TileGrid) { - var floored = new Vector2i((int) x, (int) y); + var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize; - var chunkOrigin = SharedMapSystem.GetChunkIndices(floored, SharedNavMapSystem.ChunkSize); + if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right) + continue; - if (!navMap.Chunks.TryGetValue(chunkOrigin, out var chunk)) + if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top) continue; - // TODO: Okay maybe I should just use ushorts lmao... - for (var i = 0; i < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; i++) + foreach (var chunkedLine in chunkedLines) + { + var start = Scale(chunkedLine.Origin - new Vector2(offset.X, -offset.Y)); + var end = Scale(chunkedLine.Terminus - new Vector2(offset.X, -offset.Y)); + + walls.Add(start); + walls.Add(end); + } + } + + if (walls.Count > 0) + { + if (!_sRGBLookUp.TryGetValue(WallColor, out var sRGB)) { - var value = (int) Math.Pow(2, i); - - var mask = chunk.TileData & value; - - if (mask == 0x0) - continue; - - // Alright now we'll work out our edges - var relativeTile = SharedNavMapSystem.GetTile(mask); - var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * grid.TileSize - offset; - var position = new Vector2(tile.X, -tile.Y); - NavMapChunk? neighborChunk; - bool neighbor; - - // North edge - if (relativeTile.Y == SharedNavMapSystem.ChunkSize - 1) - { - neighbor = navMap.Chunks.TryGetValue(chunkOrigin + new Vector2i(0, 1), out neighborChunk) && - (neighborChunk.TileData & - SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, 0))) != 0x0; - } - else - { - var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, 1)); - neighbor = (chunk.TileData & flag) != 0x0; - } - - if (!neighbor) - { - handle.DrawLine(Scale(position + new Vector2(0f, -grid.TileSize)), Scale(position + tileSize), lineColor); - } - - // East edge - if (relativeTile.X == SharedNavMapSystem.ChunkSize - 1) - { - neighbor = navMap.Chunks.TryGetValue(chunkOrigin + new Vector2i(1, 0), out neighborChunk) && - (neighborChunk.TileData & - SharedNavMapSystem.GetFlag(new Vector2i(0, relativeTile.Y))) != 0x0; - } - else - { - var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(1, 0)); - neighbor = (chunk.TileData & flag) != 0x0; - } - - if (!neighbor) - { - handle.DrawLine(Scale(position + tileSize), Scale(position + new Vector2(grid.TileSize, 0f)), lineColor); - } - - // South edge - if (relativeTile.Y == 0) - { - neighbor = navMap.Chunks.TryGetValue(chunkOrigin + new Vector2i(0, -1), out neighborChunk) && - (neighborChunk.TileData & - SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, SharedNavMapSystem.ChunkSize - 1))) != 0x0; - } - else - { - var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, -1)); - neighbor = (chunk.TileData & flag) != 0x0; - } - - if (!neighbor) - { - handle.DrawLine(Scale(position + new Vector2(grid.TileSize, 0f)), Scale(position), lineColor); - } - - // West edge - if (relativeTile.X == 0) - { - neighbor = navMap.Chunks.TryGetValue(chunkOrigin + new Vector2i(-1, 0), out neighborChunk) && - (neighborChunk.TileData & - SharedNavMapSystem.GetFlag(new Vector2i(SharedNavMapSystem.ChunkSize - 1, relativeTile.Y))) != 0x0; - } - else - { - var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(-1, 0)); - neighbor = (chunk.TileData & flag) != 0x0; - } - - if (!neighbor) - { - handle.DrawLine(Scale(position), Scale(position + new Vector2(0f, -grid.TileSize)), lineColor); - } - - // Draw a diagonal line for interiors. - handle.DrawLine(Scale(position + new Vector2(0f, -grid.TileSize)), Scale(position + new Vector2(grid.TileSize, 0f)), lineColor); + sRGB = Color.ToSrgb(WallColor); + _sRGBLookUp[WallColor] = sRGB; } + + handle.DrawPrimitives(DrawPrimitiveTopology.LineList, walls.Span, sRGB); + } + } + + // Beacons + if (_beacons.Pressed) + { + var rectBuffer = new Vector2(5f, 3f); + + foreach (var beacon in _navMap.Beacons) + { + var position = beacon.Position - offset; + position = Scale(position with { Y = -position.Y }); + + var textDimensions = handle.GetDimensions(_font, beacon.Text, 1f); + handle.DrawRect(new UIBox2(position - textDimensions / 2 - rectBuffer, position + textDimensions / 2 + rectBuffer), _beaconColor); + handle.DrawString(_font, position - textDimensions / 2, beacon.Text, beacon.Color); } } @@ -325,43 +378,241 @@ protected override void Draw(DrawingHandleScreen handle) var blinkFrequency = 1f / 1f; var lit = curTime.TotalSeconds % blinkFrequency > blinkFrequency / 2f; + // Tracked coordinates (simple dot, legacy) foreach (var (coord, value) in TrackedCoordinates) { if (lit && value.Visible) { - var mapPos = coord.ToMap(_entManager); + var mapPos = coord.ToMap(_entManager, _transformSystem); if (mapPos.MapId != MapId.Nullspace) { - var position = xform.InvWorldMatrix.Transform(mapPos.Position) - offset; + var position = _transformSystem.GetInvWorldMatrix(_xform).Transform(mapPos.Position) - offset; position = Scale(new Vector2(position.X, -position.Y)); - handle.DrawCircle(position, MinimapScale / 2f, value.Color); + handle.DrawCircle(position, float.Sqrt(MinimapScale) * 2f, value.Color); } } } - // Beacons - var labelOffset = new Vector2(0.5f, 0.5f) * MinimapScale; - var rectBuffer = new Vector2(5f, 3f); + // Tracked entities (can use a supplied sprite as a marker instead; should probably just replace TrackedCoordinates with this eventually) + var iconVertexUVs = new Dictionary<(Texture, Color), ValueList>(); - foreach (var beacon in navMap.Beacons) + foreach (var blip in TrackedEntities.Values) { - var position = beacon.Position - offset; + if (blip.Blinks && !lit) + continue; + + if (blip.Texture == null) + continue; - position = Scale(position with { Y = -position.Y }); + if (!iconVertexUVs.TryGetValue((blip.Texture, blip.Color), out var vertexUVs)) + vertexUVs = new(); - handle.DrawCircle(position, MinimapScale / 2f, beacon.Color); - var textDimensions = handle.GetDimensions(_font, beacon.Text, 1f); + var mapPos = blip.Coordinates.ToMap(_entManager, _transformSystem); - var labelPosition = position + labelOffset; - handle.DrawRect(new UIBox2(labelPosition, labelPosition + textDimensions + rectBuffer * 2), BeaconColor); - handle.DrawString(_font, labelPosition + rectBuffer, beacon.Text, beacon.Color); + if (mapPos.MapId != MapId.Nullspace) + { + var position = _transformSystem.GetInvWorldMatrix(_xform).Transform(mapPos.Position) - offset; + position = Scale(new Vector2(position.X, -position.Y)); + + var scalingCoefficient = 2.5f; + var positionOffset = scalingCoefficient * float.Sqrt(MinimapScale); + + vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y - positionOffset), new Vector2(1f, 1f))); + vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y + positionOffset), new Vector2(1f, 0f))); + vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y - positionOffset), new Vector2(0f, 1f))); + vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y + positionOffset), new Vector2(1f, 0f))); + vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y - positionOffset), new Vector2(0f, 1f))); + vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y + positionOffset), new Vector2(0f, 0f))); + } + + iconVertexUVs[(blip.Texture, blip.Color)] = vertexUVs; } + + foreach ((var (texture, color), var vertexUVs) in iconVertexUVs) + { + if (!_sRGBLookUp.TryGetValue(color, out var sRGB)) + { + sRGB = Color.ToSrgb(color); + _sRGBLookUp[color] = sRGB; + } + + handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, texture, vertexUVs.Span, sRGB); + } + } + + protected override void FrameUpdate(FrameEventArgs args) + { + // Update the timer + _updateTimer += args.DeltaSeconds; + + if (_updateTimer >= UpdateTime) + { + _updateTimer -= UpdateTime; + + UpdateNavMap(); + } + } + + private void UpdateNavMap() + { + if (_navMap == null || _grid == null) + return; + + TileGrid = GetDecodedWallChunks(_navMap.Chunks, _grid); } - private Vector2 Scale(Vector2 position) + public Dictionary> GetDecodedWallChunks + (Dictionary chunks, + MapGridComponent grid) + { + var decodedOutput = new Dictionary>(); + + foreach ((var chunkOrigin, var chunk) in chunks) + { + var list = new List(); + + // TODO: Okay maybe I should just use ushorts lmao... + for (var i = 0; i < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; i++) + { + var value = (int) Math.Pow(2, i); + + var mask = chunk.TileData & value; + + if (mask == 0x0) + continue; + + // Alright now we'll work out our edges + var relativeTile = SharedNavMapSystem.GetTile(mask); + var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * grid.TileSize; + var position = new Vector2(tile.X, -tile.Y); + NavMapChunk? neighborChunk; + bool neighbor; + + // North edge + if (relativeTile.Y == SharedNavMapSystem.ChunkSize - 1) + { + neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(0, 1), out neighborChunk) && + (neighborChunk.TileData & + SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, 0))) != 0x0; + } + else + { + var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, 1)); + neighbor = (chunk.TileData & flag) != 0x0; + } + + if (!neighbor) + { + // Add points + list.Add(new NavMapLine(position + new Vector2(0f, -grid.TileSize), position + new Vector2(grid.TileSize, -grid.TileSize))); + } + + // East edge + if (relativeTile.X == SharedNavMapSystem.ChunkSize - 1) + { + neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(1, 0), out neighborChunk) && + (neighborChunk.TileData & + SharedNavMapSystem.GetFlag(new Vector2i(0, relativeTile.Y))) != 0x0; + } + else + { + var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(1, 0)); + neighbor = (chunk.TileData & flag) != 0x0; + } + + if (!neighbor) + { + // Add points + list.Add(new NavMapLine(position + new Vector2(grid.TileSize, -grid.TileSize), position + new Vector2(grid.TileSize, 0f))); + } + + // South edge + if (relativeTile.Y == 0) + { + neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(0, -1), out neighborChunk) && + (neighborChunk.TileData & + SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, SharedNavMapSystem.ChunkSize - 1))) != 0x0; + } + else + { + var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, -1)); + neighbor = (chunk.TileData & flag) != 0x0; + } + + if (!neighbor) + { + // Add points + list.Add(new NavMapLine(position + new Vector2(grid.TileSize, 0f), position)); + } + + // West edge + if (relativeTile.X == 0) + { + neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(-1, 0), out neighborChunk) && + (neighborChunk.TileData & + SharedNavMapSystem.GetFlag(new Vector2i(SharedNavMapSystem.ChunkSize - 1, relativeTile.Y))) != 0x0; + } + else + { + var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(-1, 0)); + neighbor = (chunk.TileData & flag) != 0x0; + } + + if (!neighbor) + { + // Add point + list.Add(new NavMapLine(position, position + new Vector2(0f, -grid.TileSize))); + } + + // Draw a diagonal line for interiors. + list.Add(new NavMapLine(position + new Vector2(0f, -grid.TileSize), position + new Vector2(grid.TileSize, 0f))); + } + + decodedOutput.Add(chunkOrigin, list); + } + + return decodedOutput; + } + + protected Vector2 Scale(Vector2 position) { return position * MinimapScale + MidpointVector; } + + protected Vector2 GetOffset() + { + return _offset + (_physics != null ? _physics.LocalCenter : new Vector2()); + } +} + +public struct NavMapBlip +{ + public EntityCoordinates Coordinates; + public Texture Texture; + public Color Color; + public bool Blinks; + public bool Selectable; + + public NavMapBlip(EntityCoordinates coordinates, Texture texture, Color color, bool blinks, bool selectable = true) + { + Coordinates = coordinates; + Texture = texture; + Color = color; + Blinks = blinks; + Selectable = selectable; + } +} + +public struct NavMapLine +{ + public readonly Vector2 Origin; + public readonly Vector2 Terminus; + + public NavMapLine(Vector2 origin, Vector2 terminus) + { + Origin = origin; + Terminus = terminus; + } } diff --git a/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs b/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs index 1fa12fa9e7..f52f536775 100644 --- a/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs +++ b/Content.Client/Pinpointer/UI/StationMapWindow.xaml.cs @@ -21,5 +21,7 @@ public StationMapWindow(EntityUid? mapUid, EntityUid? trackedEntity) { Title = metadata.EntityName; } + + NavMapScreen.ForceNavMapUpdate(); } } diff --git a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs index 0e559d0f8c..5027f77663 100644 --- a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs +++ b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs @@ -1,6 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text; using Content.Shared.CCVar; using Content.Shared.Players; using Content.Shared.Players.PlayTimeTracking; @@ -123,4 +121,24 @@ public bool CheckRoleTime(HashSet? requirements, [NotNullWhen(fa reason = reasons.Count == 0 ? null : FormattedMessage.FromMarkup(string.Join('\n', reasons)); return reason == null; } + + public TimeSpan FetchOverallPlaytime() + { + return _roles.TryGetValue("Overall", out var overallPlaytime) ? overallPlaytime : TimeSpan.Zero; + } + + public IEnumerable> FetchPlaytimeByRoles() + { + var jobsToMap = _prototypes.EnumeratePrototypes(); + + foreach (var job in jobsToMap) + { + if (_roles.TryGetValue(job.PlayTimeTracker, out var locJobName)) + { + yield return new KeyValuePair(job.Name, locJobName); + } + } + } + + } diff --git a/Content.Client/Players/PlayerSystem.cs b/Content.Client/Players/PlayerSystem.cs index d5ce4ec197..dba95ef7a6 100644 --- a/Content.Client/Players/PlayerSystem.cs +++ b/Content.Client/Players/PlayerSystem.cs @@ -1,11 +1,11 @@ using Content.Shared.Players; -using Robust.Shared.Players; +using Robust.Shared.Player; namespace Content.Client.Players; public sealed class PlayerSystem : SharedPlayerSystem { - public override PlayerData? ContentData(ICommonSession? session) + public override ContentPlayerData? ContentData(ICommonSession? session) { return null; } diff --git a/Content.Client/Popups/PopupSystem.cs b/Content.Client/Popups/PopupSystem.cs index 1d4ca19ce2..d68272a107 100644 --- a/Content.Client/Popups/PopupSystem.cs +++ b/Content.Client/Popups/PopupSystem.cs @@ -9,7 +9,6 @@ using Robust.Shared.Configuration; using Robust.Shared.Map; using Robust.Shared.Player; -using Robust.Shared.Players; using Robust.Shared.Prototypes; using Robust.Shared.Replays; using Robust.Shared.Timing; diff --git a/Content.Client/Power/Generator/GeneratorWindow.xaml.cs b/Content.Client/Power/Generator/GeneratorWindow.xaml.cs index d3949807b7..0b8f94ceae 100644 --- a/Content.Client/Power/Generator/GeneratorWindow.xaml.cs +++ b/Content.Client/Power/Generator/GeneratorWindow.xaml.cs @@ -14,6 +14,7 @@ public sealed partial class GeneratorWindow : FancyWindow [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly ILocalizationManager _loc = default!; + private readonly SharedPowerSwitchableSystem _switchable; private readonly FuelGeneratorComponent? _component; private PortableGeneratorComponentBuiState? _lastState; @@ -24,6 +25,7 @@ public GeneratorWindow(PortableGeneratorBoundUserInterface bui, EntityUid entity IoCManager.InjectDependencies(this); _entityManager.TryGetComponent(entity, out _component); + _switchable = _entityManager.System(); EntityView.SetEntity(entity); TargetPower.IsValid += IsValid; @@ -99,17 +101,16 @@ public void Update(PortableGeneratorComponentBuiState state) StatusLabel.SetOnlyStyleClass("Danger"); } - var canSwitch = _entityManager.TryGetComponent(_entity, out PowerSwitchableGeneratorComponent? switchable); + var canSwitch = _entityManager.TryGetComponent(_entity, out PowerSwitchableComponent? switchable); OutputSwitchLabel.Visible = canSwitch; OutputSwitchButton.Visible = canSwitch; - if (canSwitch) + if (switchable != null) { - var isHV = switchable!.ActiveOutput == PowerSwitchableGeneratorOutput.HV; - OutputSwitchLabel.Text = - Loc.GetString(isHV ? "portable-generator-ui-switch-hv" : "portable-generator-ui-switch-mv"); - OutputSwitchButton.Text = - Loc.GetString(isHV ? "portable-generator-ui-switch-to-mv" : "portable-generator-ui-switch-to-hv"); + var voltage = _switchable.VoltageString(_switchable.GetVoltage(_entity, switchable)); + OutputSwitchLabel.Text = Loc.GetString("portable-generator-ui-current-output", ("voltage", voltage)); + var nextVoltage = _switchable.VoltageString(_switchable.GetNextVoltage(_entity, switchable)); + OutputSwitchButton.Text = Loc.GetString("power-switchable-switch-voltage", ("voltage", nextVoltage)); OutputSwitchButton.Disabled = state.On; } diff --git a/Content.Client/Power/Generator/PowerSwitchableSystem.cs b/Content.Client/Power/Generator/PowerSwitchableSystem.cs new file mode 100644 index 0000000000..b235dee77d --- /dev/null +++ b/Content.Client/Power/Generator/PowerSwitchableSystem.cs @@ -0,0 +1,7 @@ +using Content.Shared.Power.Generator; + +namespace Content.Client.Power.Generator; + +public sealed class PowerSwitchableSystem : SharedPowerSwitchableSystem +{ +} diff --git a/Content.Client/PowerCell/PowerCellSystem.cs b/Content.Client/PowerCell/PowerCellSystem.cs index ec69f0d56b..1423dffaa3 100644 --- a/Content.Client/PowerCell/PowerCellSystem.cs +++ b/Content.Client/PowerCell/PowerCellSystem.cs @@ -1,4 +1,5 @@ using Content.Shared.PowerCell; +using Content.Shared.PowerCell.Components; using JetBrains.Annotations; using Robust.Client.GameObjects; @@ -15,6 +16,29 @@ public override void Initialize() SubscribeLocalEvent(OnPowerCellVisualsChange); } + /// + public override bool HasActivatableCharge(EntityUid uid, PowerCellDrawComponent? battery = null, PowerCellSlotComponent? cell = null, + EntityUid? user = null) + { + if (!Resolve(uid, ref battery, ref cell, false)) + return true; + + return battery.CanUse; + } + + /// + public override bool HasDrawCharge( + EntityUid uid, + PowerCellDrawComponent? battery = null, + PowerCellSlotComponent? cell = null, + EntityUid? user = null) + { + if (!Resolve(uid, ref battery, ref cell, false)) + return true; + + return battery.CanDraw; + } + private void OnPowerCellVisualsChange(EntityUid uid, PowerCellVisualsComponent component, ref AppearanceChangeEvent args) { if (args.Sprite == null) diff --git a/Content.Client/Preferences/UI/CharacterSetupGui.xaml b/Content.Client/Preferences/UI/CharacterSetupGui.xaml index 5db8610475..9a76029ce0 100644 --- a/Content.Client/Preferences/UI/CharacterSetupGui.xaml +++ b/Content.Client/Preferences/UI/CharacterSetupGui.xaml @@ -10,10 +10,13 @@