diff --git a/.github/ISSUE_TEMPLATE/heisentest_template.md b/.github/ISSUE_TEMPLATE/heisentest_template.md new file mode 100644 index 0000000000..2fde892b79 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/heisentest_template.md @@ -0,0 +1,42 @@ +--- +title: "{{ env.NAME }} test failure on main ({{ env.FULLNAME }})" +labels: autogenerated +--- +## Test Failure On Main + +A test failure has been detected on the main branch. This should never occur. + +> [!NOTE] +> This issue is automatically generated by a github action and will be updated when this failure reoccurs. +> Do not edit the body of this issue. + +## Failure Details +
+
Test name
+
{{ env.NAME }}
+
Fully qualified name
+
{{ env.FULLNAME }}
+
Last encountered
+
{{ date | date('dddd, MMMM Do') }}
+
+ +
+Failure message + +``` +{{ env.FAILURE }} +``` + +
+ +
+Test output + +``` +{{ env.OUTPUT }} +``` + +
+ +## Using this issue +Please place troubleshooting notes in the comments of this issue. diff --git a/.github/config.yml b/.github/config.yml deleted file mode 100644 index c846d99fa9..0000000000 --- a/.github/config.yml +++ /dev/null @@ -1,9 +0,0 @@ -Project-Condor: - org: space-wizards - project: 2 - inbox: Inbox - labels: - "W: In Progress": "In Progress" - "W: Discussion": "Design and Discussion" - "W: Backlog": "Backlog" - "W: Next": "Next" diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml deleted file mode 100644 index 66dc2c24f7..0000000000 --- a/.github/workflows/benchmarks.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Benchmarks -on: - workflow_dispatch: - schedule: - - cron: '0 8 * * *' - -concurrency: benchmarks - -jobs: - benchmark: - name: Run Benchmarks - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - submodules: 'recursive' - - name: Get Engine version - run: | - cd RobustToolbox - git fetch --depth=1 - echo "::set-output name=out::$(git rev-parse HEAD)" - id: engine_version - - name: Run script on centcomm - uses: appleboy/ssh-action@master - with: - host: centcomm.spacestation14.io - username: robust-benchmark-runner - key: ${{ secrets.CENTCOMM_ROBUST_BENCHMARK_RUNNER_KEY }} - command_timeout: 100000m - script: | - mkdir benchmark_run_content_${{ github.sha }} - cd benchmark_run_content_${{ github.sha }} - git clone https://github.com/space-wizards/space-station-14.git repo_dir --recursive - cd repo_dir - git checkout ${{ github.sha }} - cd Content.Benchmarks - dotnet restore - export ROBUST_BENCHMARKS_ENABLE_SQL=1 - export ROBUST_BENCHMARKS_SQL_ADDRESS="${{ secrets.BENCHMARKS_WRITE_ADDRESS }}" - export ROBUST_BENCHMARKS_SQL_PORT="${{ secrets.BENCHMARKS_WRITE_PORT }}" - export ROBUST_BENCHMARKS_SQL_USER="${{ secrets.BENCHMARKS_WRITE_USER }}" - export ROBUST_BENCHMARKS_SQL_PASSWORD="${{ secrets.BENCHMARKS_WRITE_PASSWORD }}" - export ROBUST_BENCHMARKS_SQL_DATABASE="content_benchmarks" - export GITHUB_SHA="${{ github.sha }}" - dotnet run --filter '*' --configuration Release - cd ../../.. - rm -rf benchmark_run_content_${{ github.sha }} diff --git a/.github/workflows/build-docfx.yml b/.github/workflows/build-docfx.yml deleted file mode 100644 index 55e3b151e1..0000000000 --- a/.github/workflows/build-docfx.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Build & Publish Docfx - -on: - schedule: - - cron: "0 0 * * 0" - -jobs: - docfx: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - name: Setup submodule - run: | - git submodule update --init --recursive - - name: Pull engine updates - uses: space-wizards/submodule-dependency@v0.1.5 - - name: Update Engine Submodules - run: | - cd RobustToolbox/ - git submodule update --init --recursive - - name: Setup .NET Core - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 10.0.x - - - name: Install dependencies - run: dotnet restore - - - name: Build Project - run: dotnet build --no-restore - - - name: Build DocFX - uses: nikeee/docfx-action@v1.0.0 - with: - args: Content.Docfx/docfx.json - - - name: Publish Docfx Documentation on GitHub Pages - uses: maxheld83/ghpages@master - env: - BUILD_DIR: Content.Docfx/_content-site - GH_PAT: ${{ secrets.GH_PAT }} diff --git a/.github/workflows/heisendetector.yml b/.github/workflows/heisendetector.yml new file mode 100644 index 0000000000..e26f9a8c26 --- /dev/null +++ b/.github/workflows/heisendetector.yml @@ -0,0 +1,87 @@ +name: Hunt for Heisentests + +on: + workflow_dispatch: + # every 15 minutes + schedule: + - cron: "*/15 * * * *" + +jobs: + hunt: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.get_fails.outputs.matrix }} + count: ${{ steps.get_fails.outputs.count }} + + steps: + - name: Checkout Master + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Setup .NET Core + uses: actions/setup-dotnet@v4.1.0 + with: + dotnet-version: 10.0.x + + - name: Install dependencies + run: dotnet restore + + - name: Build Project + run: dotnet build --configuration DebugOpt --no-restore /m + + - name: Run Content.Tests + id: tests + shell: pwsh + run: dotnet test --no-build --configuration DebugOpt Content.Tests/Content.Tests.csproj -- NUnit.ConsoleOut=0 NUnit.TestOutputXml="logs" NUnit.WorkDirectory="$(pwd)/test_results" + + - name: Run Content.IntegrationTests + id: integration_tests + shell: pwsh + run: | + $env:DOTNET_gcServer=1 + dotnet test --no-build --configuration DebugOpt Content.IntegrationTests/Content.IntegrationTests.csproj -- NUnit.ConsoleOut=0 NUnit.MapWarningTo=Failed NUnit.TestOutputXml="logs" NUnit.WorkDirectory="$(pwd)/test_results" + + - name: Archive NUnit3 test results. + if: failure() + uses: actions/upload-artifact@v7 + with: + name: nunit3-results + path: test_results/* + retention-days: 7 + compression-level: 9 + + - name: Extract Failures + if: failure() + id: get_fails + run: | + python -m pip install --upgrade pip + pip install xmltodict + python Tools/actions_extract_failures.py + + report: + needs: [ hunt ] + runs-on: ubuntu-latest + permissions: + issues: write + strategy: + matrix: + value: ${{ fromJSON(needs.hunt.outputs.matrix) }} + if: ${{ always() && failure() && (needs.hunt.outputs.count < 5) }} + name: Error for ${{ matrix.value.name }} + steps: + - name: Checkout Master + uses: actions/checkout@v6 + + - name: Create Issue + uses: dblock/create-a-github-issue@v3 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NAME: ${{ matrix.value.name }} + FULLNAME: ${{ matrix.value.fullname }} + OUTPUT: ${{ matrix.value.output }} + FAILURE: ${{ matrix.value.failure }} + with: + filename: .github/ISSUE_TEMPLATE/heisentest_template.md + update_existing: true + search_existing: all diff --git a/.gitignore b/.gitignore index 4771b6a664..fa68594da8 100644 --- a/.gitignore +++ b/.gitignore @@ -315,6 +315,9 @@ Resources/MapImages *.bak +# Shader error output +error.glsl + # Direnv stuff .direnv/ diff --git a/Content.Benchmarks/EffectDispatchBenchmark.cs b/Content.Benchmarks/EffectDispatchBenchmark.cs new file mode 100644 index 0000000000..598b0df3b7 --- /dev/null +++ b/Content.Benchmarks/EffectDispatchBenchmark.cs @@ -0,0 +1,196 @@ +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Content.IntegrationTests; +using Content.IntegrationTests.Pair; +using Content.Shared.EntityEffects; +using Robust.Shared; +using Robust.Shared.Analyzers; +using Robust.Shared.GameObjects; + +namespace Content.Benchmarks; + +/// +/// Benchmark comparing EntityEffect dispatch strategies. +/// Old: event bus SubscribeLocalEvent + RaiseLocalEvent +/// New: static Dictionary{{Type, IEntityEffectHandler}} + direct interface call +/// +[Virtual] +public partial class EffectDispatchBenchmark +{ + private TestPair _pair = default!; + private BenchSystem _sys = default!; + + [GlobalSetup] + public void Setup() + { + ProgramShared.PathOffset = "../../../../"; + PoolManager.Startup(typeof(BenchSystem).Assembly); + _pair = PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)).GetAwaiter().GetResult(); + + var entMan = _pair.Server.EntMan; + _sys = entMan.System(); + + _pair.Server.WaitPost(() => + { + var hitUid = entMan.Spawn(); + entMan.AddComponent(hitUid); + _sys.HitTarget = new(hitUid, entMan.GetComponent(hitUid)); + + var missUid = entMan.Spawn(); + _sys.MissTarget = new(missUid, entMan.GetComponent(missUid)); + + }) + .GetAwaiter() + .GetResult(); + } + + [GlobalCleanup] + public async Task Cleanup() + { + await _pair.DisposeAsync(); + PoolManager.Shutdown(); + } + + [Benchmark(Baseline = true)] + public int EventBusDispatch_Hit() + { + return _sys.RaiseViaEventBusHit(); + } + + [Benchmark] + public int EventBusDispatch_Miss() + { + return _sys.RaiseViaEventBusMiss(); + } + + [Benchmark] + public int CurrentImplementation_Hit() + { + return _sys.RaiseViaCurrentImplementation(); + } + + [Benchmark] + public int CurrentImplementation_Miss() + { + return _sys.RaiseViaCurrentImplementationMiss(); + } + + [Benchmark] + public int CSharpEventDispatch_Hit() + { + return _sys.RaiseViaCSharpEventHit(); + } + + [Benchmark] + public int CSharpEventDispatch_Miss() + { + return _sys.RaiseViaCSharpEventHit(); + } + + public sealed partial class BenchSystem : EntitySystem + { + private SharedEntityEffectsSystem _effectsSystem = default!; + + public Entity HitTarget; + public Entity MissTarget; + + private EntityEffect _effect = new TestEffect(); + + private int _counter; + + public delegate void EffectHandler(EntityUid uid); + + public event EffectHandler OnEffect; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnEventBus); + + OnEffect += OnCSharpEvent; + + _effectsSystem = EntityManager.System(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void OnEventBus(Entity entity, ref TestEffectEvent args) + { + _counter++; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void OnCSharpEvent(EntityUid uid) + { + TryComp(uid, out _); + _counter++; + } + + public int RaiseViaEventBusHit() + { + _counter = 0; + var ev = new TestEffectEvent(); + RaiseLocalEvent(HitTarget.Owner, ref ev); + return _counter; + } + + public int RaiseViaEventBusMiss() + { + _counter = 0; + var ev = new TestEffectEvent(); + RaiseLocalEvent(MissTarget.Owner, ref ev); + return _counter; + } + + public int RaiseViaCurrentImplementation() + { + _counter = 0; + _effectsSystem.ApplyEffect(HitTarget, _effect); + return _counter; + } + + public int RaiseViaCurrentImplementationMiss() + { + _counter = 0; + _effectsSystem.ApplyEffect(MissTarget, _effect); + return _counter; + } + + public int RaiseViaCSharpEventHit() + { + _counter = 0; + OnEffect?.Invoke(HitTarget.Owner); + return _counter; + } + + public int RaiseViaCSharpEventMiss() + { + _counter = 0; + OnEffect?.Invoke(MissTarget.Owner); + return _counter; + } + } + + [ByRefEvent] + public struct TestEffectEvent + { + } + + public sealed partial class TestEffectSystem : EntityEffectSystem + { + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + } + } + public sealed partial class TestEffect : EntityEffectBase + { + + } + + [RegisterComponent] + public sealed partial class DummyComponent : Component + { + } +} diff --git a/Content.Benchmarks/RaiseEventBenchmark.cs b/Content.Benchmarks/RaiseEventBenchmark.cs index 99e032d0b5..b8c6bd7d47 100644 --- a/Content.Benchmarks/RaiseEventBenchmark.cs +++ b/Content.Benchmarks/RaiseEventBenchmark.cs @@ -12,7 +12,7 @@ using Robust.Shared.GameObjects; namespace Content.Benchmarks; [Virtual] -public class RaiseEventBenchmark +public partial class RaiseEventBenchmark { private TestPair _pair = default!; private BenchSystem _sys = default!; @@ -35,6 +35,48 @@ public class RaiseEventBenchmark _sys.Ent2 = new(_sys.Ent.Owner, _sys.Ent.Comp); _sys.NetId = fact.GetRegistration().NetID!.Value; _sys.EvSubs = bus.GetNetCompEventHandlers(); + + var id2 = entMan.Spawn(); + entMan.AddComponent(id2); + entMan.AddComponent(id2); + _sys.Ent2Comps = id2; + + var id4 = entMan.Spawn(); + entMan.AddComponent(id4); + entMan.AddComponent(id4); + entMan.AddComponent(id4); + entMan.AddComponent(id4); + _sys.Ent4Comps = id4; + + var id8 = entMan.Spawn(); + entMan.AddComponent(id8); + entMan.AddComponent(id8); + entMan.AddComponent(id8); + entMan.AddComponent(id8); + entMan.AddComponent(id8); + entMan.AddComponent(id8); + entMan.AddComponent(id8); + entMan.AddComponent(id8); + _sys.Ent8Comps = id8; + + var id16 = entMan.Spawn(); + entMan.AddComponent(id16); + entMan.AddComponent(id16); + entMan.AddComponent(id16); + entMan.AddComponent(id16); + entMan.AddComponent(id16); + entMan.AddComponent(id16); + entMan.AddComponent(id16); + entMan.AddComponent(id16); + entMan.AddComponent(id16); + entMan.AddComponent(id16); + entMan.AddComponent(id16); + entMan.AddComponent(id16); + entMan.AddComponent(id16); + entMan.AddComponent(id16); + entMan.AddComponent(id16); + entMan.AddComponent(id16); + _sys.Ent16Comps = id16; }) .GetAwaiter() .GetResult(); @@ -48,9 +90,33 @@ public class RaiseEventBenchmark } [Benchmark(Baseline = true)] - public int RaiseEvent() + public int RaiseEvent1() { - return _sys.RaiseEvent(); + return _sys.RaiseEvent1(); + } + + [Benchmark] + public int RaiseEvent2() + { + return _sys.RaiseEvent2(); + } + + [Benchmark] + public int RaiseEvent4() + { + return _sys.RaiseEvent4(); + } + + [Benchmark] + public int RaiseEvent8() + { + return _sys.RaiseEvent8(); + } + + [Benchmark] + public int RaiseEvent16() + { + return _sys.RaiseEvent16(); } [Benchmark] @@ -77,10 +143,14 @@ public class RaiseEventBenchmark return _sys.CSharpEvent(); } - public sealed class BenchSystem : EntitySystem + public sealed partial class BenchSystem : EntitySystem { public Entity Ent; public Entity Ent2; + public EntityUid Ent2Comps; + public EntityUid Ent4Comps; + public EntityUid Ent8Comps; + public EntityUid Ent16Comps; public delegate void EntityEventHandler(EntityUid uid, TransformComponent comp, ref BenchEv ev); @@ -92,16 +162,60 @@ public class RaiseEventBenchmark { base.Initialize(); SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); OnCSharpEvent += OnEvent; } - public int RaiseEvent() + public int RaiseEvent1() { var ev = new BenchEv(); RaiseLocalEvent(Ent.Owner, ref ev); return ev.N; } + public int RaiseEvent2() + { + var ev = new BenchEv(); + RaiseLocalEvent(Ent2Comps, ref ev); + return ev.N; + } + + public int RaiseEvent4() + { + var ev = new BenchEv(); + RaiseLocalEvent(Ent4Comps, ref ev); + return ev.N; + } + + public int RaiseEvent8() + { + var ev = new BenchEv(); + RaiseLocalEvent(Ent8Comps, ref ev); + return ev.N; + } + + public int RaiseEvent16() + { + var ev = new BenchEv(); + RaiseLocalEvent(Ent16Comps, ref ev); + return ev.N; + } + public int RaiseCompEvent() { var ev = new BenchEv(); @@ -134,7 +248,7 @@ public class RaiseEventBenchmark } [MethodImpl(MethodImplOptions.NoInlining)] - private void OnEvent(EntityUid uid, TransformComponent component, ref BenchEv args) + private void OnEvent(EntityUid uid, T component, ref BenchEv args) { args.N += uid.Id; } @@ -145,5 +259,53 @@ public class RaiseEventBenchmark { public int N; } + + [RegisterComponent] + public sealed partial class Bench1Component : Component; + + [RegisterComponent] + public sealed partial class Bench2Component : Component; + + [RegisterComponent] + public sealed partial class Bench3Component : Component; + + [RegisterComponent] + public sealed partial class Bench4Component : Component; + + [RegisterComponent] + public sealed partial class Bench5Component : Component; + + [RegisterComponent] + public sealed partial class Bench6Component : Component; + + [RegisterComponent] + public sealed partial class Bench7Component : Component; + + [RegisterComponent] + public sealed partial class Bench8Component : Component; + + [RegisterComponent] + public sealed partial class Bench9Component : Component; + + [RegisterComponent] + public sealed partial class Bench10Component : Component; + + [RegisterComponent] + public sealed partial class Bench11Component : Component; + + [RegisterComponent] + public sealed partial class Bench12Component : Component; + + [RegisterComponent] + public sealed partial class Bench13Component : Component; + + [RegisterComponent] + public sealed partial class Bench14Component : Component; + + [RegisterComponent] + public sealed partial class Bench15Component : Component; + + [RegisterComponent] + public sealed partial class Bench16Component : Component; } } diff --git a/Content.Client/Administration/AdminQuickInfoSystem.cs b/Content.Client/Administration/AdminQuickInfoSystem.cs index 55e1979a99..846dd3f0f0 100644 --- a/Content.Client/Administration/AdminQuickInfoSystem.cs +++ b/Content.Client/Administration/AdminQuickInfoSystem.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Content.Client.Administration.Systems; using Content.Client.Stylesheets; using Content.Shared.Administration; @@ -59,7 +59,7 @@ internal sealed partial class AdminQuickInfoSystem : EntitySystem public void OpenPopupFor(NetEntity[] entities) { - var vBox = new VBox(); + var vBox = new VBox() { SeparationOverride = 6 };; var popup = new Popup { Children = @@ -74,9 +74,19 @@ internal sealed partial class AdminQuickInfoSystem : EntitySystem }, }, }; - + var first = true; foreach (var entity in entities) { + if (!first) + { + vBox.AddChild(new PanelContainer + { + StyleClasses = { StyleClass.LowDivider }, + HorizontalExpand = true, + }); + first = false; + } + var playerInfo = _adminSystem.PlayerList.FirstOrDefault(p => p.NetEntity == entity); var control = new InfoControl(this, entity, playerInfo); popup.OnPopupHide += () => control.Unsubscribe(); @@ -92,19 +102,21 @@ internal sealed partial class AdminQuickInfoSystem : EntitySystem { private readonly AdminQuickInfoSystem _system; private readonly NetEntity _entity; - private readonly PlayerInfo? _playerInfo; + private readonly bool _activeControl; + private PlayerInfo? _playerInfo; private QuickInfoShared.SingleEntityInfo? _response; private readonly RichTextLabel _contents = new(); private ILocalizationManager Loc => _system.Loc; + private AdminSystem Admin => _system._adminSystem; public InfoControl(AdminQuickInfoSystem system, NetEntity entity, PlayerInfo? playerInfo) { _system = system; _entity = entity; _playerInfo = playerInfo; - + _activeControl = (playerInfo != null); AddChild(_contents); system.EntityResponseReceived += OnEntityResponseReceived; @@ -119,6 +131,7 @@ internal sealed partial class AdminQuickInfoSystem : EntitySystem return; _response = ev; + _playerInfo ??= Admin.PlayerList.FirstOrDefault(p => p.SessionId == ev.LastPlayer); Rebuild(); } @@ -131,6 +144,10 @@ internal sealed partial class AdminQuickInfoSystem : EntitySystem private void Rebuild() { var sb = new FormattedStringBuilder(); + if (!_activeControl) + { + sb.AppendMarkupLine(Loc.GetString("admin-quick-info-inactive")); + } if (_playerInfo != null) { diff --git a/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs b/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs index 5515600d1e..0e7de8acba 100644 --- a/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs +++ b/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs @@ -106,13 +106,35 @@ public sealed partial class NoteEdit : FancyWindow { PermanentCheckBox.Pressed = false; UpdatePermanentCheckboxFields(); - ExpiryLineEdit.Text = ExpiryTime.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); + + var timeLeft = ConvertDateToTimeFromNow(ExpiryTime.Value.ToLocalTime()); + + ExpiryLineEdit.Text = Math.Round(timeLeft.Item2, 2).ToString(); + ExpiryLengthDropdown.SelectId((int)timeLeft.Item1); } } UpdateSubmitButton(); } + // Convert the given date time into a multiplier and value. + // This is for having a simple format like 2 weeks instead of everything being in hours. + // For example, a 2 weeks old date would return (Multipliers.Days, 14) + private (Multipliers, double) ConvertDateToTimeFromNow(DateTime expirationDate) + { + var deltaTime = expirationDate - DateTime.Now; + + if (deltaTime.TotalMinutes <= 0) + return (Multipliers.Minutes, 0.0); + + return deltaTime.TotalDays switch + { + < 1 => (Multipliers.Minutes, deltaTime.TotalMinutes), // Less than a day + < 365 => (Multipliers.Days, deltaTime.TotalDays), // Less than a year + _ => (Multipliers.Months, deltaTime.TotalDays / 30) // More than a year + }; + } + private void OnSubmitButtonMouseEntered(GUIMouseHoverEventArgs args) { if (!SubmitButton.Disabled) @@ -300,7 +322,7 @@ public sealed partial class NoteEdit : FancyWindow return true; } - if (string.IsNullOrWhiteSpace(ExpiryLineEdit.Text) || !uint.TryParse(ExpiryLineEdit.Text, out var inputInt)) + if (string.IsNullOrWhiteSpace(ExpiryLineEdit.Text) || !double.TryParse(ExpiryLineEdit.Text, out var inputDouble) || inputDouble < 0) { ExpiryLineEdit.ModulateSelfOverride = Color.Red; return false; @@ -317,7 +339,8 @@ public sealed partial class NoteEdit : FancyWindow (int) Multipliers.Centuries => TimeSpan.FromDays(36525).TotalMinutes, _ => throw new ArgumentOutOfRangeException(nameof(ExpiryLengthDropdown.SelectedId), "Multiplier out of range :(") }; - ExpiryTime = DateTime.UtcNow.AddMinutes(inputInt * mult); + + ExpiryTime = DateTime.UtcNow.AddMinutes(inputDouble * mult); ExpiryLineEdit.ModulateSelfOverride = null; return true; } @@ -339,4 +362,4 @@ public sealed partial class NoteEdit : FancyWindow SubmitPressed = null; } -} +} \ No newline at end of file diff --git a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs index 4b9ffcbef2..cb23c08b02 100644 --- a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs +++ b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs @@ -9,6 +9,7 @@ using Robust.Client.Player; using Robust.Client.UserInterface; using Robust.Client.UserInterface.XAML; using Robust.Shared.Configuration; +using Robust.Shared.Prototypes; using static Content.Client.Administration.UI.Tabs.PlayerTab.PlayerTabHeader; using static Robust.Client.UserInterface.Controls.BaseButton; @@ -20,6 +21,7 @@ public sealed partial class PlayerTab : Control [Dependency] private IEntityManager _entManager = default!; [Dependency] private IConfigurationManager _config = default!; [Dependency] private IPlayerManager _playerMan = default!; + [Dependency] private IPrototypeManager _proto = default!; private const string ArrowUp = "↑"; private const string ArrowDown = "↓"; @@ -155,7 +157,12 @@ public sealed partial class PlayerTab : Control UpdateHeaderSymbols(); SearchList.PopulateList(sortedPlayers.Select(info => new PlayerListData(info, - $"{info.Username} {info.CharacterName} {info.IdentityName} {info.StartingJob}")) + $"{info.Username} " + + $"{info.CharacterName} " + + $"{info.IdentityName} " + + $"{info.StartingJob} " + + $"{(info.Subtype is not null ? Loc.GetString(info.Subtype) : string.Empty)} " + + $"{(_proto.TryIndex(info.RoleProto, out var proto) ? Loc.GetString(proto.Name) : string.Empty)}")) .ToList()); } diff --git a/Content.Client/Administration/UI/Tabs/ServerTab.xaml b/Content.Client/Administration/UI/Tabs/ServerTab.xaml index f3a6421aea..c67f9322bd 100644 --- a/Content.Client/Administration/UI/Tabs/ServerTab.xaml +++ b/Content.Client/Administration/UI/Tabs/ServerTab.xaml @@ -7,9 +7,9 @@ - - - + + + diff --git a/Content.Client/Anomaly/Effects/ClientInnerBodySystem.cs b/Content.Client/Anomaly/Effects/ClientInnerBodySystem.cs index 6d48254c19..173aca24bf 100644 --- a/Content.Client/Anomaly/Effects/ClientInnerBodySystem.cs +++ b/Content.Client/Anomaly/Effects/ClientInnerBodySystem.cs @@ -1,3 +1,4 @@ +using Content.Client.DisplacementMap; using Content.Shared.Anomaly.Components; using Content.Shared.Anomaly.Effects; using Content.Shared.Humanoid; @@ -8,6 +9,9 @@ namespace Content.Client.Anomaly.Effects; public sealed partial class ClientInnerBodyAnomalySystem : SharedInnerBodyAnomalySystem { [Dependency] private SpriteSystem _sprite = default!; + [Dependency] private DisplacementMapSystem _displacement = default!; + + [Dependency] private EntityQuery _visualsQuery = default!; public override void Initialize() { @@ -37,6 +41,22 @@ public sealed partial class ClientInnerBodyAnomalySystem : SharedInnerBodyAnomal _sprite.LayerSetVisible((ent.Owner, sprite), index, true); sprite.LayerSetShader(index, "unshaded"); + + if (_visualsQuery.TryGetComponent(ent, out var visuals) && visuals.Displacement != null) + { + if (ProtoMan.Resolve(visuals.Displacement, out var displacement)) + { + _displacement.TryAddDisplacement(displacement.Displacement, + (ent.Owner, sprite), + index, + ent.Comp.LayerMap, + out _); + } + else + { + _displacement.EnsureDisplacementIsNotOnSprite((ent.Owner, sprite), ent.Comp.LayerMap); + } + } } private void OnCompShutdown(Entity ent, ref ComponentShutdown args) @@ -46,5 +66,7 @@ public sealed partial class ClientInnerBodyAnomalySystem : SharedInnerBodyAnomal var index = _sprite.LayerMapGet((ent.Owner, sprite), ent.Comp.LayerMap); _sprite.LayerSetVisible((ent.Owner, sprite), index, false); + + _displacement.EnsureDisplacementIsNotOnSprite((ent.Owner, sprite), ent.Comp.LayerMap); } } diff --git a/Content.Client/Atmos/Components/FireVisualsComponent.cs b/Content.Client/Atmos/Components/FireVisualsComponent.cs index 02278e9479..ac7a78edcf 100644 --- a/Content.Client/Atmos/Components/FireVisualsComponent.cs +++ b/Content.Client/Atmos/Components/FireVisualsComponent.cs @@ -1,3 +1,6 @@ +using Content.Shared.DisplacementMap; +using Robust.Shared.Prototypes; + namespace Content.Client.Atmos.Components; /// @@ -39,4 +42,10 @@ public sealed partial class FireVisualsComponent : Component /// the burning entity as entities don't support having multiple point-lights. /// public EntityUid? LightEntity; + + /// + /// Tracks the currently applied displacement, so that only new changes update the layer. + /// + [DataField] + public ProtoId? CurrentDisplacement; } diff --git a/Content.Client/Atmos/EntitySystems/FireVisualizerSystem.cs b/Content.Client/Atmos/EntitySystems/FireVisualizerSystem.cs index ec4500935c..f308e0da03 100644 --- a/Content.Client/Atmos/EntitySystems/FireVisualizerSystem.cs +++ b/Content.Client/Atmos/EntitySystems/FireVisualizerSystem.cs @@ -1,5 +1,7 @@ using Content.Client.Atmos.Components; +using Content.Client.DisplacementMap; using Content.Shared.Atmos; +using Content.Shared.DisplacementMap; using Robust.Client.GameObjects; using Robust.Shared.Map; using Robust.Shared.Utility; @@ -12,6 +14,7 @@ namespace Content.Client.Atmos.EntitySystems; public sealed partial class FireVisualizerSystem : VisualizerSystem { [Dependency] private PointLightSystem _lights = default!; + [Dependency] private DisplacementMapSystem _displacement = default!; public override void Initialize() { @@ -65,6 +68,7 @@ public sealed partial class FireVisualizerSystem : VisualizerSystem(uid, FireVisuals.OnFire, out var onFire, appearance); AppearanceSystem.TryGetData(uid, FireVisuals.FireStacks, out var fireStacks, appearance); + AppearanceSystem.TryGetData(uid, FireVisuals.FireDisplacement, out var fireDisplacement, appearance); SpriteSystem.LayerSetVisible((uid, sprite), index, onFire); if (!onFire) @@ -83,6 +87,16 @@ public sealed partial class FireVisualizerSystem : VisualizerSystem(fireDisplacement, out var displacementProto)) + _displacement.TryAddDisplacement(displacementProto.Displacement, (uid, sprite), index, FireVisualLayers.Fire, out _); + else + _displacement.EnsureDisplacementIsNotOnSprite((uid, sprite), FireVisualLayers.Fire); + + component.CurrentDisplacement = fireDisplacement; + } + component.LightEntity ??= Spawn(null, new EntityCoordinates(uid, default)); var light = EnsureComp(component.LightEntity.Value); diff --git a/Content.Client/Atmos/Overlays/GasTileHeatBlurOverlay.cs b/Content.Client/Atmos/Overlays/GasTileHeatBlurOverlay.cs index 4dc1300193..43b95b975a 100644 --- a/Content.Client/Atmos/Overlays/GasTileHeatBlurOverlay.cs +++ b/Content.Client/Atmos/Overlays/GasTileHeatBlurOverlay.cs @@ -70,7 +70,7 @@ public sealed partial class GasTileHeatBlurOverlay : Overlay _heatGradientTexture = _resourceCache.GetTexture("/Textures/Effects/HeatBlur/soft_circle.png"); _shader = _proto.Index(HeatOverlayShader).InstanceUnique(); - _configManager.OnValueChanged(CCVars.ReducedMotion, SetReducedMotion, invokeImmediately: true); + _configManager.OnValueChanged(CCVars.DisableHeatDistortion, SetReducedMotion, invokeImmediately: true); } private void SetReducedMotion(bool reducedMotion) @@ -227,7 +227,7 @@ public sealed partial class GasTileHeatBlurOverlay : Overlay { _resources.Dispose(); - _configManager.UnsubValueChanged(CCVars.ReducedMotion, SetReducedMotion); + _configManager.UnsubValueChanged(CCVars.DisableHeatDistortion, SetReducedMotion); base.DisposeBehavior(); } diff --git a/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs b/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs index b98ad5ad74..f7660b2ec8 100644 --- a/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs +++ b/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs @@ -22,7 +22,6 @@ public sealed partial class ContentAudioSystem { [Dependency] private IConfigurationManager _configManager = default!; [Dependency] private IGameTiming _timing = default!; - [Dependency] private ILogManager _logManager = default!; [Dependency] private IPlayerManager _player = default!; [Dependency] private IRobustRandom _random = default!; [Dependency] private IStateManager _state = default!; @@ -59,7 +58,7 @@ public sealed partial class ContentAudioSystem private void InitializeAmbientMusic() { Subs.CVar(_configManager, CCVars.AmbientMusicVolume, AmbienceCVarChanged, true); - _sawmill = _logManager.GetSawmill("audio.ambience"); + _sawmill = LogManager.GetSawmill("audio.ambience"); // Reset audio _nextAudio = TimeSpan.MaxValue; diff --git a/Content.Client/Audio/Midi/MidiFileCollectionManager.cs b/Content.Client/Audio/Midi/MidiFileCollectionManager.cs new file mode 100644 index 0000000000..9f84fad30b --- /dev/null +++ b/Content.Client/Audio/Midi/MidiFileCollectionManager.cs @@ -0,0 +1,201 @@ +using System.IO; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Robust.Shared.ContentPack; +using Robust.Shared.Utility; + +namespace Content.Client.Audio.Midi; + +/// +/// Handles storage/management of MIDI files stored inside the user data directory. +/// +[PublicAPI] +public sealed partial class MidiFileCollectionManager : IPostInjectInit +{ + /// + /// Directory path to use inside UserData for storing MIDIs. + /// + private static readonly ResPath UserMidiDirectory = new("/UserMidis/"); + + private const string SawmillCategory = "midifilecollection"; + + [Dependency] private IResourceManager _resManager = default!; + [Dependency] private ILogManager _logManager = default!; + + private ISawmill _sawmill = default!; + + private readonly List _filePaths = []; + + /// + /// Raised after a MIDI file has been added to the library. Contains added file path as argument. + /// + public event Action? MidiFileAdded; + + /// + /// Raised after a MIDI file has been removed from the library. Contains removed file path as argument. + /// + public event Action? MidiFileRemoved; + + /// + /// Raised after more than one or two library changes occured at once. (i.e. initial load or when all items deleted) + /// + public event Action? MidiFilesReset; + + /// + public void PostInject() + { + EnsureMidiDirectoryExists(); + ReloadLibrary(); + _sawmill = _logManager.GetSawmill(SawmillCategory); + } + + /// + /// Returns the binary content of the given MIDI file. + /// + /// MIDI file path to get. + /// MIDI binary as a byte array or an empty byte array if the file doesn't exist. + public byte[] GetMidiData(ResPath filePath) + { + try + { + var fullPath = UserMidiDirectory / filePath; + return _resManager.UserData.ReadAllBytes(fullPath); + } + catch (Exception e) + { + _sawmill.Error($"Failed to read MIDI data from '{filePath}': {e.Message}"); + return []; + } + } + + /// + /// Returns an enumeration of all available MIDI files. + /// + /// An enumeration of MIDI file paths. + public IEnumerable GetMidiFiles() + { + return _filePaths; + } + + /// + /// Stores the given byte stream with the given file path inside the directory. + /// + /// File path to write. + /// Binary data to write. + /// Raises on success. + public async Task AddMidiFile(ResPath filePath, Stream data) + { + try + { + await using var file = _resManager.UserData.OpenWrite(UserMidiDirectory / filePath); + await data.CopyToAsync(file); + _filePaths.Add(filePath); + MidiFileAdded?.Invoke(filePath); + return true; + } + catch (Exception e) + { + _sawmill.Error($"Failed to store MIDI file '{filePath}' in library: {e.Message}"); + return false; + } + } + + /// + /// Stores the given byte array with the given file path inside the directory. + /// + /// File path to write. + /// Binary data to write. + /// Raises on success. + public async Task AddMidiFile(ResPath filePath, byte[] data) + { + return await AddMidiFile(filePath, new MemoryStream(data)); + } + + /// + /// Renames a MIDI file inside the library. + /// + /// Current file path + /// New file path + /// Raises and on success. + public bool RenameMidiFile(ResPath oldPath, ResPath newPath) + { + try + { + var fullOldPath = UserMidiDirectory / oldPath; + var fullNewPath = UserMidiDirectory / newPath; + fullOldPath = fullOldPath.Clean(); + fullNewPath = fullNewPath.Clean(); + _resManager.UserData.Rename(fullOldPath, fullNewPath); + _filePaths.Remove(oldPath); + MidiFileRemoved?.Invoke(oldPath); + _filePaths.Add(newPath); + MidiFileAdded?.Invoke(newPath); + return true; + } + catch (Exception e) + { + _sawmill.Error($"Failed to rename MIDI file '{oldPath}' with '{newPath}': {e.Message}"); + return false; + } + } + + /// + /// Permanently removes the given MIDI file if it exists inside . + /// + /// File path to remove. + /// Raises + public void RemoveMidiFile(ResPath filePath) + { + DeleteMidiFile(filePath); + _filePaths.Remove(filePath); + MidiFileRemoved?.Invoke(filePath); + } + + /// + /// Removes all registered MIDI files, permanently. + /// + /// Raises + public void RemoveAllMidiFiles() + { + foreach (var path in _filePaths) + { + DeleteMidiFile(path); + } + + _filePaths.Clear(); + MidiFilesReset?.Invoke(); + } + + /// + /// Clears and reloads the entire MIDI library. + /// + public void ReloadLibrary() + { + _filePaths.Clear(); + if (!_resManager.UserData.IsDir(UserMidiDirectory)) + return; + + foreach (var path in _resManager.UserData.DirectoryEntries(UserMidiDirectory)) + { + var filePath = new ResPath(UserMidiDirectory + path); + if (!filePath.Extension.Equals("midi") && !filePath.Extension.Equals("mid")) + continue; + + _filePaths.Add(new ResPath(path)); + } + + MidiFilesReset?.Invoke(); + } + + private void DeleteMidiFile(ResPath filePath) + { + var path = (UserMidiDirectory / filePath).Clean(); + _resManager.UserData.Delete(path); + } + + private void EnsureMidiDirectoryExists() + { + if (!_resManager.UserData.Exists(UserMidiDirectory)) + _resManager.UserData.CreateDir(UserMidiDirectory); + } +} diff --git a/Content.Client/Body/VisualBodySystem.cs b/Content.Client/Body/VisualBodySystem.cs index c29a60cc97..ad44317924 100644 --- a/Content.Client/Body/VisualBodySystem.cs +++ b/Content.Client/Body/VisualBodySystem.cs @@ -2,6 +2,7 @@ using System.Linq; using Content.Client.DisplacementMap; using Content.Shared.Body; using Content.Shared.CCVar; +using Content.Shared.DisplacementMap; using Content.Shared.Humanoid.Markings; using Content.Shared.Humanoid; using Robust.Client.GameObjects; @@ -73,6 +74,16 @@ public sealed partial class VisualBodySystem : SharedVisualBodySystem return; _sprite.LayerSetData(target, index, ent.Comp.Data); + + var displacement = ent.Comp.Displacement; + if (displacement != null && ProtoMan.Resolve(displacement, out var displacementProto)) + { + _displacement.TryAddDisplacement(displacementProto.Displacement, + (target, Comp(target)), + index, + ent.Comp.Layer, + out _); + } } private void RemoveVisual(Entity ent, EntityUid target) @@ -81,6 +92,8 @@ public sealed partial class VisualBodySystem : SharedVisualBodySystem return; _sprite.LayerSetRsiState(target, index, RSI.StateId.Invalid); + + _displacement.EnsureDisplacementIsNotOnSprite((target, Comp(target)), ent.Comp.Layer); } private void OnMarkingsGotInserted(Entity ent, ref OrganGotInsertedEvent args) @@ -183,6 +196,7 @@ public sealed partial class VisualBodySystem : SharedVisualBodySystem ent.Comp.MarkingsDisplacement.TryGetValue(proto.BodyPart, out var displacement); + var numDisplacements = 0; for (var i = 0; i < proto.Sprites.Count; i++) { var sprite = proto.Sprites[i]; @@ -195,7 +209,14 @@ public sealed partial class VisualBodySystem : SharedVisualBodySystem if (!_sprite.LayerMapTryGet(target, layerId, out _, false)) { - var spriteLayer = _sprite.AddLayer(target, sprite, index + i + 1); + // Having three separate indices and a magic +1 is cursed, but: + // - index refers to the index of the organ the marking is applied to + // - i is the current sprite of the marking that is being applied + // - numDisplacements tracks how many displacements have been applied, and is + // an additional offset to ensure that the order of the base sprites is correct + // after inserting a displacement layer + // - The +1 ensures that markings render on top of the base organ + var spriteLayer = _sprite.AddLayer(target, sprite, index + i + numDisplacements + 1); _sprite.LayerMapSet(target, layerId, spriteLayer); _sprite.LayerSetSprite(target, layerId, rsi); } @@ -206,7 +227,18 @@ public sealed partial class VisualBodySystem : SharedVisualBodySystem _sprite.LayerSetColor(target, layerId, Color.White); if (displacement != null && proto.CanBeDisplaced) - _displacement.TryAddDisplacement(displacement, (target, target.Comp), index + i + 1, layerId, out _); + { + _displacement.TryAddDisplacement( + displacement, + (target, target.Comp), + // Similar logic as above, but this makes the displacement layer go below the + // original sprite. So it should be all the displacements, then all the sprite layers on top + index + i + 1, + layerId, + out _ + ); + numDisplacements++; + } } applied.Add(marking); diff --git a/Content.Client/CardboardBox/CardboardBoxSystem.cs b/Content.Client/CardboardBox/CardboardBoxSystem.cs index f527caf678..2ba237fc8b 100644 --- a/Content.Client/CardboardBox/CardboardBoxSystem.cs +++ b/Content.Client/CardboardBox/CardboardBoxSystem.cs @@ -11,10 +11,11 @@ namespace Content.Client.CardboardBox; public sealed partial class CardboardBoxSystem : SharedCardboardBoxSystem { [Dependency] private EntityLookupSystem _entityLookup = default!; - [Dependency] private TransformSystem _transform = default!; [Dependency] private ExamineSystemShared _examine = default!; [Dependency] private SpriteSystem _sprite = default!; - [Dependency] private EntityQuery _mobStateQuery = default!; + [Dependency] private TransformSystem _transform = default!; + + [Dependency] private EntityQuery _mobStateQuery; public override void Initialize() { @@ -33,13 +34,13 @@ public sealed partial class CardboardBoxSystem : SharedCardboardBoxSystem var xform = Transform(source); var sourcePos = _transform.GetMapCoordinates(source, xform); - //Any mob that can move should be surprised? - //God mind rework needs to come faster so it can just check for mind - //TODO: Replace with Mind Query when mind rework is in. + // Any mob that can move should be surprised? + // God mind rework needs to come faster so it can just check for mind + // TODO: Replace with Mind Query when mind rework is in. var mobMoverEntities = new List(); var mover = GetEntity(msg.Mover); - //Filter out entities in range to see that they're a mob and add them to the mobMoverEntities hash for faster lookup + // Filter out entities in range to see that they're a mob and add them to the mobMoverEntities hash for faster lookup var movers = new HashSet>(); _entityLookup.GetEntitiesInRange(xform.Coordinates, box.Distance, movers); @@ -52,7 +53,7 @@ public sealed partial class CardboardBoxSystem : SharedCardboardBoxSystem mobMoverEntities.Add(uid); } - //Play the effect for the mobs as long as they can see the box and are in range. + // Play the effect for the mobs as long as they can see the box and are in range. foreach (var mob in mobMoverEntities) { var mapPos = _transform.GetMapCoordinates(mob); @@ -64,13 +65,11 @@ public sealed partial class CardboardBoxSystem : SharedCardboardBoxSystem continue; var ent = Spawn(box.Effect, mapPos); - if (!TryComp(ent, out TransformComponent? entTransform) || !TryComp(ent, out var sprite)) continue; _sprite.SetOffset((ent, sprite), new Vector2(0, 1)); _transform.SetParent(ent, entTransform, mob); } - } } diff --git a/Content.Client/CartridgeLoader/CartridgeLoaderBoundUserInterface.cs b/Content.Client/CartridgeLoader/CartridgeLoaderBoundUserInterface.cs index e4d6a9fb8b..41a46c9c6a 100644 --- a/Content.Client/CartridgeLoader/CartridgeLoaderBoundUserInterface.cs +++ b/Content.Client/CartridgeLoader/CartridgeLoaderBoundUserInterface.cs @@ -1,6 +1,5 @@ -using Content.Client.UserInterface.Fragments; +using Content.Client.UserInterface.Fragments; using Content.Shared.CartridgeLoader; -using Robust.Client.GameObjects; using Robust.Client.UserInterface; namespace Content.Client.CartridgeLoader; @@ -67,7 +66,7 @@ public abstract class CartridgeLoaderBoundUserInterface : BoundUserInterface protected void ActivateCartridge(EntityUid cartridgeUid) { var message = new CartridgeLoaderUiMessage(_entManager.GetNetEntity(cartridgeUid), CartridgeUiMessageAction.Activate); - SendMessage(message); + SendPredictedMessage(message); } protected void DeactivateActiveCartridge() @@ -76,19 +75,19 @@ public abstract class CartridgeLoaderBoundUserInterface : BoundUserInterface return; var message = new CartridgeLoaderUiMessage(_entManager.GetNetEntity(_activeProgram.Value), CartridgeUiMessageAction.Deactivate); - SendMessage(message); + SendPredictedMessage(message); } protected void InstallCartridge(EntityUid cartridgeUid) { var message = new CartridgeLoaderUiMessage(_entManager.GetNetEntity(cartridgeUid), CartridgeUiMessageAction.Install); - SendMessage(message); + SendPredictedMessage(message); } protected void UninstallCartridge(EntityUid cartridgeUid) { var message = new CartridgeLoaderUiMessage(_entManager.GetNetEntity(cartridgeUid), CartridgeUiMessageAction.Uninstall); - SendMessage(message); + SendPredictedMessage(message); } private List<(EntityUid, CartridgeComponent)> GetCartridgeComponents(List programs) diff --git a/Content.Client/CartridgeLoader/CartridgeLoaderSystem.cs b/Content.Client/CartridgeLoader/CartridgeLoaderSystem.cs deleted file mode 100644 index e06a547814..0000000000 --- a/Content.Client/CartridgeLoader/CartridgeLoaderSystem.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Content.Shared.CartridgeLoader; - -namespace Content.Client.CartridgeLoader; - -public sealed class CartridgeLoaderSystem : SharedCartridgeLoaderSystem -{ - //Empty client system for component replication -} diff --git a/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs b/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs index 12b678d5d4..183de499a8 100644 --- a/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs +++ b/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs @@ -22,7 +22,7 @@ public sealed partial class LogProbeUi : UIFragment { var ev = new LogProbePrintMessage(); var message = new CartridgeUiMessage(ev); - ui.SendMessage(message); + ui.SendPredictedMessage(message); }; } diff --git a/Content.Client/CartridgeLoader/Cartridges/NanoTaskCartridgeSystem.cs b/Content.Client/CartridgeLoader/Cartridges/NanoTaskCartridgeSystem.cs deleted file mode 100644 index 85a5659c4b..0000000000 --- a/Content.Client/CartridgeLoader/Cartridges/NanoTaskCartridgeSystem.cs +++ /dev/null @@ -1,5 +0,0 @@ -using Content.Shared.CartridgeLoader.Cartridges; - -namespace Content.Client.CartridgeLoader.Cartridges; - -public sealed class NanoTaskCartridgeSystem : SharedNanoTaskCartridgeSystem; diff --git a/Content.Client/CartridgeLoader/Cartridges/NanoTaskUi.cs b/Content.Client/CartridgeLoader/Cartridges/NanoTaskUi.cs index ac08051c89..ca29fe8474 100644 --- a/Content.Client/CartridgeLoader/Cartridges/NanoTaskUi.cs +++ b/Content.Client/CartridgeLoader/Cartridges/NanoTaskUi.cs @@ -1,8 +1,6 @@ -using System.Linq; using Content.Client.UserInterface.Fragments; using Content.Shared.CartridgeLoader; using Content.Shared.CartridgeLoader.Cartridges; -using Robust.Client.GameObjects; using Robust.Client.UserInterface; namespace Content.Client.CartridgeLoader.Cartridges; @@ -44,7 +42,7 @@ public sealed partial class NanoTaskUi : UIFragment if (_fragment.Tasks.Find(task => task.Id == id) is not NanoTaskItemAndId task) return; - userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskUpdateTask(new(id, new( + userInterface.SendPredictedMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskUpdateTask(new(id, new( description: task.Data.Description, taskIsFor: task.Data.TaskIsFor, isTaskDone: !task.Data.IsTaskDone, @@ -53,22 +51,22 @@ public sealed partial class NanoTaskUi : UIFragment }; _popup.TaskSaved += (id, data) => { - userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskUpdateTask(new(id, data))))); + userInterface.SendPredictedMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskUpdateTask(new(id, data))))); _popup.Close(); }; _popup.TaskDeleted += id => { - userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskDeleteTask(id)))); + userInterface.SendPredictedMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskDeleteTask(id)))); _popup.Close(); }; _popup.TaskCreated += data => { - userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskAddTask(data)))); + userInterface.SendPredictedMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskAddTask(data)))); _popup.Close(); }; _popup.TaskPrinted += data => { - userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskPrintTask(data)))); + userInterface.SendPredictedMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskPrintTask(data)))); }; } diff --git a/Content.Client/CartridgeLoader/Cartridges/NotekeeperUi.cs b/Content.Client/CartridgeLoader/Cartridges/NotekeeperUi.cs index 1018d88cce..da7041f4dd 100644 --- a/Content.Client/CartridgeLoader/Cartridges/NotekeeperUi.cs +++ b/Content.Client/CartridgeLoader/Cartridges/NotekeeperUi.cs @@ -1,7 +1,6 @@ using Content.Client.UserInterface.Fragments; using Content.Shared.CartridgeLoader; using Content.Shared.CartridgeLoader.Cartridges; -using Robust.Client.GameObjects; using Robust.Client.UserInterface; namespace Content.Client.CartridgeLoader.Cartridges; @@ -34,6 +33,6 @@ public sealed partial class NotekeeperUi : UIFragment { var notekeeperMessage = new NotekeeperUiMessageEvent(action, note); var message = new CartridgeUiMessage(notekeeperMessage); - userInterface.SendMessage(message); + userInterface.SendPredictedMessage(message); } } diff --git a/Content.Client/Changeling/Systems/ChangelingLastResortSystem.cs b/Content.Client/Changeling/Systems/ChangelingLastResortSystem.cs new file mode 100644 index 0000000000..62560c4310 --- /dev/null +++ b/Content.Client/Changeling/Systems/ChangelingLastResortSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Changeling.Systems; + +namespace Content.Client.Changeling.Systems; + +public sealed partial class ChangelingLastResortSystem : SharedChangelingLastResortSystem; diff --git a/Content.Client/Chasm/ChasmFallingVisualsSystem.cs b/Content.Client/Chasm/ChasmFallingVisualsSystem.cs index a9612add6e..feb306fdff 100644 --- a/Content.Client/Chasm/ChasmFallingVisualsSystem.cs +++ b/Content.Client/Chasm/ChasmFallingVisualsSystem.cs @@ -6,14 +6,17 @@ using Robust.Shared.Animations; namespace Content.Client.Chasm; /// -/// Handles the falling animation for entities that fall into a chasm. +/// Handles the falling animation for entities that fall into an entity with . /// public sealed partial class ChasmFallingVisualsSystem : EntitySystem { [Dependency] private AnimationPlayerSystem _anim = default!; [Dependency] private SpriteSystem _sprite = default!; - private readonly string _chasmFallAnimationKey = "chasm_fall"; + [Dependency] private EntityQuery _animationPlayerQuery; + [Dependency] private EntityQuery _spriteQuery; + + private const string ChasmFallAnimationKey = "chasm_fall"; public override void Initialize() { @@ -23,60 +26,62 @@ public sealed partial class ChasmFallingVisualsSystem : EntitySystem SubscribeLocalEvent(OnComponentRemove); } - private void OnComponentInit(EntityUid uid, ChasmFallingComponent component, ComponentInit args) + private void OnComponentInit(Entity entity, ref ComponentInit args) { - if (!TryComp(uid, out var sprite) || - TerminatingOrDeleted(uid)) + if (!_spriteQuery.TryComp(entity, out var sprite) || + TerminatingOrDeleted(entity)) { return; } - component.OriginalScale = sprite.Scale; + entity.Comp.OriginalScale = sprite.Scale; - if (!TryComp(uid, out var player)) - return; - - if (_anim.HasRunningAnimation(player, _chasmFallAnimationKey)) - return; - - _anim.Play((uid, player), GetFallingAnimation(component), _chasmFallAnimationKey); - } - - private void OnComponentRemove(EntityUid uid, ChasmFallingComponent component, ComponentRemove args) - { - if (!TryComp(uid, out var sprite)) - return; - - _sprite.SetScale((uid, sprite), component.OriginalScale); - - if (!TryComp(uid, out var player)) - return; - - if (_anim.HasRunningAnimation(player, _chasmFallAnimationKey)) - _anim.Stop((uid, player), _chasmFallAnimationKey); - } - - private Animation GetFallingAnimation(ChasmFallingComponent component) - { - var length = component.AnimationTime; - - return new Animation() + if (!_animationPlayerQuery.TryComp(entity, out var player) || + _anim.HasRunningAnimation(player, ChasmFallAnimationKey)) { - Length = length, + return; + } + + _anim.Play((entity, player), GetFallingAnimation(entity.Comp), ChasmFallAnimationKey); + } + + private void OnComponentRemove(Entity entity, ref ComponentRemove args) + { + if (!_spriteQuery.TryComp(entity, out var sprite)) + { + return; + } + + _sprite.SetScale((entity, sprite), entity.Comp.OriginalScale); + + if (!_animationPlayerQuery.TryComp(entity, out var player) || + !_anim.HasRunningAnimation(player, ChasmFallAnimationKey)) + { + return; + } + + _anim.Stop((entity, player), ChasmFallAnimationKey); + } + + private static Animation GetFallingAnimation(ChasmFallingComponent component) + { + return new Animation + { + Length = component.AnimationTime, AnimationTracks = { - new AnimationTrackComponentProperty() + new AnimationTrackComponentProperty { ComponentType = typeof(SpriteComponent), Property = nameof(SpriteComponent.Scale), KeyFrames = { new AnimationTrackProperty.KeyFrame(component.OriginalScale, 0.0f), - new AnimationTrackProperty.KeyFrame(component.AnimationScale, length.Seconds), + new AnimationTrackProperty.KeyFrame(component.AnimationScale, component.AnimationTime.Seconds), }, - InterpolationMode = AnimationInterpolationMode.Cubic - } - } + InterpolationMode = AnimationInterpolationMode.Cubic, + }, + }, }; } } diff --git a/Content.Client/Chemistry/UI/ReagentDispenserWindow.xaml b/Content.Client/Chemistry/UI/ReagentDispenserWindow.xaml index 1145d421a0..398c48caee 100644 --- a/Content.Client/Chemistry/UI/ReagentDispenserWindow.xaml +++ b/Content.Client/Chemistry/UI/ReagentDispenserWindow.xaml @@ -44,7 +44,8 @@ + VerticalExpand="True" + MouseFilter="Ignore"> (ent => new StatusControl(ent, _charges, _entityManager)); + Subs.ItemStatus(ent => new StatusControl(ent, _charges, EntityManager)); } private sealed class StatusControl : Control @@ -43,8 +42,8 @@ public sealed partial class CrayonSystem : SharedCrayonSystem base.FrameUpdate(args); _label.SetMarkup(Robust.Shared.Localization.Loc.GetString("crayon-drawing-label", - ("color",_crayon.Comp.Color), - ("state",_crayon.Comp.SelectedState), + ("color", _crayon.Comp.Color), + ("state", _crayon.Comp.SelectedState), ("charges", _charges.GetCurrentCharges(_crayon.Owner)), ("capacity", _capacity))); } diff --git a/Content.Client/Damage/DamageVisualsComponent.cs b/Content.Client/Damage/DamageVisualsComponent.cs index 26356496bb..a092e30337 100644 --- a/Content.Client/Damage/DamageVisualsComponent.cs +++ b/Content.Client/Damage/DamageVisualsComponent.cs @@ -1,4 +1,6 @@ +using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; +using Content.Shared.DisplacementMap; using Content.Shared.FixedPoint; using Robust.Shared.Prototypes; @@ -117,6 +119,13 @@ public sealed partial class DamageVisualsComponent : Component /// [DataField("damageOverlay")] public DamageVisualizerSprite? DamageOverlay; + /// + /// Applies a displacement map to the damage visuals. + /// Is set via . + /// + [DataField] + public DisplacementData? Displacement; + public readonly List TargetLayerMapKeys = new(); public bool Disabled = false; public bool Valid = true; diff --git a/Content.Client/Damage/DamageVisualsSystem.cs b/Content.Client/Damage/DamageVisualsSystem.cs index b374831059..13ff8a0137 100644 --- a/Content.Client/Damage/DamageVisualsSystem.cs +++ b/Content.Client/Damage/DamageVisualsSystem.cs @@ -1,8 +1,10 @@ using System.Linq; +using Content.Client.DisplacementMap; using Content.Shared.Damage; using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; using Content.Shared.Damage.Systems; +using Content.Shared.DisplacementMap; using Content.Shared.FixedPoint; using Robust.Client.GameObjects; using Robust.Shared.Prototypes; @@ -29,6 +31,7 @@ namespace Content.Client.Damage; public sealed partial class DamageVisualsSystem : VisualizerSystem { [Dependency] private DamageableSystem _damageable = default!; + [Dependency] private DisplacementMapSystem _displacement = default!; public override void Initialize() { @@ -326,10 +329,14 @@ public sealed partial class DamageVisualsSystem : VisualizerSystem private void AddDamageLayerToSprite(Entity spriteEnt, DamageVisualizerSprite sprite, string state, string mapKey, int? index = null) { + if (!Resolve(spriteEnt, ref spriteEnt.Comp)) + return; + var newLayer = SpriteSystem.AddLayer( spriteEnt, new SpriteSpecifier.Rsi( - new(sprite.Sprite), state + new(sprite.Sprite), + state ), index ); @@ -354,6 +361,12 @@ public sealed partial class DamageVisualsSystem : VisualizerSystem(uid, DamageVisualizerKeys.Displacement, out var displacement, args.Component) && + ProtoMan.Resolve(displacement, out var displacementProto)) + damageVisComp.Displacement = displacementProto.Displacement; + else + damageVisComp.Displacement = null; + HandleDamage(uid, args.Component, damageVisComp); } @@ -503,7 +516,7 @@ public sealed partial class DamageVisualsSystem : VisualizerSystem /// Updates an overlay that is tracking all damage. /// - private void UpdateOverlay(Entity spriteEnt, FixedPoint2 threshold) + private void UpdateOverlay(Entity spriteEnt, FixedPoint2 threshold, DisplacementData? displacement = null) { SpriteSystem.LayerMapTryGet(spriteEnt.AsNullable(), $"DamageOverlay", out var spriteLayer, false); UpdateDamageLayerState(spriteEnt, spriteLayer, $"DamageOverlay", - threshold); + threshold, + $"DamageOverlay", + displacement); } /// /// Updates an overlay based on damage group. /// - private void UpdateOverlay(Entity entity, string damageGroup, FixedPoint2 threshold) + private void UpdateOverlay(Entity entity, string damageGroup, FixedPoint2 threshold, DisplacementData? displacement = null) { var spriteComponent = entity.Comp1; var damageVisComp = entity.Comp2; @@ -695,7 +718,9 @@ public sealed partial class DamageVisualsSystem : VisualizerSystem - private void UpdateDamageLayerState(Entity spriteEnt, int spriteLayer, string statePrefix, FixedPoint2 threshold) + private void UpdateDamageLayerState(Entity spriteEnt, int spriteLayer, string statePrefix, FixedPoint2 threshold, string layerKey, DisplacementData? displacement) { if (threshold == 0) { @@ -719,6 +744,14 @@ public sealed partial class DamageVisualsSystem : VisualizerSystem /// The sprite to remove the displacement layer from. /// The key of the layer that is referenced by the displacement layer we want to remove. - /// Whether to report an error if the displacement map isn't on the sprite. - public void EnsureDisplacementIsNotOnSprite(Entity sprite, object key) + /// Returns true if the displacement existed and was removed. + public bool EnsureDisplacementIsNotOnSprite(Entity sprite, object key) { var displacementLayerKey = BuildDisplacementLayerKey(key); if (displacementLayerKey is null) - return; + return false; - _sprite.RemoveLayer(sprite.AsNullable(), displacementLayerKey, false); + return _sprite.RemoveLayer(sprite.AsNullable(), displacementLayerKey, false); } } diff --git a/Content.Client/Drugs/RainbowOverlay.cs b/Content.Client/Drugs/RainbowOverlay.cs index 68a00c050c..1884ead4bb 100644 --- a/Content.Client/Drugs/RainbowOverlay.cs +++ b/Content.Client/Drugs/RainbowOverlay.cs @@ -44,7 +44,7 @@ public sealed partial class RainbowOverlay : Overlay _statusEffects = _sysMan.GetEntitySystem(); _rainbowShader = _prototypeManager.Index(Shader).InstanceUnique(); - _config.OnValueChanged(CCVars.ReducedMotion, OnReducedMotionChanged, invokeImmediately: true); + _config.OnValueChanged(CCVars.DisableRainbowOverlay, OnReducedMotionChanged, invokeImmediately: true); } private void OnReducedMotionChanged(bool reducedMotion) diff --git a/Content.Client/Drunk/DrunkOverlay.cs b/Content.Client/Drunk/DrunkOverlay.cs index 12974049f5..80917c8970 100644 --- a/Content.Client/Drunk/DrunkOverlay.cs +++ b/Content.Client/Drunk/DrunkOverlay.cs @@ -50,7 +50,7 @@ public sealed partial class DrunkOverlay : Overlay IoCManager.InjectDependencies(this); _statusEffectsSystem = _entityManager.System(); _drunkShader = _prototypeManager.Index(DrunkShader).InstanceUnique(); - _configManager.OnValueChanged(CCVars.ReducedMotion, OnReducedMotionChanged, invokeImmediately: true); + _configManager.OnValueChanged(CCVars.DisableDrunkOverlay, OnReducedMotionChanged, invokeImmediately: true); } private void OnReducedMotionChanged(bool reducedMotion) diff --git a/Content.Client/Eye/Blinding/BlurryVisionOverlay.cs b/Content.Client/Eye/Blinding/BlurryVisionOverlay.cs index ee7c9eef01..1c14dfd6d5 100644 --- a/Content.Client/Eye/Blinding/BlurryVisionOverlay.cs +++ b/Content.Client/Eye/Blinding/BlurryVisionOverlay.cs @@ -92,7 +92,7 @@ namespace Content.Client.Eye.Blinding // While the cataracts shader is designed to be tame enough to keep motion sickness at bay, the general waviness means that those who are particularly sensitive to motion sickness will probably hurl. // So the reasonable alternative here is to replace it with a static effect! Specifically, one that replicates the blindness effect seen across most SS13 servers. - if (_configManager.GetCVar(CCVars.ReducedMotion)) + if (_configManager.GetCVar(CCVars.DisableBlurryVision)) { _circleMaskShader.SetParameter("SCREEN_TEXTURE", ScreenTexture); _circleMaskShader.SetParameter("Zoom", zoom); diff --git a/Content.Client/Fax/System/FaxVisualsSystem.cs b/Content.Client/Fax/System/FaxVisualsSystem.cs index 19b18da8da..b9431b9706 100644 --- a/Content.Client/Fax/System/FaxVisualsSystem.cs +++ b/Content.Client/Fax/System/FaxVisualsSystem.cs @@ -42,8 +42,7 @@ public sealed partial class FaxVisualsSystem : EntitySystem LayerKey = FaxMachineVisuals.VisualState, KeyFrames = { - new AnimationTrackSpriteFlick.KeyFrame(component.InsertingState, 0f), - new AnimationTrackSpriteFlick.KeyFrame("icon", 2.4f), + new AnimationTrackSpriteFlick.KeyFrame(component.InsertingState, 0f) }, }, }, diff --git a/Content.Client/Flash/FlashOverlay.cs b/Content.Client/Flash/FlashOverlay.cs index c833ad69ac..844c1c3f0c 100644 --- a/Content.Client/Flash/FlashOverlay.cs +++ b/Content.Client/Flash/FlashOverlay.cs @@ -1,7 +1,7 @@ using Content.Shared.CCVar; using Content.Shared.Flash; using Content.Shared.Flash.Components; -using Content.Shared.StatusEffect; +using Content.Shared.StatusEffectNew; using Robust.Client.Graphics; using Robust.Client.Player; using Robust.Shared.Configuration; @@ -9,101 +9,100 @@ using Robust.Shared.Enums; using Robust.Shared.Prototypes; using Robust.Shared.Timing; -namespace Content.Client.Flash +namespace Content.Client.Flash; + +public sealed partial class FlashOverlay : Overlay { - public sealed partial class FlashOverlay : Overlay + private static readonly ProtoId FlashedEffectShader = "FlashedEffect"; + + [Dependency] private IPrototypeManager _prototypeManager = default!; + [Dependency] private IEntityManager _entityManager = default!; + [Dependency] private IPlayerManager _playerManager = default!; + [Dependency] private IGameTiming _timing = default!; + [Dependency] private IConfigurationManager _configManager = default!; + + private readonly StatusEffectsSystem _statusSys; + + public override OverlaySpace Space => OverlaySpace.WorldSpace; + private readonly ShaderInstance _shader; + private bool _reducedMotion; + public float PercentComplete; + public Texture? ScreenshotTexture; + + public FlashOverlay() { - private static readonly ProtoId FlashedEffectShader = "FlashedEffect"; + IoCManager.InjectDependencies(this); + _shader = _prototypeManager.Index(FlashedEffectShader).InstanceUnique(); + _statusSys = _entityManager.System(); - [Dependency] private IPrototypeManager _prototypeManager = default!; - [Dependency] private IEntityManager _entityManager = default!; - [Dependency] private IPlayerManager _playerManager = default!; - [Dependency] private IGameTiming _timing = default!; - [Dependency] private IConfigurationManager _configManager = default!; + _configManager.OnValueChanged(CCVars.DisableFlashEffect, (b) => { _reducedMotion = b; }, invokeImmediately: true); + } - private readonly SharedFlashSystem _flash; - private readonly StatusEffectsSystem _statusSys; + protected override void FrameUpdate(FrameEventArgs args) + { + var playerEntity = _playerManager.LocalEntity; - public override OverlaySpace Space => OverlaySpace.WorldSpace; - private readonly ShaderInstance _shader; - private bool _reducedMotion; - public float PercentComplete; - public Texture? ScreenshotTexture; + if (playerEntity == null) + return; - public FlashOverlay() + if (!_statusSys.HasEffectComp(playerEntity)) + return; + + if (!_statusSys.TryGetTime(playerEntity.Value, SharedFlashSystem.FlashedKey, out var time)) + return; + + var curTime = _timing.CurTime; + if (time.StartEffectTime == null || time.EndEffectTime == null) + return; + + var lastsFor = (float)(time.EndEffectTime.Value - time.StartEffectTime.Value).TotalSeconds; + var timeDone = (float)(curTime - time.StartEffectTime.Value).TotalSeconds; + + PercentComplete = timeDone / lastsFor; + } + + protected override bool BeforeDraw(in OverlayDrawArgs args) + { + if (!_entityManager.TryGetComponent(_playerManager.LocalEntity, out EyeComponent? eyeComp)) + return false; + if (args.Viewport.Eye != eyeComp.Eye) + return false; + + return PercentComplete < 1.0f; + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (RequestScreenTexture && ScreenTexture != null) { - IoCManager.InjectDependencies(this); - _shader = _prototypeManager.Index(FlashedEffectShader).InstanceUnique(); - _flash = _entityManager.System(); - _statusSys = _entityManager.System(); - - _configManager.OnValueChanged(CCVars.ReducedMotion, (b) => { _reducedMotion = b; }, invokeImmediately: true); + ScreenshotTexture = ScreenTexture; + RequestScreenTexture = false; // we only need the first frame, so we can stop the request now for performance reasons } + if (ScreenshotTexture == null) + return; - protected override void FrameUpdate(FrameEventArgs args) + var worldHandle = args.WorldHandle; + if (_reducedMotion) { - var playerEntity = _playerManager.LocalEntity; - - if (playerEntity == null) - return; - - if (!_entityManager.HasComponent(playerEntity) - || !_entityManager.TryGetComponent(playerEntity, out var status)) - return; - - if (!_statusSys.TryGetTime(playerEntity.Value, _flash.FlashedKey, out var time, status)) - return; - - var curTime = _timing.CurTime; - var lastsFor = (float)(time.Value.Item2 - time.Value.Item1).TotalSeconds; - var timeDone = (float)(curTime - time.Value.Item1).TotalSeconds; - - PercentComplete = timeDone / lastsFor; + // TODO: This is a very simple placeholder. + // Replace it with a proper shader once we come up with something good. + // Turns out making an effect that is supposed to be a bright, sudden, and disorienting flash + // not do any of that while also being equivalent in terms of game balance is hard. + var alpha = 1 - MathF.Pow(PercentComplete, 8f); // similar falloff curve to the flash shader + worldHandle.DrawRect(args.WorldBounds, new Color(0f, 0f, 0f, alpha)); } - - protected override bool BeforeDraw(in OverlayDrawArgs args) + else { - if (!_entityManager.TryGetComponent(_playerManager.LocalEntity, out EyeComponent? eyeComp)) - return false; - if (args.Viewport.Eye != eyeComp.Eye) - return false; - - return PercentComplete < 1.0f; - } - - protected override void Draw(in OverlayDrawArgs args) - { - if (RequestScreenTexture && ScreenTexture != null) - { - ScreenshotTexture = ScreenTexture; - RequestScreenTexture = false; // we only need the first frame, so we can stop the request now for performance reasons - } - if (ScreenshotTexture == null) - return; - - var worldHandle = args.WorldHandle; - if (_reducedMotion) - { - // TODO: This is a very simple placeholder. - // Replace it with a proper shader once we come up with something good. - // Turns out making an effect that is supposed to be a bright, sudden, and disorienting flash - // not do any of that while also being equivalent in terms of game balance is hard. - var alpha = 1 - MathF.Pow(PercentComplete, 8f); // similar falloff curve to the flash shader - worldHandle.DrawRect(args.WorldBounds, new Color(0f, 0f, 0f, alpha)); - } - else - { - _shader.SetParameter("percentComplete", PercentComplete); - worldHandle.UseShader(_shader); - worldHandle.DrawTextureRectRegion(ScreenshotTexture, args.WorldBounds); - worldHandle.UseShader(null); - } - } - - protected override void DisposeBehavior() - { - base.DisposeBehavior(); - ScreenshotTexture = null; + _shader.SetParameter("percentComplete", PercentComplete); + worldHandle.UseShader(_shader); + worldHandle.DrawTextureRectRegion(ScreenshotTexture, args.WorldBounds); + worldHandle.UseShader(null); } } + + protected override void DisposeBehavior() + { + base.DisposeBehavior(); + ScreenshotTexture = null; + } } diff --git a/Content.Client/Flash/FlashSystem.cs b/Content.Client/Flash/FlashSystem.cs index 22631b7ec5..59db0f9605 100644 --- a/Content.Client/Flash/FlashSystem.cs +++ b/Content.Client/Flash/FlashSystem.cs @@ -1,5 +1,6 @@ using Content.Shared.Flash; using Content.Shared.Flash.Components; +using Content.Shared.StatusEffectNew; using Robust.Client.Graphics; using Robust.Client.Player; using Robust.Shared.Player; @@ -10,49 +11,55 @@ public sealed partial class FlashSystem : SharedFlashSystem { [Dependency] private IPlayerManager _player = default!; [Dependency] private IOverlayManager _overlayMan = default!; + [Dependency] private StatusEffectsSystem _statusEffects = default!; private FlashOverlay _overlay = default!; public override void Initialize() { base.Initialize(); - - SubscribeLocalEvent(OnInit); - SubscribeLocalEvent(OnShutdown); - SubscribeLocalEvent(OnPlayerAttached); - SubscribeLocalEvent(OnPlayerDetached); - _overlay = new(); } - private void OnPlayerAttached(EntityUid uid, FlashedComponent component, LocalPlayerAttachedEvent args) + [SubscribeLocalEvent] + private void OnApplied(Entity ent, ref StatusEffectAppliedEvent args) { - _overlayMan.AddOverlay(_overlay); - } - - private void OnPlayerDetached(EntityUid uid, FlashedComponent component, LocalPlayerDetachedEvent args) - { - _overlay.ScreenshotTexture = null; - _overlay.RequestScreenTexture = false; - _overlayMan.RemoveOverlay(_overlay); - } - - private void OnInit(EntityUid uid, FlashedComponent component, ComponentInit args) - { - if (_player.LocalEntity == uid) + if (_player.LocalEntity == args.Target) { _overlay.RequestScreenTexture = true; _overlayMan.AddOverlay(_overlay); } } - private void OnShutdown(EntityUid uid, FlashedComponent component, ComponentShutdown args) + [SubscribeLocalEvent] + private void OnRemoved(Entity ent, ref StatusEffectRemovedEvent args) { - if (_player.LocalEntity == uid) + if (_player.LocalEntity != args.Target) + return; + + if (!_statusEffects.HasEffectComp(args.Target)) { _overlay.ScreenshotTexture = null; _overlay.RequestScreenTexture = false; _overlayMan.RemoveOverlay(_overlay); } } + + [SubscribeLocalEvent] + private void OnPlayerAttached(Entity ent, ref StatusEffectRelayedEvent args) + { + _overlay.RequestScreenTexture = true; + _overlayMan.AddOverlay(_overlay); + } + + [SubscribeLocalEvent] + private void OnPlayerDetached(Entity ent, ref StatusEffectRelayedEvent args) + { + if (_player.LocalEntity is null || _statusEffects.HasEffectComp(_player.LocalEntity.Value)) + return; + + _overlay.ScreenshotTexture = null; + _overlay.RequestScreenTexture = false; + _overlayMan.RemoveOverlay(_overlay); + } } diff --git a/Content.Client/Ghost/GhostSystem.cs b/Content.Client/Ghost/GhostSystem.cs index d4b1dfca0e..ab60e18654 100644 --- a/Content.Client/Ghost/GhostSystem.cs +++ b/Content.Client/Ghost/GhostSystem.cs @@ -1,6 +1,8 @@ using Content.Client.Movement.Systems; using Content.Shared.Actions; using Content.Shared.Ghost; +using Content.Shared.NightVision; +using Content.Shared.Overlays; using Robust.Client.Console; using Robust.Client.GameObjects; using Robust.Client.Player; @@ -13,9 +15,9 @@ namespace Content.Client.Ghost [Dependency] private IClientConsoleHost _console = default!; [Dependency] private IPlayerManager _playerManager = default!; [Dependency] private SharedActionsSystem _actions = default!; - [Dependency] private PointLightSystem _pointLightSystem = default!; [Dependency] private ContentEyeSystem _contentEye = default!; [Dependency] private SpriteSystem _sprite = default!; + [Dependency] private SharedNightVisionSystem _nv = default!; public int AvailableGhostRoleCount { get; private set; } @@ -81,27 +83,25 @@ namespace Content.Client.Ghost if (args.Handled) return; - TryComp(uid, out var light); - if (!component.DrawLight) { // normal lighting Popup.PopupEntity(Loc.GetString("ghost-gui-toggle-lighting-manager-popup-normal"), args.Performer); _contentEye.RequestEye(component.DrawFov, true); } - else if (!light?.Enabled ?? false) // skip this option if we have no PointLightComponent + else if (TryComp(uid, out var nv) && !nv.Enabled) { - // enable personal light - Popup.PopupEntity(Loc.GetString("ghost-gui-toggle-lighting-manager-popup-personal-light"), args.Performer); - _pointLightSystem.SetEnabled(uid, true, light); + Popup.PopupEntity(Loc.GetString("ghost-gui-toggle-lighting-manager-popup-half-bright"), args.Performer); + _nv.SetEnabled((uid, nv), true); } else { // fullbright mode Popup.PopupEntity(Loc.GetString("ghost-gui-toggle-lighting-manager-popup-fullbright"), args.Performer); _contentEye.RequestEye(component.DrawFov, false); - _pointLightSystem.SetEnabled(uid, false, light); + _nv.SetEnabled((uid, nv), false); } + args.Handled = true; } diff --git a/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml.cs b/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml.cs index 66d3f693b8..464f700b06 100644 --- a/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml.cs +++ b/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml.cs @@ -36,7 +36,7 @@ public sealed partial class GuideReagentReaction : BoxContainer, ISearchableCont Container container = ReactantsContainer; SetReagents(prototype.Reactants, ref container, protoMan); Container productContainer = ProductsContainer; - var products = new Dictionary(prototype.Products); + var products = new Dictionary, FixedPoint2>(prototype.Products); foreach (var (reagent, reactantProto) in prototype.Reactants) { if (reactantProto.Catalyst) @@ -101,11 +101,11 @@ public sealed partial class GuideReagentReaction : BoxContainer, ISearchableCont ReactantsContainer.Visible = true; ReactantsContainer.AddChild(label); - if (prototype.Reagent != null) + if (prototype.Reagent is {} reagent) { - var quantity = new Dictionary + var quantity = new Dictionary, FixedPoint2> { - { prototype.Reagent, FixedPoint2.New(0.21f) } + { reagent, FixedPoint2.New(0.21f) } }; Container productContainer = ProductsContainer; SetReagents(quantity, ref productContainer, protoMan, false); @@ -113,9 +113,9 @@ public sealed partial class GuideReagentReaction : BoxContainer, ISearchableCont SetMixingCategory(categories, null, sysMan); } - private void SetReagents(List reagents, ref Container container, IPrototypeManager protoMan, bool addLinks = true) + private static void SetReagents(List reagents, ref Container container, IPrototypeManager protoMan, bool addLinks = true) { - var amounts = new Dictionary(); + var amounts = new Dictionary, FixedPoint2>(); foreach (var (reagent, quantity) in reagents) { amounts.Add(reagent.Prototype, quantity); @@ -123,13 +123,13 @@ public sealed partial class GuideReagentReaction : BoxContainer, ISearchableCont SetReagents(amounts, ref container, protoMan, addLinks); } - private void SetReagents( - Dictionary reactants, + private static void SetReagents( + Dictionary, ReactantInfo> reactants, ref Container container, IPrototypeManager protoMan, bool addLinks = true) { - var amounts = new Dictionary(); + var amounts = new Dictionary, FixedPoint2>(); foreach (var (reagent, reactantPrototype) in reactants) { amounts.Add(reagent, reactantPrototype.Amount); @@ -137,22 +137,7 @@ public sealed partial class GuideReagentReaction : BoxContainer, ISearchableCont SetReagents(amounts, ref container, protoMan, addLinks); } - [PublicAPI] - private void SetReagents( - Dictionary, ReactantPrototype> reactants, - ref Container container, - IPrototypeManager protoMan, - bool addLinks = true) - { - var amounts = new Dictionary(); - foreach (var (reagent, reactantPrototype) in reactants) - { - amounts.Add(reagent, reactantPrototype.Amount); - } - SetReagents(amounts, ref container, protoMan, addLinks); - } - - private void SetReagents(Dictionary reagents, ref Container container, IPrototypeManager protoMan, bool addLinks = true) + private static void SetReagents(Dictionary, FixedPoint2> reagents, ref Container container, IPrototypeManager protoMan, bool addLinks = true) { foreach (var (product, amount) in reagents.OrderByDescending(p => p.Value)) { diff --git a/Content.Client/Guidebook/Controls/GuidebookWindow.xaml b/Content.Client/Guidebook/Controls/GuidebookWindow.xaml index 69534af7f6..9ee62e503b 100644 --- a/Content.Client/Guidebook/Controls/GuidebookWindow.xaml +++ b/Content.Client/Guidebook/Controls/GuidebookWindow.xaml @@ -13,6 +13,7 @@ + , GuideEntry> _entries = new(); + private Dictionary, GuideEntry> _entries = []; private readonly ISawmill _sawmill; - public ProtoId LastEntry; + public ProtoId? Selected { get; private set; } + public GuidebookWindow() { @@ -34,6 +34,7 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IA _sawmill = Logger.GetSawmill("guidebook"); Tree.OnSelectedItemChanged += OnSelectionChanged; + TableOfContents.OnSelectedItemChanged += OnTableOfContentsSelectionChanged; SearchBar.OnTextChanged += _ => { @@ -76,7 +77,7 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IA UserInterfaceManager.DeferAction(() => { - if (control.GetControlScrollPosition() is not {} position) + if (control.GetControlScrollPosition() is not { } position) return; Scroll.HScrollTarget = position.X; @@ -91,6 +92,10 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IA { if (item != null && item.Metadata is GuideEntry entry) { + // do nothing if the guide is the same as the currently selected one + if (entry.Id == Selected) + return; + ShowGuide(entry); var isRulesEntry = entry.RuleEntry; @@ -101,12 +106,29 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IA ClearSelectedGuide(); } + private void OnTableOfContentsSelectionChanged(TreeItem? item) + { + if (item is null || item.Metadata is not Label entry) + return; + + UserInterfaceManager.DeferAction(() => + { + if (entry.GetControlScrollPosition() is not { } position) + return; + + Scroll.HScrollTarget = position.X; + Scroll.VScrollTarget = position.Y; + }); + } + public void ClearSelectedGuide() { Placeholder.Visible = true; EntryContainer.Visible = false; SearchContainer.Visible = false; EntryContainer.RemoveAllChildren(); + + Selected = null; } private void ShowGuide(GuideEntry entry) @@ -127,11 +149,11 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IA _sawmill.Error($"Failed to parse contents of guide document {entry.Id}."); } - LastEntry = entry.Id; + Selected = entry.Id; var (linkableControls, linkControls) = GetLinkableControlsAndLinks(EntryContainer); - HashSet availablePrototypeLinks = new(); + HashSet availablePrototypeLinks = []; foreach (var linkableControl in linkableControls) { var prototype = linkableControl.RepresentedPrototype; @@ -145,48 +167,108 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IA if (prototype != null && availablePrototypeLinks.Contains(prototype)) linkControl.EnablePrototypeLink(); } + + RepopulateTableOfContents(); } - public void UpdateGuides( + private int? HeadingDepth(Label control) + { + if (control.StyleClasses.Contains("LabelHeadingBigger")) + return 1; + else if (control.StyleClasses.Contains("LabelHeading")) + return 2; + else if (control.StyleClasses.Contains("LabelKeyText")) + return 3; + + return null; + } + + private void RepopulateTableOfContents() + { + TableOfContents.Clear(); + + var firstEntry = TableOfContents.AddItem(null); + firstEntry.Label.Text = Loc.GetString("guidebook-toc-header"); + + var labels = EntryContainer.GetControlOfType