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/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 a487e5d8d8..508f3404ba 100644 --- a/Content.Client/Actions/ActionsSystem.cs +++ b/Content.Client/Actions/ActionsSystem.cs @@ -6,6 +6,7 @@ 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; @@ -86,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; 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/UI/CustomControls/PlayerListControl.xaml.cs b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs index 5fa28bf1ac..050262cc99 100644 --- a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs +++ b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs @@ -60,7 +60,7 @@ private void PlayerListItemPressed(BaseButton.ButtonEventArgs? args, ListData? d } else if (args.Event.Function == EngineKeyFunctions.UseSecondary && selectedPlayer.NetEntity != null) { - _uiManager.GetUIController().OpenVerbMenu(_entManager.GetEntity(selectedPlayer.NetEntity.Value)); + _uiManager.GetUIController().OpenVerbMenu(selectedPlayer.NetEntity.Value, true); } } @@ -122,7 +122,7 @@ private void GenerateButton(ListData data, ListContainerButton button) } } }); - button.EnableAllKeybinds = true; + button.AddStyleClass(ListContainer.StyleClassListContainerButton); } } 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(); @@ -30,7 +32,7 @@ public sealed partial class PlayerTab : Control 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); 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 5089022415..83327ad77b 100644 --- a/Content.Client/Alerts/ClientAlertsSystem.cs +++ b/Content.Client/Alerts/ClientAlertsSystem.cs @@ -2,6 +2,7 @@ using Content.Shared.Alert; using JetBrains.Annotations; using Robust.Client.Player; +using Robust.Shared.Player; using Robust.Shared.Prototypes; namespace Content.Client.Alerts; 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/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..0fc0c18b62 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"); @@ -79,11 +75,11 @@ 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); } } @@ -92,7 +88,7 @@ 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) @@ -129,8 +125,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 +159,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 +221,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/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/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/Construction/ConstructionSystem.cs b/Content.Client/Construction/ConstructionSystem.cs index 9fc638cea2..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 @@ -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; @@ -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/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/Disposal/Systems/DisposalUnitSystem.cs b/Content.Client/Disposal/Systems/DisposalUnitSystem.cs index d10101754c..344bd2ec97 100644 --- a/Content.Client/Disposal/Systems/DisposalUnitSystem.cs +++ b/Content.Client/Disposal/Systems/DisposalUnitSystem.cs @@ -6,6 +6,8 @@ using Robust.Client.GameObjects; using Robust.Client.Animations; using Robust.Client.Graphics; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; using Robust.Shared.GameStates; using Robust.Shared.Physics.Events; using static Content.Shared.Disposal.Components.SharedDisposalUnitComponent; diff --git a/Content.Client/DoAfter/DoAfterOverlay.cs b/Content.Client/DoAfter/DoAfterOverlay.cs index 1fc00a81b9..2957dafdb7 100644 --- a/Content.Client/DoAfter/DoAfterOverlay.cs +++ b/Content.Client/DoAfter/DoAfterOverlay.cs @@ -4,6 +4,7 @@ using Robust.Client.Graphics; using Robust.Shared.Enums; using Robust.Shared.Graphics; +using Robust.Client.Player; using Robust.Shared.Prototypes; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -14,6 +15,7 @@ public sealed class DoAfterOverlay : Overlay { private readonly IEntityManager _entManager; private readonly IGameTiming _timing; + private readonly IPlayerManager _player; private readonly SharedTransformSystem _transform; private readonly MetaDataSystem _meta; @@ -31,13 +33,14 @@ public sealed class DoAfterOverlay : Overlay public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV; - public DoAfterOverlay(IEntityManager entManager, IPrototypeManager protoManager, IGameTiming timing) + public DoAfterOverlay(IEntityManager entManager, IPrototypeManager protoManager, IGameTiming timing, IPlayerManager player) { _entManager = entManager; _timing = timing; + _player = player; _transform = _entManager.EntitySysManager.GetEntitySystem(); _meta = _entManager.EntitySysManager.GetEntitySystem(); - var sprite = new SpriteSpecifier.Rsi(new ("/Textures/Interface/Misc/progress_bar.rsi"), "icon"); + var sprite = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/progress_bar.rsi"), "icon"); _barTexture = _entManager.EntitySysManager.GetEntitySystem().Frame0(sprite); _shader = protoManager.Index("unshaded").Instance(); @@ -58,6 +61,7 @@ protected override void Draw(in OverlayDrawArgs args) var curTime = _timing.CurTime; var bounds = args.WorldAABB.Enlarged(5f); + var localEnt = _player.LocalSession?.AttachedEntity; var metaQuery = _entManager.GetEntityQuery(); var enumerator = _entManager.AllEntityQueryEnumerator(); @@ -88,6 +92,17 @@ protected override void Draw(in OverlayDrawArgs args) foreach (var doAfter in comp.DoAfters.Values) { + // Hide some DoAfters from other players for stealthy actions (ie: thieving gloves) + var alpha = 1f; + if (doAfter.Args.Hidden) + { + if (uid != localEnt) + continue; + + // Hints to the local player that this do-after is not visible to other players. + alpha = 0.5f; + } + // Use the sprite itself if we know its bounds. This means short or tall sprites don't get overlapped // by the bar. float yOffset = sprite.Bounds.Height / 2f + 0.05f; @@ -108,15 +123,15 @@ protected override void Draw(in OverlayDrawArgs args) { var elapsed = doAfter.CancelledTime.Value - doAfter.StartTime; elapsedRatio = (float) Math.Min(1, elapsed.TotalSeconds / doAfter.Args.Delay.TotalSeconds); - var cancelElapsed = (time - doAfter.CancelledTime.Value).TotalSeconds; + var cancelElapsed = (time - doAfter.CancelledTime.Value).TotalSeconds; var flash = Math.Floor(cancelElapsed / FlashTime) % 2 == 0; - color = new Color(1f, 0f, 0f, flash ? 1f : 0f); + color = new Color(1f, 0f, 0f, flash ? alpha : 0f); } else { var elapsed = time - doAfter.StartTime; elapsedRatio = (float) Math.Min(1, elapsed.TotalSeconds / doAfter.Args.Delay.TotalSeconds); - color = GetProgressColor(elapsedRatio); + color = GetProgressColor(elapsedRatio, alpha); } var xProgress = (EndX - StartX) * elapsedRatio + StartX; @@ -131,14 +146,14 @@ protected override void Draw(in OverlayDrawArgs args) handle.SetTransform(Matrix3.Identity); } - public static Color GetProgressColor(float progress) + public static Color GetProgressColor(float progress, float alpha = 1f) { if (progress >= 1.0f) { - return new Color(0f, 1f, 0f); + return new Color(0f, 1f, 0f, alpha); } // lerp var hue = (5f / 18f) * progress; - return Color.FromHsv((hue, 1f, 0.75f, 1f)); + return Color.FromHsv((hue, 1f, 0.75f, alpha)); } } diff --git a/Content.Client/DoAfter/DoAfterSystem.cs b/Content.Client/DoAfter/DoAfterSystem.cs index 8c2d8344d3..38e1a714ac 100644 --- a/Content.Client/DoAfter/DoAfterSystem.cs +++ b/Content.Client/DoAfter/DoAfterSystem.cs @@ -21,7 +21,7 @@ public sealed class DoAfterSystem : SharedDoAfterSystem public override void Initialize() { base.Initialize(); - _overlay.AddOverlay(new DoAfterOverlay(EntityManager, _prototype, GameTiming)); + _overlay.AddOverlay(new DoAfterOverlay(EntityManager, _prototype, GameTiming, _player)); } public override void Shutdown() diff --git a/Content.Client/Doors/DoorSystem.cs b/Content.Client/Doors/DoorSystem.cs index 4b82d3506a..80ce47e0c2 100644 --- a/Content.Client/Doors/DoorSystem.cs +++ b/Content.Client/Doors/DoorSystem.cs @@ -138,6 +138,6 @@ private void OnAppearanceChange(EntityUid uid, DoorComponent comp, ref Appearanc protected override void PlaySound(EntityUid uid, SoundSpecifier soundSpecifier, AudioParams audioParams, EntityUid? predictingPlayer, bool predicted) { if (GameTiming.InPrediction && GameTiming.IsFirstTimePredicted) - Audio.Play(soundSpecifier, Filter.Local(), uid, false, audioParams); + Audio.PlayEntity(soundSpecifier, Filter.Local(), uid, false, audioParams); } } diff --git a/Content.Client/Drugs/DrugOverlaySystem.cs b/Content.Client/Drugs/DrugOverlaySystem.cs index ec0d014072..3c42033be6 100644 --- a/Content.Client/Drugs/DrugOverlaySystem.cs +++ b/Content.Client/Drugs/DrugOverlaySystem.cs @@ -1,6 +1,7 @@ using Content.Shared.Drugs; using Robust.Client.Graphics; using Robust.Client.Player; +using Robust.Shared.Player; namespace Content.Client.Drugs; diff --git a/Content.Client/Drunk/DrunkSystem.cs b/Content.Client/Drunk/DrunkSystem.cs index 4f2ec70b56..dcd2758623 100644 --- a/Content.Client/Drunk/DrunkSystem.cs +++ b/Content.Client/Drunk/DrunkSystem.cs @@ -1,6 +1,7 @@ using Content.Shared.Drunk; using Robust.Client.Graphics; using Robust.Client.Player; +using Robust.Shared.Player; namespace Content.Client.Drunk; diff --git a/Content.Client/Explosion/ExplosionOverlaySystem.cs b/Content.Client/Explosion/ExplosionOverlaySystem.cs index 60208ea1a0..064b068a97 100644 --- a/Content.Client/Explosion/ExplosionOverlaySystem.cs +++ b/Content.Client/Explosion/ExplosionOverlaySystem.cs @@ -19,11 +19,6 @@ public sealed class ExplosionOverlaySystem : EntitySystem [Dependency] private readonly IOverlayManager _overlayMan = default!; [Dependency] private readonly SharedPointLightSystem _lights = default!; - /// - /// For how many seconds should an explosion stay on-screen once it has finished expanding? - /// - public float ExplosionPersistence = 0.3f; - public override void Initialize() { base.Initialize(); diff --git a/Content.Client/Eye/Blinding/BlindingSystem.cs b/Content.Client/Eye/Blinding/BlindingSystem.cs index f255f7ef01..2bff5db4d6 100644 --- a/Content.Client/Eye/Blinding/BlindingSystem.cs +++ b/Content.Client/Eye/Blinding/BlindingSystem.cs @@ -2,6 +2,7 @@ using Robust.Client.Player; using Content.Shared.Eye.Blinding.Components; using Content.Shared.GameTicking; +using Robust.Shared.Player; namespace Content.Client.Eye.Blinding; diff --git a/Content.Client/Eye/Blinding/BlurryVisionOverlay.cs b/Content.Client/Eye/Blinding/BlurryVisionOverlay.cs index 94590b54a5..7d0e7916da 100644 --- a/Content.Client/Eye/Blinding/BlurryVisionOverlay.cs +++ b/Content.Client/Eye/Blinding/BlurryVisionOverlay.cs @@ -54,7 +54,7 @@ protected override void Draw(in OverlayDrawArgs args) // Maybe gradually shrink the view-size? // Make the effect only apply to the edge of the viewport? // Actually make it blurry?? - var opacity = 0.75f * _magnitude / BlurryVisionComponent.MaxMagnitude; + var opacity = 1f * _magnitude / BlurryVisionComponent.MaxMagnitude; var worldHandle = args.WorldHandle; var viewport = args.WorldBounds; worldHandle.SetTransform(Matrix3.Identity); diff --git a/Content.Client/Eye/Blinding/BlurryVisionSystem.cs b/Content.Client/Eye/Blinding/BlurryVisionSystem.cs index 8be5b4ed93..91090fc460 100644 --- a/Content.Client/Eye/Blinding/BlurryVisionSystem.cs +++ b/Content.Client/Eye/Blinding/BlurryVisionSystem.cs @@ -1,6 +1,7 @@ using Content.Shared.Eye.Blinding.Components; using Robust.Client.Graphics; using Robust.Client.Player; +using Robust.Shared.Player; namespace Content.Client.Eye.Blinding; diff --git a/Content.Client/Eye/EyeLerpingSystem.cs b/Content.Client/Eye/EyeLerpingSystem.cs index b46921a9b4..8889b97100 100644 --- a/Content.Client/Eye/EyeLerpingSystem.cs +++ b/Content.Client/Eye/EyeLerpingSystem.cs @@ -5,6 +5,7 @@ using Robust.Client.GameObjects; using Robust.Client.Physics; using Robust.Client.Player; +using Robust.Shared.Player; using Robust.Shared.Timing; namespace Content.Client.Eye; diff --git a/Content.Client/Forensics/ForensicScannerMenu.xaml.cs b/Content.Client/Forensics/ForensicScannerMenu.xaml.cs index 84ffd7969e..8b6152c861 100644 --- a/Content.Client/Forensics/ForensicScannerMenu.xaml.cs +++ b/Content.Client/Forensics/ForensicScannerMenu.xaml.cs @@ -58,6 +58,12 @@ public void UpdateState(ForensicScannerBoundUserInterfaceState msg) { text.AppendLine(dna); } + text.AppendLine(); + text.AppendLine(Loc.GetString("forensic-scanner-interface-residues")); + foreach (var residue in msg.Residues) + { + text.AppendLine(residue); + } Diagnostics.Text = text.ToString(); } } diff --git a/Content.Client/GameTicking/Managers/ClientGameTicker.cs b/Content.Client/GameTicking/Managers/ClientGameTicker.cs index e363ae764b..a25b592f57 100644 --- a/Content.Client/GameTicking/Managers/ClientGameTicker.cs +++ b/Content.Client/GameTicking/Managers/ClientGameTicker.cs @@ -7,6 +7,8 @@ using JetBrains.Annotations; using Robust.Client.Graphics; using Robust.Client.State; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; using Robust.Shared.Configuration; using Robust.Shared.Player; using Robust.Shared.Utility; diff --git a/Content.Client/Gateway/UI/GatewayBoundUserInterface.cs b/Content.Client/Gateway/UI/GatewayBoundUserInterface.cs index 78e0060e9c..fdb3cdbc01 100644 --- a/Content.Client/Gateway/UI/GatewayBoundUserInterface.cs +++ b/Content.Client/Gateway/UI/GatewayBoundUserInterface.cs @@ -17,7 +17,8 @@ protected override void Open() { base.Open(); - _window = new GatewayWindow(); + _window = new GatewayWindow(EntMan.GetNetEntity(Owner)); + _window.OpenPortal += destination => { SendMessage(new GatewayOpenPortalMessage(destination)); diff --git a/Content.Client/Gateway/UI/GatewayWindow.xaml b/Content.Client/Gateway/UI/GatewayWindow.xaml index 49e6bb679b..7650850a70 100644 --- a/Content.Client/Gateway/UI/GatewayWindow.xaml +++ b/Content.Client/Gateway/UI/GatewayWindow.xaml @@ -3,11 +3,26 @@ Title="{Loc 'gateway-window-title'}" MinSize="800 360"> - - - + + + + [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/Medical/CrewMonitoring/CrewMonitoringBoundUserInterface.cs b/Content.Client/Medical/CrewMonitoring/CrewMonitoringBoundUserInterface.cs index fc632575c7..3978880987 100644 --- a/Content.Client/Medical/CrewMonitoring/CrewMonitoringBoundUserInterface.cs +++ b/Content.Client/Medical/CrewMonitoring/CrewMonitoringBoundUserInterface.cs @@ -1,53 +1,56 @@ using Content.Shared.Medical.CrewMonitoring; -using Robust.Client.GameObjects; -namespace Content.Client.Medical.CrewMonitoring +namespace Content.Client.Medical.CrewMonitoring; + +public sealed class CrewMonitoringBoundUserInterface : BoundUserInterface { - public sealed class CrewMonitoringBoundUserInterface : BoundUserInterface + [ViewVariables] + private CrewMonitoringWindow? _menu; + + public CrewMonitoringBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { - [ViewVariables] - private CrewMonitoringWindow? _menu; + } - public CrewMonitoringBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) - { - } + protected override void Open() + { + EntityUid? gridUid = null; + string stationName = string.Empty; - protected override void Open() + if (EntMan.TryGetComponent(Owner, out var xform)) { - EntityUid? gridUid = null; + gridUid = xform.GridUid; - if (EntMan.TryGetComponent(Owner, out var xform)) + if (EntMan.TryGetComponent(gridUid, out var metaData)) { - gridUid = xform.GridUid; + stationName = metaData.EntityName; } - - _menu = new CrewMonitoringWindow(gridUid); - - _menu.OpenCentered(); - _menu.OnClose += Close; } - protected override void UpdateState(BoundUserInterfaceState state) - { - base.UpdateState(state); + _menu = new CrewMonitoringWindow(stationName, gridUid); - switch (state) - { - case CrewMonitoringState st: - EntMan.TryGetComponent(Owner, out var xform); + _menu.OpenCentered(); + _menu.OnClose += Close; + } - _menu?.ShowSensors(st.Sensors, xform?.Coordinates, st.Snap, st.Precision); - break; - } - } + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); - protected override void Dispose(bool disposing) + switch (state) { - base.Dispose(disposing); - if (!disposing) - return; - - _menu?.Dispose(); + case CrewMonitoringState st: + EntMan.TryGetComponent(Owner, out var xform); + _menu?.ShowSensors(st.Sensors, Owner, xform?.Coordinates); + break; } } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!disposing) + return; + + _menu?.Dispose(); + } } diff --git a/Content.Client/Medical/CrewMonitoring/CrewMonitoringNavMapControl.cs b/Content.Client/Medical/CrewMonitoring/CrewMonitoringNavMapControl.cs new file mode 100644 index 0000000000..e6adf13bed --- /dev/null +++ b/Content.Client/Medical/CrewMonitoring/CrewMonitoringNavMapControl.cs @@ -0,0 +1,79 @@ +using Content.Client.Pinpointer.UI; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.Controls; + +namespace Content.Client.Medical.CrewMonitoring; + +public sealed partial class CrewMonitoringNavMapControl : NavMapControl +{ + public NetEntity? Focus; + public Dictionary LocalizedNames = new(); + + private Color _backgroundColor; + private Label _trackedEntityLabel; + private PanelContainer _trackedEntityPanel; + + public CrewMonitoringNavMapControl() : base() + { + WallColor = new Color(192, 122, 196); + TileColor = new(71, 42, 72); + + _backgroundColor = Color.FromSrgb(TileColor.WithAlpha(0.8f)); + + _trackedEntityLabel = new Label + { + Margin = new Thickness(10f, 8f), + HorizontalAlignment = HAlignment.Center, + VerticalAlignment = VAlignment.Center, + Modulate = Color.White, + }; + + _trackedEntityPanel = new PanelContainer + { + PanelOverride = new StyleBoxFlat + { + BackgroundColor = _backgroundColor, + }, + + Margin = new Thickness(5f, 10f), + HorizontalAlignment = HAlignment.Left, + VerticalAlignment = VAlignment.Bottom, + Visible = false, + }; + + _trackedEntityPanel.AddChild(_trackedEntityLabel); + this.AddChild(_trackedEntityPanel); + } + + protected override void Draw(DrawingHandleScreen handle) + { + base.Draw(handle); + + if (Focus == null) + { + _trackedEntityLabel.Text = string.Empty; + _trackedEntityPanel.Visible = false; + + return; + } + + foreach ((var netEntity, var blip) in TrackedEntities) + { + if (netEntity != Focus) + continue; + + if (!LocalizedNames.TryGetValue(netEntity, out var name)) + name = "Unknown"; + + var message = name + "\nLocation: [x = " + MathF.Round(blip.Coordinates.X) + ", y = " + MathF.Round(blip.Coordinates.Y) + "]"; + + _trackedEntityLabel.Text = message; + _trackedEntityPanel.Visible = true; + + return; + } + + _trackedEntityLabel.Text = string.Empty; + _trackedEntityPanel.Visible = false; + } +} diff --git a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml index 559a12d63b..80bf5a3f8b 100644 --- a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml +++ b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml @@ -1,39 +1,49 @@ - - - - - - - + + + + + + diff --git a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs index ff08af6bb6..d8c87899db 100644 --- a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs +++ b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs @@ -1,275 +1,437 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; +using Content.Client.Pinpointer.UI; using Content.Client.Stylesheets; using Content.Client.UserInterface.Controls; using Content.Shared.Medical.SuitSensor; +using Content.Shared.StatusIcon; using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Map; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; +using Robust.Shared.Utility; using static Robust.Client.UserInterface.Controls.BoxContainer; -namespace Content.Client.Medical.CrewMonitoring +namespace Content.Client.Medical.CrewMonitoring; + +[GenerateTypedNameReferences] +public sealed partial class CrewMonitoringWindow : FancyWindow { - [GenerateTypedNameReferences] - public sealed partial class CrewMonitoringWindow : FancyWindow + private List _rowsContent = new(); + private readonly IEntityManager _entManager; + private readonly IPrototypeManager _prototypeManager; + private readonly SpriteSystem _spriteSystem; + + private NetEntity? _trackedEntity; + private bool _tryToScrollToListFocus; + private Texture? _blipTexture; + + public CrewMonitoringWindow(string stationName, EntityUid? mapUid) + { + RobustXamlLoader.Load(this); + + _entManager = IoCManager.Resolve(); + _prototypeManager = IoCManager.Resolve(); + _spriteSystem = _entManager.System(); + + _blipTexture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png"))); + + if (_entManager.TryGetComponent(mapUid, out var xform)) + NavMap.MapUid = xform.GridUid; + + else + NavMap.Visible = false; + + StationName.AddStyleClass("LabelBig"); + StationName.Text = stationName; + + NavMap.TrackedEntitySelectedAction += SetTrackedEntityFromNavMap; + NavMap.ForceNavMapUpdate(); + } + + protected override void FrameUpdate(FrameEventArgs args) { - private List _rowsContent = new(); - private List<(DirectionIcon Icon, Vector2 Position)> _directionIcons = new(); - private readonly IEntityManager _entManager; - private readonly IEyeManager _eye; - private EntityUid? _stationUid; - private CrewMonitoringButton? _trackedButton; + base.FrameUpdate(args); - public static int IconSize = 16; // XAML has a `VSeparationOverride` of 20 for each row. + if (_tryToScrollToListFocus) + TryToScrollToFocus(); + } - public CrewMonitoringWindow(EntityUid? mapUid) + public void ShowSensors(List sensors, EntityUid monitor, EntityCoordinates? monitorCoords) + { + ClearOutDatedData(); + + // No server label + if (sensors.Count == 0) { - RobustXamlLoader.Load(this); - _eye = IoCManager.Resolve(); - _entManager = IoCManager.Resolve(); - _stationUid = mapUid; + NoServerLabel.Visible = true; + return; + } + + NoServerLabel.Visible = false; + + // Order sensor data + var orderedSensors = sensors.OrderBy(n => n.Name).OrderBy(j => j.Job); + var assignedSensors = new HashSet(); + var departments = sensors.SelectMany(d => d.JobDepartments).Distinct().OrderBy(n => n); - if (_entManager.TryGetComponent(mapUid, out var xform)) + // Create department labels and populate lists + foreach (var department in departments) + { + var departmentSensors = orderedSensors.Where(d => d.JobDepartments.Contains(department)); + + if (departmentSensors == null || !departmentSensors.Any()) + continue; + + foreach (var sensor in departmentSensors) + assignedSensors.Add(sensor); + + if (SensorsTable.ChildCount > 0) { - NavMap.MapUid = xform.GridUid; + var spacer = new Control() + { + SetHeight = 20, + }; + + SensorsTable.AddChild(spacer); + _rowsContent.Add(spacer); } - else + + var deparmentLabel = new RichTextLabel() { - NavMap.Visible = false; - SetSize = new Vector2(775, 400); - MinSize = SetSize; - } + Margin = new Thickness(10, 0), + HorizontalExpand = true, + }; + + deparmentLabel.SetMessage(department); + deparmentLabel.StyleClasses.Add(StyleNano.StyleClassTooltipActionDescription); + + SensorsTable.AddChild(deparmentLabel); + _rowsContent.Add(deparmentLabel); + + PopulateDepartmentList(departmentSensors); } - public void ShowSensors(List stSensors, EntityCoordinates? monitorCoords, bool snap, float precision) + // Account for any non-station users + var remainingSensors = orderedSensors.Except(assignedSensors); + + if (remainingSensors.Any()) { - ClearAllSensors(); + var spacer = new Control() + { + SetHeight = 20, + }; - var monitorCoordsInStationSpace = _stationUid != null ? monitorCoords?.WithEntityId(_stationUid.Value, _entManager).Position : null; + SensorsTable.AddChild(spacer); + _rowsContent.Add(spacer); - // TODO scroll container - // TODO filter by name & occupation - // TODO make each row a xaml-control. Get rid of some of this c# control creation. - if (stSensors.Count == 0) + var deparmentLabel = new RichTextLabel() { - NoServerLabel.Visible = true; - return; - } - NoServerLabel.Visible = false; + Margin = new Thickness(10, 0), + HorizontalExpand = true, + }; + + deparmentLabel.SetMessage(Loc.GetString("crew-monitoring-user-interface-no-department")); + deparmentLabel.StyleClasses.Add(StyleNano.StyleClassTooltipActionDescription); - // add a row for each sensor - foreach (var sensor in stSensors.OrderBy(a => a.Name)) + SensorsTable.AddChild(deparmentLabel); + _rowsContent.Add(deparmentLabel); + + PopulateDepartmentList(remainingSensors); + } + + // Show monitor on nav map + if (monitorCoords != null && _blipTexture != null) + { + NavMap.TrackedEntities[_entManager.GetNetEntity(monitor)] = new NavMapBlip(monitorCoords.Value, _blipTexture, Color.Cyan, true, false); + } + } + + private void PopulateDepartmentList(IEnumerable departmentSensors) + { + // Populate departments + foreach (var sensor in departmentSensors) + { + var coordinates = _entManager.GetCoordinates(sensor.Coordinates); + + // Add a button that will hold a username and other details + NavMap.LocalizedNames.TryAdd(sensor.SuitSensorUid, sensor.Name + ", " + sensor.Job); + + var sensorButton = new CrewMonitoringButton() { - var sensorEntity = _entManager.GetEntity(sensor.SuitSensorUid); - var coordinates = _entManager.GetCoordinates(sensor.Coordinates); + SuitSensorUid = sensor.SuitSensorUid, + Coordinates = coordinates, + Disabled = (coordinates == null), + HorizontalExpand = true, + }; - // add button with username - var nameButton = new CrewMonitoringButton() - { - SuitSensorUid = sensorEntity, - Coordinates = coordinates, - Text = sensor.Name, - Margin = new Thickness(5f, 5f), - }; - if (sensorEntity == _trackedButton?.SuitSensorUid) - nameButton.AddStyleClass(StyleNano.StyleClassButtonColorGreen); - SetColorLabel(nameButton.Label, sensor.TotalDamage, sensor.IsAlive); - SensorsTable.AddChild(nameButton); - _rowsContent.Add(nameButton); - - // add users job - // format: JobName - var jobLabel = new Label() - { - Text = sensor.Job, - HorizontalExpand = true - }; - SetColorLabel(jobLabel, sensor.TotalDamage, sensor.IsAlive); - SensorsTable.AddChild(jobLabel); - _rowsContent.Add(jobLabel); - - // add users status and damage - // format: IsAlive (TotalDamage) - var statusText = Loc.GetString(sensor.IsAlive ? - "crew-monitoring-user-interface-alive" : - "crew-monitoring-user-interface-dead"); - if (sensor.TotalDamage != null) - { - statusText += $" ({sensor.TotalDamage})"; - } - var statusLabel = new Label() - { - Text = statusText - }; - SetColorLabel(statusLabel, sensor.TotalDamage, sensor.IsAlive); - SensorsTable.AddChild(statusLabel); - _rowsContent.Add(statusLabel); + if (sensor.SuitSensorUid == _trackedEntity) + sensorButton.AddStyleClass(StyleNano.StyleClassButtonColorGreen); - // add users positions - // format: (x, y) - var box = GetPositionBox(sensor, monitorCoordsInStationSpace ?? Vector2.Zero, snap, precision); + SensorsTable.AddChild(sensorButton); + _rowsContent.Add(sensorButton); - SensorsTable.AddChild(box); - _rowsContent.Add(box); + // Primary container to hold the button UI elements + var mainContainer = new BoxContainer() + { + Orientation = LayoutOrientation.Horizontal, + HorizontalExpand = true, + }; - if (coordinates != null && NavMap.Visible) - { - NavMap.TrackedCoordinates.TryAdd(coordinates.Value, - (true, sensorEntity == _trackedButton?.SuitSensorUid ? StyleNano.PointGreen : StyleNano.PointRed)); + sensorButton.AddChild(mainContainer); - nameButton.OnButtonUp += args => - { - if (_trackedButton != null && _trackedButton?.Coordinates != null) - //Make previous point red - NavMap.TrackedCoordinates[_trackedButton.Coordinates.Value] = (true, StyleNano.PointRed); + // User status container + var statusContainer = new BoxContainer() + { + SizeFlagsStretchRatio = 1.25f, + Orientation = LayoutOrientation.Horizontal, + HorizontalExpand = true, + }; - NavMap.TrackedCoordinates[coordinates.Value] = (true, StyleNano.PointGreen); - NavMap.CenterToCoordinates(coordinates.Value); + mainContainer.AddChild(statusContainer); - nameButton.AddStyleClass(StyleNano.StyleClassButtonColorGreen); - if (_trackedButton != null) - { //Make previous button default - var previosButton = SensorsTable.GetChild(_trackedButton.IndexInTable); - previosButton.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen); - } - _trackedButton = nameButton; - _trackedButton.IndexInTable = nameButton.GetPositionInParent(); - }; - } - } - // Show monitor point - if (monitorCoords != null) - NavMap.TrackedCoordinates.Add(monitorCoords.Value, (true, StyleNano.PointMagenta)); - } + // Suit coords indicator + var suitCoordsIndicator = new TextureRect() + { + Texture = _blipTexture, + TextureScale = new Vector2(0.25f, 0.25f), + Modulate = coordinates != null ? Color.LimeGreen : Color.DarkRed, + HorizontalAlignment = HAlignment.Center, + VerticalAlignment = VAlignment.Center, + }; - private BoxContainer GetPositionBox(SuitSensorStatus sensor, Vector2 monitorCoordsInStationSpace, bool snap, float precision) - { - EntityCoordinates? coordinates = _entManager.GetCoordinates(sensor.Coordinates); - var box = new BoxContainer() { Orientation = LayoutOrientation.Horizontal }; + statusContainer.AddChild(suitCoordsIndicator); + + // Specify texture for the user status icon + var specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "alive"); - if (coordinates == null || _stationUid == null) + if (!sensor.IsAlive) { - var dirIcon = new DirectionIcon() - { - SetSize = new Vector2(IconSize, IconSize), - Margin = new(0, 0, 4, 0) - }; - box.AddChild(dirIcon); - box.AddChild(new Label() { Text = Loc.GetString("crew-monitoring-user-interface-no-info") }); + specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "dead"); } - else + + else if (sensor.TotalDamage != null) { - var local = coordinates.Value.WithEntityId(_stationUid.Value, _entManager).Position; + var index = MathF.Round(4f * (sensor.TotalDamage.Value / 100f)); - var displayPos = local.Floored(); - var dirIcon = new DirectionIcon(snap, precision) - { - SetSize = new Vector2(IconSize, IconSize), - Margin = new(0, 0, 4, 0) - }; - box.AddChild(dirIcon); - Label label = new Label() { Text = displayPos.ToString() }; - SetColorLabel(label, sensor.TotalDamage, sensor.IsAlive); - box.AddChild(label); - _directionIcons.Add((dirIcon, local - monitorCoordsInStationSpace)); + if (index >= 5) + specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "critical"); + + else + specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "health" + index); } - return box; - } + // Status icon + var statusIcon = new AnimatedTextureRect + { + HorizontalAlignment = HAlignment.Center, + VerticalAlignment = VAlignment.Center, + Margin = new Thickness(0, 1, 3, 0), + }; - protected override void FrameUpdate(FrameEventArgs args) - { - // the window is separate from any specific viewport, so there is no real way to get an eye-rotation without - // using IEyeManager. Eventually this will have to be reworked for a station AI with multi-viewports. - // (From the future: Or alternatively, just disable the angular offset for station AIs?) - - // An offsetAngle of zero here perfectly aligns directions to the station map. - // Note that the "relative angle" does this weird inverse-inverse thing. - // Could recalculate it all in world coordinates and then pass in eye directly... or do this. - var offsetAngle = Angle.Zero; - if (_entManager.TryGetComponent(_stationUid, out var xform)) + statusIcon.SetFromSpriteSpecifier(specifier); + statusIcon.DisplayRect.TextureScale = new Vector2(2f, 2f); + + statusContainer.AddChild(statusIcon); + + // User name + var nameLabel = new Label() + { + Text = sensor.Name, + HorizontalExpand = true, + ClipText = true, + }; + + statusContainer.AddChild(nameLabel); + + // User job container + var jobContainer = new BoxContainer() + { + Orientation = LayoutOrientation.Horizontal, + HorizontalExpand = true, + }; + + mainContainer.AddChild(jobContainer); + + // Job icon + if (_prototypeManager.TryIndex(sensor.JobIcon, out var proto)) { - // Apply the offset relative to the eye. - // For a station at 45 degrees rotation, the current eye rotation is -45 degrees. - // TODO: This feels sketchy. Is there something underlying wrong with eye rotation? - offsetAngle = -(_eye.CurrentEye.Rotation + xform.WorldRotation); + var jobIcon = new TextureRect() + { + TextureScale = new Vector2(2f, 2f), + Stretch = TextureRect.StretchMode.KeepCentered, + Texture = _spriteSystem.Frame0(proto.Icon), + Margin = new Thickness(5, 0, 5, 0), + }; + + jobContainer.AddChild(jobIcon); } - foreach (var (icon, pos) in _directionIcons) + // Job name + var jobLabel = new Label() { - icon.UpdateDirection(pos, offsetAngle); + Text = sensor.Job, + HorizontalExpand = true, + ClipText = true, + }; + + jobContainer.AddChild(jobLabel); + + // Add user coordinates to the navmap + if (coordinates != null && NavMap.Visible && _blipTexture != null) + { + NavMap.TrackedEntities.TryAdd(sensor.SuitSensorUid, + new NavMapBlip + (coordinates.Value, + _blipTexture, + (_trackedEntity == null || sensor.SuitSensorUid == _trackedEntity) ? Color.LimeGreen : Color.LimeGreen * Color.DimGray, + sensor.SuitSensorUid == _trackedEntity)); + + NavMap.Focus = _trackedEntity; + + // On button up + sensorButton.OnButtonUp += args => + { + var prevTrackedEntity = _trackedEntity; + + if (_trackedEntity == sensor.SuitSensorUid) + { + _trackedEntity = null; + } + + else + { + _trackedEntity = sensor.SuitSensorUid; + NavMap.CenterToCoordinates(coordinates.Value); + } + + NavMap.Focus = _trackedEntity; + + UpdateSensorsTable(_trackedEntity, prevTrackedEntity); + }; } } + } + + private void SetTrackedEntityFromNavMap(NetEntity? netEntity) + { + var prevTrackedEntity = _trackedEntity; + _trackedEntity = netEntity; + + if (_trackedEntity == prevTrackedEntity) + prevTrackedEntity = null; + + NavMap.Focus = _trackedEntity; + _tryToScrollToListFocus = true; + + UpdateSensorsTable(_trackedEntity, prevTrackedEntity); + } - private void ClearAllSensors() + private void UpdateSensorsTable(NetEntity? currTrackedEntity, NetEntity? prevTrackedEntity) + { + foreach (var sensor in SensorsTable.Children) { - foreach (var child in _rowsContent) + if (sensor is not CrewMonitoringButton) + continue; + + var castSensor = (CrewMonitoringButton) sensor; + + if (castSensor.SuitSensorUid == prevTrackedEntity) + castSensor.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen); + + else if (castSensor.SuitSensorUid == currTrackedEntity) + castSensor.AddStyleClass(StyleNano.StyleClassButtonColorGreen); + + if (castSensor?.Coordinates == null) + continue; + + if (NavMap.TrackedEntities.TryGetValue(castSensor.SuitSensorUid, out var data)) { - SensorsTable.RemoveChild(child); + data = new NavMapBlip + (data.Coordinates, + data.Texture, + (currTrackedEntity == null || castSensor.SuitSensorUid == currTrackedEntity) ? Color.LimeGreen : Color.LimeGreen * Color.DimGray, + castSensor.SuitSensorUid == currTrackedEntity); + + NavMap.TrackedEntities[castSensor.SuitSensorUid] = data; } - _rowsContent.Clear(); - _directionIcons.Clear(); - NavMap.TrackedCoordinates.Clear(); } + } - private void SetColorLabel(Label label, int? totalDamage, bool isAlive) + private void TryToScrollToFocus() + { + if (!_tryToScrollToListFocus) + return; + + if (!TryGetVerticalScrollbar(SensorScroller, out var vScrollbar)) + return; + + if (TryGetNextScrollPosition(out float? nextScrollPosition)) { - var startColor = Color.White; - var critColor = Color.Yellow; - var endColor = Color.Red; + vScrollbar.ValueTarget = nextScrollPosition.Value; - if (!isAlive) + if (MathHelper.CloseToPercent(vScrollbar.Value, vScrollbar.ValueTarget)) { - label.FontColorOverride = endColor; + _tryToScrollToListFocus = false; return; } + } + } - //Convert from null to regular int - int damage; - if (totalDamage == null) return; - else damage = (int) totalDamage; + private bool TryGetVerticalScrollbar(ScrollContainer scroll, [NotNullWhen(true)] out VScrollBar? vScrollBar) + { + vScrollBar = null; - if (damage <= 0) - { - label.FontColorOverride = startColor; - } - else if (damage >= 200) - { - label.FontColorOverride = endColor; - } - else if (damage >= 0 && damage <= 100) - { - label.FontColorOverride = GetColorLerp(startColor, critColor, damage); - } - else if (damage >= 100 && damage <= 200) - { - //We need a number from 0 to 100. Divide the number from 100 to 200 by 2 - damage /= 2; - label.FontColorOverride = GetColorLerp(critColor, endColor, damage); - } + foreach (var child in scroll.Children) + { + if (child is not VScrollBar) + continue; + + vScrollBar = (VScrollBar) child; + return true; } - private Color GetColorLerp(Color startColor, Color endColor, int damage) + return false; + } + + private bool TryGetNextScrollPosition([NotNullWhen(true)] out float? nextScrollPosition) + { + nextScrollPosition = 0; + + foreach (var sensor in SensorsTable.Children) { - //Smooth transition from one color to another depending on the percentage - var t = damage / 100f; - var r = MathHelper.Lerp(startColor.R, endColor.R, t); - var g = MathHelper.Lerp(startColor.G, endColor.G, t); - var b = MathHelper.Lerp(startColor.B, endColor.B, t); - var a = MathHelper.Lerp(startColor.A, endColor.A, t); - - return new Color(r, g, b, a); + if (sensor is CrewMonitoringButton && + ((CrewMonitoringButton) sensor).SuitSensorUid == _trackedEntity) + return true; + + nextScrollPosition += sensor.Height; } + + // Failed to find control + nextScrollPosition = null; + + return false; } - public sealed class CrewMonitoringButton : Button + private void ClearOutDatedData() { - public int IndexInTable; - public EntityUid? SuitSensorUid; - public EntityCoordinates? Coordinates; + SensorsTable.RemoveAllChildren(); + _rowsContent.Clear(); + NavMap.TrackedCoordinates.Clear(); + NavMap.TrackedEntities.Clear(); + NavMap.LocalizedNames.Clear(); } } + +public sealed class CrewMonitoringButton : Button +{ + public int IndexInTable; + public NetEntity SuitSensorUid; + public EntityCoordinates? Coordinates; +} diff --git a/Content.Client/Movement/Systems/ContentEyeSystem.cs b/Content.Client/Movement/Systems/ContentEyeSystem.cs index 4056f5fd85..d8f73b0057 100644 --- a/Content.Client/Movement/Systems/ContentEyeSystem.cs +++ b/Content.Client/Movement/Systems/ContentEyeSystem.cs @@ -30,14 +30,18 @@ public void RequestToggleFov() public void RequestToggleFov(EntityUid uid, EyeComponent? eye = null) { if (Resolve(uid, ref eye, false)) - RequestFov(!eye.DrawFov); + RequestEye(!eye.DrawFov, eye.DrawLight); } - public void RequestFov(bool value) + public void RequestToggleLight(EntityUid uid, EyeComponent? eye = null) { - RaisePredictiveEvent(new RequestFovEvent() - { - Fov = value, - }); + if (Resolve(uid, ref eye, false)) + RequestEye(eye.DrawFov, !eye.DrawLight); + } + + + public void RequestEye(bool drawFov, bool drawLight) + { + RaisePredictiveEvent(new RequestEyeEvent(drawFov, drawLight)); } } diff --git a/Content.Client/Options/UI/OptionsMenu.xaml b/Content.Client/Options/UI/OptionsMenu.xaml index 5d028879fe..ab3b88ca4e 100644 --- a/Content.Client/Options/UI/OptionsMenu.xaml +++ b/Content.Client/Options/UI/OptionsMenu.xaml @@ -3,6 +3,7 @@ Title="{Loc 'ui-options-title'}" MinSize="800 450"> + diff --git a/Content.Client/Options/UI/OptionsMenu.xaml.cs b/Content.Client/Options/UI/OptionsMenu.xaml.cs index 1a924d2af1..c3a8e66470 100644 --- a/Content.Client/Options/UI/OptionsMenu.xaml.cs +++ b/Content.Client/Options/UI/OptionsMenu.xaml.cs @@ -15,10 +15,11 @@ public OptionsMenu() RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); - Tabs.SetTabTitle(0, Loc.GetString("ui-options-tab-graphics")); - Tabs.SetTabTitle(1, Loc.GetString("ui-options-tab-controls")); - Tabs.SetTabTitle(2, Loc.GetString("ui-options-tab-audio")); - Tabs.SetTabTitle(3, Loc.GetString("ui-options-tab-network")); + Tabs.SetTabTitle(0, Loc.GetString("ui-options-tab-misc")); + Tabs.SetTabTitle(1, Loc.GetString("ui-options-tab-graphics")); + Tabs.SetTabTitle(2, Loc.GetString("ui-options-tab-controls")); + Tabs.SetTabTitle(3, Loc.GetString("ui-options-tab-audio")); + Tabs.SetTabTitle(4, Loc.GetString("ui-options-tab-network")); UpdateTabs(); } diff --git a/Content.Client/Options/UI/Tabs/AudioTab.xaml.cs b/Content.Client/Options/UI/Tabs/AudioTab.xaml.cs index 5875c4a33a..80e13d4a43 100644 --- a/Content.Client/Options/UI/Tabs/AudioTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/AudioTab.xaml.cs @@ -1,10 +1,14 @@ +using Content.Client.Audio; using Content.Shared.CCVar; +using Robust.Client.Audio; using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared; +using Robust.Shared.Audio.Systems; using Robust.Shared.Configuration; using Range = Robust.Client.UserInterface.Controls.Range; @@ -14,13 +18,14 @@ namespace Content.Client.Options.UI.Tabs public sealed partial class AudioTab : Control { [Dependency] private readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly IClydeAudio _clydeAudio = default!; + private readonly IAudioManager _audio; public AudioTab() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); + _audio = IoCManager.Resolve(); LobbyMusicCheckBox.Pressed = _cfg.GetCVar(CCVars.LobbyMusicEnabled); RestartSoundsCheckBox.Pressed = _cfg.GetCVar(CCVars.RestartSoundsEnabled); EventMusicCheckBox.Pressed = _cfg.GetCVar(CCVars.EventMusicEnabled); @@ -79,7 +84,7 @@ private void OnAmbienceSoundsSliderChanged(Range obj) private void OnMasterVolumeSliderChanged(Range range) { - _clydeAudio.SetMasterVolume(MasterVolumeSlider.Value / 100); + _audio.SetMasterGain(MasterVolumeSlider.Value / 100f * ContentAudioSystem.MasterVolumeMultiplier); UpdateChanges(); } @@ -108,15 +113,16 @@ private void OnAdminSoundsCheckToggled(BaseButton.ButtonEventArgs args) private void OnApplyButtonPressed(BaseButton.ButtonEventArgs args) { - _cfg.SetCVar(CVars.AudioMasterVolume, MasterVolumeSlider.Value / 100); + _cfg.SetCVar(CVars.AudioMasterVolume, MasterVolumeSlider.Value / 100f * ContentAudioSystem.MasterVolumeMultiplier); // Want the CVar updated values to have the multiplier applied // For the UI we just display 0-100 still elsewhere - _cfg.SetCVar(CVars.MidiVolume, LV100ToDB(MidiVolumeSlider.Value, CCVars.MidiMultiplier)); - _cfg.SetCVar(CCVars.AmbienceVolume, LV100ToDB(AmbienceVolumeSlider.Value, CCVars.AmbienceMultiplier)); - _cfg.SetCVar(CCVars.AmbientMusicVolume, LV100ToDB(AmbientMusicVolumeSlider.Value, CCVars.AmbientMusicMultiplier)); + _cfg.SetCVar(CVars.MidiVolume, MidiVolumeSlider.Value / 100f * ContentAudioSystem.MidiVolumeMultiplier); + _cfg.SetCVar(CCVars.AmbienceVolume, AmbienceVolumeSlider.Value / 100f * ContentAudioSystem.AmbienceMultiplier); + _cfg.SetCVar(CCVars.AmbientMusicVolume, AmbientMusicVolumeSlider.Value / 100f * ContentAudioSystem.AmbientMusicMultiplier); + _cfg.SetCVar(CCVars.LobbyMusicVolume, LobbyVolumeSlider.Value / 100f * ContentAudioSystem.LobbyMultiplier); - _cfg.SetCVar(CCVars.LobbyMusicVolume, LV100ToDB(LobbyVolumeSlider.Value)); _cfg.SetCVar(CCVars.MaxAmbientSources, (int)AmbienceSoundsSlider.Value); + _cfg.SetCVar(CCVars.LobbyMusicEnabled, LobbyMusicCheckBox.Pressed); _cfg.SetCVar(CCVars.RestartSoundsEnabled, RestartSoundsCheckBox.Pressed); _cfg.SetCVar(CCVars.EventMusicEnabled, EventMusicCheckBox.Pressed); @@ -132,13 +138,14 @@ private void OnResetButtonPressed(BaseButton.ButtonEventArgs args) private void Reset() { - MasterVolumeSlider.Value = _cfg.GetCVar(CVars.AudioMasterVolume) * 100; - MidiVolumeSlider.Value = DBToLV100(_cfg.GetCVar(CVars.MidiVolume), CCVars.MidiMultiplier); - AmbienceVolumeSlider.Value = DBToLV100(_cfg.GetCVar(CCVars.AmbienceVolume), CCVars.AmbienceMultiplier); - AmbientMusicVolumeSlider.Value = - DBToLV100(_cfg.GetCVar(CCVars.AmbientMusicVolume), CCVars.AmbientMusicMultiplier); - LobbyVolumeSlider.Value = DBToLV100(_cfg.GetCVar(CCVars.LobbyMusicVolume)); + MasterVolumeSlider.Value = _cfg.GetCVar(CVars.AudioMasterVolume) * 100f / ContentAudioSystem.MasterVolumeMultiplier; + MidiVolumeSlider.Value = _cfg.GetCVar(CVars.MidiVolume) * 100f / ContentAudioSystem.MidiVolumeMultiplier; + AmbienceVolumeSlider.Value = _cfg.GetCVar(CCVars.AmbienceVolume) * 100f / ContentAudioSystem.AmbienceMultiplier; + AmbientMusicVolumeSlider.Value = _cfg.GetCVar(CCVars.AmbientMusicVolume) * 100f / ContentAudioSystem.AmbientMusicMultiplier; + LobbyVolumeSlider.Value = _cfg.GetCVar(CCVars.LobbyMusicVolume) * 100f / ContentAudioSystem.LobbyMultiplier; + AmbienceSoundsSlider.Value = _cfg.GetCVar(CCVars.MaxAmbientSources); + LobbyMusicCheckBox.Pressed = _cfg.GetCVar(CCVars.LobbyMusicEnabled); RestartSoundsCheckBox.Pressed = _cfg.GetCVar(CCVars.RestartSoundsEnabled); EventMusicCheckBox.Pressed = _cfg.GetCVar(CCVars.EventMusicEnabled); @@ -146,33 +153,20 @@ private void Reset() UpdateChanges(); } - // Note: Rather than moving these functions somewhere, instead switch MidiManager to using linear units rather than dB - // Do be sure to rename the setting though - private float DBToLV100(float db, float multiplier = 1f) - { - var weh = (float) (Math.Pow(10, db / 10) * 100 / multiplier); - return weh; - } - - private float LV100ToDB(float lv100, float multiplier = 1f) - { - // Saving negative infinity doesn't work, so use -10000000 instead (MidiManager does it) - var weh = MathF.Max(-10000000, (float) (Math.Log(lv100 * multiplier / 100, 10) * 10)); - return weh; - } - private void UpdateChanges() { + // y'all need jesus. var isMasterVolumeSame = - Math.Abs(MasterVolumeSlider.Value - _cfg.GetCVar(CVars.AudioMasterVolume) * 100) < 0.01f; + Math.Abs(MasterVolumeSlider.Value - _cfg.GetCVar(CVars.AudioMasterVolume) * 100f / ContentAudioSystem.MasterVolumeMultiplier) < 0.01f; var isMidiVolumeSame = - Math.Abs(MidiVolumeSlider.Value - DBToLV100(_cfg.GetCVar(CVars.MidiVolume), CCVars.MidiMultiplier)) < 0.01f; + Math.Abs(MidiVolumeSlider.Value - _cfg.GetCVar(CVars.MidiVolume) * 100f / ContentAudioSystem.MidiVolumeMultiplier) < 0.01f; var isAmbientVolumeSame = - Math.Abs(AmbienceVolumeSlider.Value - DBToLV100(_cfg.GetCVar(CCVars.AmbienceVolume), CCVars.AmbienceMultiplier)) < 0.01f; + Math.Abs(AmbienceVolumeSlider.Value - _cfg.GetCVar(CCVars.AmbienceVolume) * 100f / ContentAudioSystem.AmbienceMultiplier) < 0.01f; var isAmbientMusicVolumeSame = - Math.Abs(AmbientMusicVolumeSlider.Value - DBToLV100(_cfg.GetCVar(CCVars.AmbientMusicVolume), CCVars.AmbientMusicMultiplier)) < 0.01f; + Math.Abs(AmbientMusicVolumeSlider.Value - _cfg.GetCVar(CCVars.AmbientMusicVolume) * 100f / ContentAudioSystem.AmbientMusicMultiplier) < 0.01f; var isLobbyVolumeSame = - Math.Abs(LobbyVolumeSlider.Value - DBToLV100(_cfg.GetCVar(CCVars.LobbyMusicVolume))) < 0.01f; + Math.Abs(LobbyVolumeSlider.Value - _cfg.GetCVar(CCVars.LobbyMusicVolume) * 100f / ContentAudioSystem.LobbyMultiplier) < 0.01f; + var isAmbientSoundsSame = (int)AmbienceSoundsSlider.Value == _cfg.GetCVar(CCVars.MaxAmbientSources); var isLobbySame = LobbyMusicCheckBox.Pressed == _cfg.GetCVar(CCVars.LobbyMusicEnabled); var isRestartSoundsSame = RestartSoundsCheckBox.Pressed == _cfg.GetCVar(CCVars.RestartSoundsEnabled); diff --git a/Content.Client/Options/UI/Tabs/GraphicsTab.xaml b/Content.Client/Options/UI/Tabs/GraphicsTab.xaml index 41b304417a..74a0c78c52 100644 --- a/Content.Client/Options/UI/Tabs/GraphicsTab.xaml +++ b/Content.Client/Options/UI/Tabs/GraphicsTab.xaml @@ -20,8 +20,6 @@ - - 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 + + + + +