diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1adc0b803df..69c5f0138ff 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -25,17 +25,7 @@ /Resources/Prototypes/Entities/Mobs/Species/ @DrSmugleaf /Content.*/Body/ @DrSmugleaf /Content.YAMLLinter @DrSmugleaf - -/Content.*/Verbs/ @ElectroJr -/Content.Client/ContextMenu/ @ElectroJr -/Content.Shared/Damage/ @ElectroJr @DrSmugleaf -/Content.Shared/Containers/ItemSlot/ @ElectroJr -/Content.*/PowerCell/ @ElectroJr -/Content.*/Doors/ @ElectroJr -/Content.*/Interaction/ @ElectroJr -/Content.*/Hands/ @ElectroJr -/Content.*/Actions/ @ElectroJr -/Content.*/Explosion/ @ElectroJr +/Content.Shared/Damage/ @DrSmugleaf /Content.*/Anomaly/ @EmoGarbage404 /Content.*/Lathe/ @EmoGarbage404 diff --git a/.github/workflows/build-docfx.yml b/.github/workflows/build-docfx.yml index 1c4b543743d..ca1a6f0af12 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 e921bd2558c..35aed1a7f7f 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 9abd4fbe17e..47f9fd1a514 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 5f79c32a1a8..b156fb5be4d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -27,7 +27,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: Get Engine Tag run: | diff --git a/.github/workflows/test-packaging.yml b/.github/workflows/test-packaging.yml index a826b1f0593..e3cd5159d5d 100644 --- a/.github/workflows/test-packaging.yml +++ b/.github/workflows/test-packaging.yml @@ -47,43 +47,24 @@ jobs: run: | cd RobustToolbox/ git submodule update --init --recursive - - # Corvax-Secrets-Start - - name: Setup secrets - env: - SSH_KEY: ${{ secrets.SECRETS_PRIVATE_KEY }} - if: ${{ env.SSH_KEY != '' }} - run: | - mkdir ~/.ssh - echo "${{ secrets.SECRETS_PRIVATE_KEY }}" > ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa - echo "HOST *" > ~/.ssh/config - echo "StrictHostKeyChecking no" >> ~/.ssh/config - git -c submodule.Secrets.update=checkout submodule update --init - # Corvax-Secrets-End + - name: Setup .NET Core uses: actions/setup-dotnet@v3.2.0 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x - - name: Remove Dotnet 8 - run: sudo apt remove --purge dotnet-sdk-8.0 - - - name: Test Dotnet - run: dotnet --info - - name: Install dependencies run: dotnet restore - name: Build Packaging - run: dotnet build Content.Packaging --configuration Release --no-restore -f net7.0 /m + run: dotnet build Content.Packaging --configuration Release --no-restore /m - name: Package server - run: dotnet run --project Content.Packaging server --platform linux-x64 -f net7.0 + run: dotnet run --project Content.Packaging server --platform linux-x64 - name: Package client - run: dotnet run --project Content.Packaging client --no-wipe-release -f net7.0 - + 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 09844f4c19d..e35d4def7d8 100644 --- a/.github/workflows/update-credits.yml +++ b/.github/workflows/update-credits.yml @@ -24,9 +24,29 @@ jobs: # TODO #- name: Get this week's Patreons # 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 + + + # 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: Commit new credit files - uses: stefanzweifel/git-auto-commit-action@v4 + - name: Create Pull Request + uses: peter-evans/create-pull-request@v5 with: - commit_message: Update Credits - commit_author: PJBot + 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: PJBot + branch: automated/credits-${{env.NOW}} diff --git a/.github/workflows/yaml-linter.yml b/.github/workflows/yaml-linter.yml index 8798c95e19a..dfe497b100c 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 a8b255c71b0..049d6f5b6f4 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 00000000000..cef6a5e35c5 --- /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 b67c1bd5b97..508f3404bac 100644 --- a/Content.Client/Actions/ActionsSystem.cs +++ b/Content.Client/Actions/ActionsSystem.cs @@ -87,6 +87,8 @@ 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; diff --git a/Content.Client/Actions/UI/ActionAlertTooltip.cs b/Content.Client/Actions/UI/ActionAlertTooltip.cs index f48350d7722..ddc498b6e91 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 39749f8ac6f..050262cc991 100644 --- a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs +++ b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs @@ -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 92d5278ab5c..0f6975e3656 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"> + 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 7f358bf42ef..6278c18b7a5 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/Atmos/EntitySystems/AtmosDebugOverlaySystem.cs b/Content.Client/Atmos/EntitySystems/AtmosDebugOverlaySystem.cs index c849abf70ef..b63d274bdca 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 72adf276bff..fcf3b04e530 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 10065b6583e..d66ee434a29 100644 --- a/Content.Client/Audio/AmbientSoundSystem.cs +++ b/Content.Client/Audio/AmbientSoundSystem.cs @@ -101,7 +101,7 @@ 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); } @@ -116,9 +116,9 @@ private void OnShutdown(EntityUid uid, AmbientSoundComponent component, Componen _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) { @@ -141,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) diff --git a/Content.Client/Audio/BackgroundAudioSystem.cs b/Content.Client/Audio/BackgroundAudioSystem.cs index a26603bf746..09ac1efcd65 100644 --- a/Content.Client/Audio/BackgroundAudioSystem.cs +++ b/Content.Client/Audio/BackgroundAudioSystem.cs @@ -14,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!; @@ -22,7 +25,7 @@ public sealed class BackgroundAudioSystem : EntitySystem private readonly AudioParams _lobbyParams = new(-5f, 1, "Master", 0, 0, 0, true, 0f); - private EntityUid? _lobbyStream; + public EntityUid? LobbyStream; public override void Initialize() { @@ -109,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; @@ -118,12 +121,12 @@ public void StartLobbyMusic() return; } - _lobbyStream = _audio.PlayGlobal(file, Filter.Local(), false, - _lobbyParams.WithVolume(_lobbyParams.Volume + _configManager.GetCVar(CCVars.LobbyMusicVolume)))?.Entity; + LobbyStream = _audio.PlayGlobal(file, Filter.Local(), false, + _lobbyParams.WithVolume(_lobbyParams.Volume + SharedAudioSystem.GainToVolume(_configManager.GetCVar(CCVars.LobbyMusicVolume))))?.Entity; } private void EndLobbyMusic() { - _lobbyStream = _audio.Stop(_lobbyStream); + LobbyStream = _audio.Stop(LobbyStream); } } diff --git a/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs b/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs index 1860d81118c..0fc0c18b62b 100644 --- a/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs +++ b/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs @@ -75,7 +75,7 @@ private void InitializeAmbientMusic() private void AmbienceCVarChanged(float obj) { - _volumeSlider = obj; + _volumeSlider = SharedAudioSystem.GainToVolume(obj); if (_ambientMusicStream != null && _musicProto != null) { @@ -159,7 +159,7 @@ 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; diff --git a/Content.Client/Audio/ContentAudioSystem.cs b/Content.Client/Audio/ContentAudioSystem.cs index 726493fdab1..603b1086d8b 100644 --- a/Content.Client/Audio/ContentAudioSystem.cs +++ b/Content.Client/Audio/ContentAudioSystem.cs @@ -1,5 +1,8 @@ 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; @@ -18,11 +21,41 @@ public sealed partial class ContentAudioSystem : SharedContentAudioSystem 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() diff --git a/Content.Client/Chat/UI/SpeechBubble.cs b/Content.Client/Chat/UI/SpeechBubble.cs index fcb6938e63a..91e8e5a90f0 100644 --- a/Content.Client/Chat/UI/SpeechBubble.cs +++ b/Content.Client/Chat/UI/SpeechBubble.cs @@ -1,8 +1,11 @@ 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; @@ -10,6 +13,10 @@ 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, @@ -34,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; @@ -49,38 +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(text, senderEntity, eyeManager, chatManager, entityManager, "sayBox", Color.FromHex("#48d1cc")); + 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, Color? fontColor = null) + 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, fontColor); + var bubble = BuildBubble(message, speechStyleClass, fontColor); AddChild(bubble); @@ -91,7 +98,7 @@ public SpeechBubble(string text, EntityUid senderEntity, IEyeManager eyeManager, _verticalOffsetAchieved = -ContentSize.Y; } - protected abstract Control BuildBubble(string text, string speechStyleClass, Color? fontColor = null); + protected abstract Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null); protected override void FrameUpdate(FrameEventArgs args) { @@ -165,34 +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, Color? fontColor = null) - : base(text, senderEntity, eyeManager, chatManager, entityManager, speechStyleClass, fontColor) + public TextSpeechBubble(ChatMessage message, EntityUid senderEntity, string speechStyleClass, Color? fontColor = null) + : base(message, senderEntity, speechStyleClass, fontColor) { - } - protected override Control BuildBubble(string text, string speechStyleClass, Color? fontColor = null) + protected override Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null) { var label = new RichTextLabel { - MaxWidth = 256, + MaxWidth = SpeechMaxWidth, }; - if (fontColor != null) - { - var msg = new FormattedMessage(); - msg.PushColor(fontColor.Value); - msg.AddMarkup(text); - label.SetMessage(msg); - } - else - { - label.SetMessage(text); - } + label.SetMessage(FormatSpeech(message.WrappedMessage, fontColor)); var panel = new PanelContainer { @@ -204,4 +226,76 @@ protected override Control BuildBubble(string text, string speechStyleClass, Col 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 00000000000..b6401c113de --- /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 928422cd1d4..cfc98de7dc6 100644 --- a/Content.Client/Clothing/ClientClothingSystem.cs +++ b/Content.Client/Clothing/ClientClothingSystem.cs @@ -205,17 +205,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 d0a14860f11..4035c68cc75 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; @@ -97,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; diff --git a/Content.Client/Explosion/ExplosionOverlaySystem.cs b/Content.Client/Explosion/ExplosionOverlaySystem.cs index 60208ea1a0d..064b068a97e 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/BlurryVisionOverlay.cs b/Content.Client/Eye/Blinding/BlurryVisionOverlay.cs index 94590b54a50..7d0e7916da2 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/Forensics/ForensicScannerMenu.xaml.cs b/Content.Client/Forensics/ForensicScannerMenu.xaml.cs index 84ffd7969e7..8b6152c8612 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/Guidebook/Controls/GuideReagentEmbed.xaml b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml index f83eb8c2407..7b1beeeb650 100644 --- a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml +++ b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml @@ -14,29 +14,12 @@ - - - - - - - - - - - + HorizontalExpand="True"/> diff --git a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs index 5c2caf2230f..cf5a1b6e59c 100644 --- a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs +++ b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs @@ -6,8 +6,10 @@ using Content.Client.UserInterface.ControlExtensions; using Content.Shared.Chemistry.Reaction; using Content.Shared.Chemistry.Reagent; +using Content.Shared.Localizations; using JetBrains.Annotations; using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; @@ -53,7 +55,7 @@ public bool CheckMatchesSearch(string query) public void SetHiddenState(bool state, string query) { - this.Visible = CheckMatchesSearch(query) ? state : !state; + Visible = CheckMatchesSearch(query) ? state : !state; } public bool TryParseTag(Dictionary args, [NotNullWhen(true)] out Control? control) @@ -84,7 +86,11 @@ private void GenerateControl(ReagentPrototype reagent) BackgroundColor = reagent.SubstanceColor }; - var textColor = Color.ToHsl(reagent.SubstanceColor).Z > 0.45 + var r = reagent.SubstanceColor.R; + var g = reagent.SubstanceColor.G; + var b = reagent.SubstanceColor.B; + + var textColor = 0.2126f * r + 0.7152f * g + 0.0722f * b > 0.5 ? Color.Black : Color.White; @@ -92,49 +98,19 @@ private void GenerateControl(ReagentPrototype reagent) ("color", textColor), ("name", reagent.LocalizedName))); #region Recipe - // by default, we assume that the reaction has the same ID as the reagent. - // if this isn't true, we'll loop through reactions. - if (!_prototype.TryIndex(reagent.ID, out var reactionPrototype)) - { - reactionPrototype = _prototype.EnumeratePrototypes() - .FirstOrDefault(p => p.Products.ContainsKey(reagent.ID)); - } + var reactions = _prototype.EnumeratePrototypes() + .Where(p => p.Products.ContainsKey(reagent.ID)) + .OrderBy(p => p.Priority) + .ThenBy(p => p.Products.Count) + .ToList(); - if (reactionPrototype != null) + if (reactions.Any()) { - var reactantMsg = new FormattedMessage(); - var reactantsCount = reactionPrototype.Reactants.Count; - var i = 0; - foreach (var (product, reactant) in reactionPrototype.Reactants) - { - reactantMsg.AddMarkup(Loc.GetString("guidebook-reagent-recipes-reagent-display", - ("reagent", _prototype.Index(product).LocalizedName), ("ratio", reactant.Amount))); - i++; - if (i < reactantsCount) - reactantMsg.PushNewline(); - } - reactantMsg.Pop(); - ReactantsLabel.SetMessage(reactantMsg); - - if (reactionPrototype.MinimumTemperature > 0.0f) + foreach (var reactionPrototype in reactions) { - MixLabel.Text = Loc.GetString("guidebook-reagent-recipes-mix-and-heat", - ("temperature", reactionPrototype.MinimumTemperature)); + var ctrl = GetRecipeGuide(reactionPrototype); + RecipesDescriptionContainer.AddChild(ctrl); } - - var productMsg = new FormattedMessage(); - var productCount = reactionPrototype.Products.Count; - var u = 0; - foreach (var (product, ratio) in reactionPrototype.Products) - { - productMsg.AddMarkup(Loc.GetString("guidebook-reagent-recipes-reagent-display", - ("reagent", _prototype.Index(product).LocalizedName), ("ratio", ratio))); - u++; - if (u < productCount) - productMsg.PushNewline(); - } - productMsg.Pop(); - ProductsLabel.SetMessage(productMsg); } else { @@ -186,8 +162,69 @@ private void GenerateControl(ReagentPrototype reagent) FormattedMessage description = new(); description.AddText(reagent.LocalizedDescription); description.PushNewline(); - description.AddText(Loc.GetString("guidebook-reagent-physical-description", + description.AddMarkup(Loc.GetString("guidebook-reagent-physical-description", ("description", reagent.LocalizedPhysicalDescription))); ReagentDescription.SetMessage(description); } + + private GuideReagentReaction GetRecipeGuide(ReactionPrototype reactionPrototype) + { + var control = new GuideReagentReaction(); + + var reactantMsg = new FormattedMessage(); + var reactantsCount = reactionPrototype.Reactants.Count; + var i = 0; + foreach (var (product, reactant) in reactionPrototype.Reactants) + { + reactantMsg.AddMarkup(Loc.GetString("guidebook-reagent-recipes-reagent-display", + ("reagent", _prototype.Index(product).LocalizedName), ("ratio", reactant.Amount))); + i++; + if (i < reactantsCount) + reactantMsg.PushNewline(); + } + reactantMsg.Pop(); + control.ReactantsLabel.SetMessage(reactantMsg); + + var productMsg = new FormattedMessage(); + var productCount = reactionPrototype.Products.Count; + var u = 0; + foreach (var (product, ratio) in reactionPrototype.Products) + { + productMsg.AddMarkup(Loc.GetString("guidebook-reagent-recipes-reagent-display", + ("reagent", _prototype.Index(product).LocalizedName), ("ratio", ratio))); + u++; + if (u < productCount) + productMsg.PushNewline(); + } + productMsg.Pop(); + control.ProductsLabel.SetMessage(productMsg); + + var mixingCategories = new List(); + if (reactionPrototype.MixingCategories != null) + { + foreach (var category in reactionPrototype.MixingCategories) + { + mixingCategories.Add(_prototype.Index(category)); + } + } + + // only use the first one for the icon. + if (mixingCategories.FirstOrDefault() is { } primaryCategory) + { + control.MixTexture.Texture = _systemManager.GetEntitySystem().Frame0(primaryCategory.Icon); + } + + var mixingVerb = mixingCategories.Count == 0 + ? Loc.GetString("guidebook-reagent-recipes-mix") + : ContentLocalizationManager.FormatList(mixingCategories.Select(p => Loc.GetString(p.VerbText)).ToList()); + + var text = Loc.GetString("guidebook-reagent-recipes-mix-info", + ("verb", mixingVerb), + ("minTemp", reactionPrototype.MinimumTemperature), + ("maxTemp", reactionPrototype.MaximumTemperature), + ("hasMax", !float.IsPositiveInfinity(reactionPrototype.MaximumTemperature))); + + control.MixLabel.SetMarkup(text); + return control; + } } diff --git a/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml b/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml new file mode 100644 index 00000000000..69c14a59af7 --- /dev/null +++ b/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + diff --git a/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml.cs b/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml.cs new file mode 100644 index 00000000000..fbc6bf13fc4 --- /dev/null +++ b/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml.cs @@ -0,0 +1,20 @@ +using Content.Client.UserInterface.ControlExtensions; +using JetBrains.Annotations; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; + +namespace Content.Client.Guidebook.Controls; + +[UsedImplicitly, GenerateTypedNameReferences] +public sealed partial class GuideReagentReaction : BoxContainer, ISearchableControl +{ + public bool CheckMatchesSearch(string query) + { + return this.ChildrenContainText(query); + } + + public void SetHiddenState(bool state, string query) + { + Visible = CheckMatchesSearch(query) ? state : !state; + } +} diff --git a/Content.Client/Info/PlaytimeStats/PlaytimeStatsEntry.xaml b/Content.Client/Info/PlaytimeStats/PlaytimeStatsEntry.xaml index 97a66e5cc24..0f973b76890 100644 --- a/Content.Client/Info/PlaytimeStats/PlaytimeStatsEntry.xaml +++ b/Content.Client/Info/PlaytimeStats/PlaytimeStatsEntry.xaml @@ -1,6 +1,5 @@ + xmlns:customControls1="clr-namespace:Content.Client.Administration.UI.CustomControls"> ? OnHeaderClicked; private SortDirection _roleDirection = SortDirection.Ascending; diff --git a/Content.Client/Info/PlaytimeStats/PlaytimeStatsHeader.xaml b/Content.Client/Info/PlaytimeStats/PlaytimeStatsHeader.xaml index 4cf4d8e2ccc..d83e794472c 100644 --- a/Content.Client/Info/PlaytimeStats/PlaytimeStatsHeader.xaml +++ b/Content.Client/Info/PlaytimeStats/PlaytimeStatsHeader.xaml @@ -1,6 +1,5 @@ - + @@ -26,4 +25,4 @@ - + diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index ba8b72788e6..03f4f3f38b7 100644 --- a/Content.Client/Input/ContentContexts.cs +++ b/Content.Client/Input/ContentContexts.cs @@ -30,6 +30,8 @@ public static void SetupContexts(IInputContextContainer contexts) common.AddFunction(ContentKeyFunctions.TakeScreenshot); common.AddFunction(ContentKeyFunctions.TakeScreenshotNoUI); common.AddFunction(ContentKeyFunctions.ToggleFullscreen); + common.AddFunction(ContentKeyFunctions.MoveStoredItem); + common.AddFunction(ContentKeyFunctions.RotateStoredItem); common.AddFunction(ContentKeyFunctions.Point); common.AddFunction(ContentKeyFunctions.ZoomOut); common.AddFunction(ContentKeyFunctions.ZoomIn); @@ -63,6 +65,8 @@ public static void SetupContexts(IInputContextContainer contexts) human.AddFunction(ContentKeyFunctions.OpenInventoryMenu); human.AddFunction(ContentKeyFunctions.SmartEquipBackpack); human.AddFunction(ContentKeyFunctions.SmartEquipBelt); + human.AddFunction(ContentKeyFunctions.OpenBackpack); + human.AddFunction(ContentKeyFunctions.OpenBelt); human.AddFunction(ContentKeyFunctions.MouseMiddle); human.AddFunction(ContentKeyFunctions.ArcadeUp); human.AddFunction(ContentKeyFunctions.ArcadeDown); diff --git a/Content.Client/Inventory/ClientInventorySystem.cs b/Content.Client/Inventory/ClientInventorySystem.cs index 6976a8b5bc9..d4615210f24 100644 --- a/Content.Client/Inventory/ClientInventorySystem.cs +++ b/Content.Client/Inventory/ClientInventorySystem.cs @@ -1,9 +1,7 @@ using Content.Client.Clothing; using Content.Client.Examine; -using Content.Client.UserInterface.Controls; using Content.Client.Verbs.UI; using Content.Shared.Clothing.Components; -using Content.Shared.Hands.Components; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Inventory; @@ -15,14 +13,12 @@ using Robust.Shared.Containers; using Robust.Shared.Input.Binding; using Robust.Shared.Player; -using Robust.Shared.Prototypes; namespace Content.Client.Inventory { [UsedImplicitly] public sealed class ClientInventorySystem : InventorySystem { - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IUserInterfaceManager _ui = default!; @@ -89,7 +85,7 @@ private void OnUseInHand(EntityUid uid, ClothingComponent component, UseInHandEv private void OnDidUnequip(InventorySlotsComponent component, DidUnequipEvent args) { UpdateSlot(args.Equipee, component, args.Slot); - if (args.Equipee != _playerManager.LocalPlayer?.ControlledEntity) + if (args.Equipee != _playerManager.LocalEntity) return; var update = new SlotSpriteUpdate(null, args.SlotGroup, args.Slot, false); OnSpriteUpdate?.Invoke(update); @@ -98,7 +94,7 @@ private void OnDidUnequip(InventorySlotsComponent component, DidUnequipEvent arg private void OnDidEquip(InventorySlotsComponent component, DidEquipEvent args) { UpdateSlot(args.Equipee, component, args.Slot); - if (args.Equipee != _playerManager.LocalPlayer?.ControlledEntity) + if (args.Equipee != _playerManager.LocalEntity) return; var update = new SlotSpriteUpdate(args.Equipment, args.SlotGroup, args.Slot, HasComp(args.Equipment)); @@ -107,10 +103,8 @@ private void OnDidEquip(InventorySlotsComponent component, DidEquipEvent args) private void OnShutdown(EntityUid uid, InventoryComponent component, ComponentShutdown args) { - if (uid != _playerManager.LocalPlayer?.ControlledEntity) - return; - - OnUnlinkInventory?.Invoke(); + if (uid == _playerManager.LocalEntity) + OnUnlinkInventory?.Invoke(); } private void OnPlayerDetached(EntityUid uid, InventorySlotsComponent component, LocalPlayerDetachedEvent args) @@ -151,13 +145,10 @@ protected override void OnInit(EntityUid uid, InventoryComponent component, Comp base.OnInit(uid, component, args); _clothingVisualsSystem.InitClothing(uid, component); - if (!_prototypeManager.TryIndex(component.TemplateId, out InventoryTemplatePrototype? invTemplate) || - !TryComp(uid, out InventorySlotsComponent? inventorySlots)) - { + if (!TryComp(uid, out InventorySlotsComponent? inventorySlots)) return; - } - foreach (var slot in invTemplate.Slots) + foreach (var slot in component.Slots) { TryAddSlotDef(uid, inventorySlots, slot); } @@ -165,7 +156,7 @@ protected override void OnInit(EntityUid uid, InventoryComponent component, Comp public void ReloadInventory(InventorySlotsComponent? component = null) { - var player = _playerManager.LocalPlayer?.ControlledEntity; + var player = _playerManager.LocalEntity; if (player == null || !Resolve(player.Value, ref component, false)) { return; @@ -179,7 +170,7 @@ public void SetSlotHighlight(EntityUid owner, InventorySlotsComponent component, { var oldData = component.SlotData[slotName]; var newData = component.SlotData[slotName] = new SlotData(oldData, state); - if (owner == _playerManager.LocalPlayer?.ControlledEntity) + if (owner == _playerManager.LocalEntity) EntitySlotUpdate?.Invoke(newData); } @@ -198,7 +189,7 @@ public void UpdateSlot(EntityUid owner, InventorySlotsComponent component, strin var newData = component.SlotData[slotName] = new SlotData(component.SlotData[slotName], newHighlight, newBlocked); - if (owner == _playerManager.LocalPlayer?.ControlledEntity) + if (owner == _playerManager.LocalEntity) EntitySlotUpdate?.Invoke(newData); } @@ -208,48 +199,11 @@ public bool TryAddSlotDef(EntityUid owner, InventorySlotsComponent component, Sl if (!component.SlotData.TryAdd(newSlotDef.Name, newSlotData)) return false; - if (owner == _playerManager.LocalPlayer?.ControlledEntity) + if (owner == _playerManager.LocalEntity) OnSlotAdded?.Invoke(newSlotData); return true; } - public void RemoveSlotDef(EntityUid owner, InventorySlotsComponent component, SlotData slotData) - { - if (component.SlotData.Remove(slotData.SlotName)) - { - if (owner == _playerManager.LocalPlayer?.ControlledEntity) - OnSlotRemoved?.Invoke(slotData); - } - } - - public void RemoveSlotDef(EntityUid owner, InventorySlotsComponent component, string slotName) - { - if (!component.SlotData.TryGetValue(slotName, out var slotData)) - return; - - component.SlotData.Remove(slotName); - - if (owner == _playerManager.LocalPlayer?.ControlledEntity) - OnSlotRemoved?.Invoke(slotData); - } - - // TODO hud refactor This should also live in a UI Controller - private void HoverInSlotButton(EntityUid uid, string slot, SlotControl control, - InventoryComponent? inventoryComponent = null, HandsComponent? hands = null) - { - if (!Resolve(uid, ref inventoryComponent)) - return; - - if (!Resolve(uid, ref hands, false)) - return; - - if (hands.ActiveHandEntity is not EntityUid heldEntity) - return; - - if (!TryGetSlotContainer(uid, slot, out var containerSlot, out var slotDef, inventoryComponent)) - return; - } - public void UIInventoryActivate(string slot) { EntityManager.RaisePredictiveEvent(new UseSlotNetworkMessage(slot)); diff --git a/Content.Client/Inventory/StrippableBoundUserInterface.cs b/Content.Client/Inventory/StrippableBoundUserInterface.cs index 3e905749520..f8a585d3341 100644 --- a/Content.Client/Inventory/StrippableBoundUserInterface.cs +++ b/Content.Client/Inventory/StrippableBoundUserInterface.cs @@ -90,11 +90,11 @@ public void UpdateMenu() _strippingMenu.ClearButtons(); - if (EntMan.TryGetComponent(Owner, out var inv) && _protoMan.TryIndex(inv.TemplateId, out var template)) + if (EntMan.TryGetComponent(Owner, out var inv)) { - foreach (var slot in template.Slots) + foreach (var slot in inv.Slots) { - AddInventoryButton(Owner, slot.Name, template, inv); + AddInventoryButton(Owner, slot.Name, inv); } } @@ -190,7 +190,7 @@ private void SlotPressed(GUIBoundKeyEventArgs ev, SlotControl slot) _ui.GetUIController().OpenVerbMenu(slot.Entity.Value); } - private void AddInventoryButton(EntityUid invUid, string slotId, InventoryTemplatePrototype _, InventoryComponent inv) + private void AddInventoryButton(EntityUid invUid, string slotId, InventoryComponent inv) { if (!_inv.TryGetSlotContainer(invUid, slotId, out var container, out var slotDef, inv)) return; diff --git a/Content.Client/Medical/CrewMonitoring/CrewMonitoringBoundUserInterface.cs b/Content.Client/Medical/CrewMonitoring/CrewMonitoringBoundUserInterface.cs index fc632575c73..39788809871 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 00000000000..e6adf13bed4 --- /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 559a12d63bc..80bf5a3f8b9 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 ff08af6bb6b..d8c87899db4 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/Options/UI/OptionsMenu.xaml b/Content.Client/Options/UI/OptionsMenu.xaml index 5d028879fe8..ab3b88ca4e6 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 1a924d2af17..c3a8e664705 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 1e5722c9009..37442f84698 100644 --- a/Content.Client/Options/UI/Tabs/AudioTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/AudioTab.xaml.cs @@ -1,3 +1,4 @@ +using Content.Client.Audio; using Content.Shared.CCVar; using Content.Shared.Corvax.CCCVars; using Robust.Client.Audio; @@ -8,6 +9,7 @@ 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; @@ -17,14 +19,14 @@ namespace Content.Client.Options.UI.Tabs public sealed partial class AudioTab : Control { [Dependency] private readonly IConfigurationManager _cfg = default!; - private readonly AudioSystem _audio; + private readonly IAudioManager _audio; public AudioTab() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); - _audio = IoCManager.Resolve().System(); + _audio = IoCManager.Resolve(); LobbyMusicCheckBox.Pressed = _cfg.GetCVar(CCVars.LobbyMusicEnabled); RestartSoundsCheckBox.Pressed = _cfg.GetCVar(CCVars.RestartSoundsEnabled); EventMusicCheckBox.Pressed = _cfg.GetCVar(CCVars.EventMusicEnabled); @@ -85,7 +87,7 @@ private void OnAmbienceSoundsSliderChanged(Range obj) private void OnMasterVolumeSliderChanged(Range range) { - _audio.SetMasterVolume(MasterVolumeSlider.Value / 100); + _audio.SetMasterGain(MasterVolumeSlider.Value / 100f * ContentAudioSystem.MasterVolumeMultiplier); UpdateChanges(); } @@ -121,16 +123,18 @@ private void OnAdminSoundsCheckToggled(BaseButton.ButtonEventArgs args) private void OnApplyButtonPressed(BaseButton.ButtonEventArgs args) { - _cfg.SetCVar(CVars.AudioMasterVolume, LV100ToDB(MasterVolumeSlider.Value, CCVars.MasterMultiplier)); + _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(CCCVars.TTSVolume, TtsVolumeSlider.Value / 100f * ContentAudioSystem.LobbyMultiplier); - _cfg.SetCVar(CCVars.LobbyMusicVolume, LV100ToDB(LobbyVolumeSlider.Value)); - _cfg.SetCVar(CCCVars.TTSVolume, LV100ToDB(TtsVolumeSlider.Value)); // Corvax-TTS _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); @@ -146,14 +150,14 @@ private void OnResetButtonPressed(BaseButton.ButtonEventArgs args) private void Reset() { - MasterVolumeSlider.Value = DBToLV100(_cfg.GetCVar(CVars.AudioMasterVolume), CCVars.MasterMultiplier); - 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)); - TtsVolumeSlider.Value = DBToLV100(_cfg.GetCVar(CCCVars.TTSVolume)); // Corvax-TTS + 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; + TtsVolumeSlider.Value = _cfg.GetCVar(CCCVars.TTSVolume) * 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); @@ -161,35 +165,21 @@ 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 beri = (float) (Math.Pow(10, db / 10) * 100 / multiplier); - return beri; - } - - 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 - DBToLV100(_cfg.GetCVar(CVars.AudioMasterVolume), CCVars.MasterMultiplier)) < 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 isTtsVolumeSame = - Math.Abs(TtsVolumeSlider.Value - DBToLV100(_cfg.GetCVar(CCCVars.TTSVolume))) < 0.01f; // Corvax-TTS + Math.Abs(TtsVolumeSlider.Value - _cfg.GetCVar(CCCVars.TTSVolume) * 100f / ContentAudioSystem.LobbyMultiplier) < 0.01f; // Corvax-TTS 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 f759c78eca8..74a0c78c526 100644 --- a/Content.Client/Options/UI/Tabs/GraphicsTab.xaml +++ b/Content.Client/Options/UI/Tabs/GraphicsTab.xaml @@ -20,9 +20,6 @@ - - - diff --git a/Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs b/Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs index e64838ba752..1773b2abe5d 100644 --- a/Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs @@ -101,9 +101,6 @@ public GraphicsTab() UpdateApplyButton(); }; - ShowHeldItemCheckBox.OnToggled += OnCheckBoxToggled; - ShowCombatModeIndicatorsCheckBox.OnToggled += OnCheckBoxToggled; - ShowLoocAboveHeadCheckBox.OnToggled += OnCheckBoxToggled; IntegerScalingCheckBox.OnToggled += OnCheckBoxToggled; ViewportLowResCheckBox.OnToggled += OnCheckBoxToggled; ParallaxLowQualityCheckBox.OnToggled += OnCheckBoxToggled; @@ -120,9 +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); - ShowLoocAboveHeadCheckBox.Pressed = _cfg.GetCVar(CCVars.LoocAboveHeadShow); ViewportWidthSlider.Value = _cfg.GetCVar(CCVars.ViewportWidth); _cfg.OnValueChanged(CCVars.ViewportMinimumWidth, _ => UpdateViewportWidthRange()); @@ -168,9 +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.LoocAboveHeadShow, ShowLoocAboveHeadCheckBox.Pressed); _cfg.SetCVar(CCVars.HudFpsCounterVisible, FpsCounterCheckBox.Pressed); _cfg.SetCVar(CCVars.ViewportWidth, (int) ViewportWidthSlider.Value); @@ -206,9 +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 isLoocShowSame = ShowLoocAboveHeadCheckBox.Pressed == _cfg.GetCVar(CCVars.LoocAboveHeadShow); 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); @@ -223,9 +211,6 @@ private void UpdateApplyButton() isVPResSame && isPLQSame && isHudThemeSame && - isShowHeldItemSame && - isCombatModeIndicatorsSame && - isLoocShowSame && isFpsCounterVisibleSame && isWidthSame && isLayoutSame; diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs index 87b1f10352f..ce5cf421aef 100644 --- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs @@ -97,6 +97,12 @@ private void HandleToggleWalk(BaseButton.ButtonToggledEventArgs args) _deferCommands.Add(_inputManager.SaveToUserData); } + private void HandleStaticStorageUI(BaseButton.ButtonToggledEventArgs args) + { + _cfg.SetCVar(CCVars.StaticStorageUI, args.Pressed); + _cfg.SaveToFile(); + } + public KeyRebindTab() { IoCManager.InjectDependencies(this); @@ -175,10 +181,14 @@ void AddCheckBox(string checkBoxName, bool currentState, Action + + + + +