diff --git a/.github/ISSUE_TEMPLATE/heisentest_template.md b/.github/ISSUE_TEMPLATE/heisentest_template.md new file mode 100644 index 00000000000..2fde892b79e --- /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 c846d99fa90..00000000000 --- 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 84bc8554377..00000000000 --- 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@v4.2.2 - 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 dee31e9b312..00000000000 --- 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@v4.2.2 - - 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@v4.1.0 - 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/build-map-renderer.yml b/.github/workflows/build-map-renderer.yml index fb6be1603c1..7e9e203f266 100644 --- a/.github/workflows/build-map-renderer.yml +++ b/.github/workflows/build-map-renderer.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Checkout Master - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v7 - name: Setup Submodule run: | @@ -34,7 +34,7 @@ jobs: git submodule update --init --recursive - name: Setup .NET Core - uses: actions/setup-dotnet@v4.1.0 + uses: actions/setup-dotnet@v5 with: dotnet-version: 10.0.x diff --git a/.github/workflows/build-test-debug.yml b/.github/workflows/build-test-debug.yml index cfd39c3fff7..a459740e68c 100644 --- a/.github/workflows/build-test-debug.yml +++ b/.github/workflows/build-test-debug.yml @@ -16,10 +16,11 @@ jobs: os: [ubuntu-latest] runs-on: ${{ matrix.os }} + timeout-minutes: 30 steps: - name: Checkout Master - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v7 - name: Setup Submodule run: | @@ -34,7 +35,7 @@ jobs: git submodule update --init --recursive - name: Setup .NET Core - uses: actions/setup-dotnet@v4.1.0 + uses: actions/setup-dotnet@v5 with: dotnet-version: 10.0.x diff --git a/.github/workflows/check-crlf.yml b/.github/workflows/check-crlf.yml index 15e21f3f396..552ca4b0978 100644 --- a/.github/workflows/check-crlf.yml +++ b/.github/workflows/check-crlf.yml @@ -10,6 +10,6 @@ jobs: if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v7 - name: Check for CRLF run: Tools/check_crlf.py diff --git a/.github/workflows/heisendetector.yml b/.github/workflows/heisendetector.yml new file mode 100644 index 00000000000..e26f9a8c268 --- /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/.github/workflows/publish-testing.yml b/.github/workflows/publish-testing.yml index 1b1344315a4..d60ab6a5afe 100644 --- a/.github/workflows/publish-testing.yml +++ b/.github/workflows/publish-testing.yml @@ -14,14 +14,17 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3.6.0 + - uses: actions/checkout@v7 with: submodules: 'recursive' - name: Setup .NET Core - uses: actions/setup-dotnet@v3.2.0 + uses: actions/setup-dotnet@v5 with: dotnet-version: 10.0.x + - name: Install dependencies + run: sudo apt-get install -y python3-paramiko python3-lxml + - name: Get Engine Tag run: | cd RobustToolbox @@ -44,3 +47,9 @@ jobs: env: PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} GITHUB_REPOSITORY: ${{ vars.GITHUB_REPOSITORY }} + + - name: Publish changelog (RSS) + continue-on-error: true + run: Tools/actions_changelog_rss.py + env: + CHANGELOG_RSS_KEY: ${{ secrets.CHANGELOG_RSS_KEY }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e7359c3be8f..1910a630ad7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,11 +21,11 @@ jobs: - name: Install dependencies run: sudo apt-get install -y python3-paramiko python3-lxml - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v7 with: submodules: 'recursive' - name: Setup .NET Core - uses: actions/setup-dotnet@v4.1.0 + uses: actions/setup-dotnet@v5 with: dotnet-version: 10.0.x diff --git a/.github/workflows/rsi-diff.yml b/.github/workflows/rsi-diff.yml index 390ddcda6e7..8ae6105ad3d 100644 --- a/.github/workflows/rsi-diff.yml +++ b/.github/workflows/rsi-diff.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v7 - name: Get changed files id: files diff --git a/.github/workflows/test-packaging.yml b/.github/workflows/test-packaging.yml index 46ce13013a2..2bddfce1ce7 100644 --- a/.github/workflows/test-packaging.yml +++ b/.github/workflows/test-packaging.yml @@ -34,7 +34,7 @@ jobs: steps: - name: Checkout Master - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v7 - name: Setup Submodule run: | @@ -50,7 +50,7 @@ jobs: # ubuntu-latest has .NET 10 # - name: Setup .NET Core - # uses: actions/setup-dotnet@v4.1.0 + # uses: actions/setup-dotnet@v5 # with: # dotnet-version: 10.0.x diff --git a/.github/workflows/update-credits.yml b/.github/workflows/update-credits.yml index eb609556d43..f6eddf7d743 100644 --- a/.github/workflows/update-credits.yml +++ b/.github/workflows/update-credits.yml @@ -13,7 +13,7 @@ jobs: if: github.repository == 'space-wizards/space-station-14' steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v7 with: ref: master diff --git a/.github/workflows/validate-rgas.yml b/.github/workflows/validate-rgas.yml index df58b97aff6..9e770bd4185 100644 --- a/.github/workflows/validate-rgas.yml +++ b/.github/workflows/validate-rgas.yml @@ -12,7 +12,7 @@ jobs: if: github.actor != 'PJBot' && github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v7 - name: Setup Submodule run: git submodule update --init - name: Pull engine updates diff --git a/.github/workflows/validate-rsis.yml b/.github/workflows/validate-rsis.yml index 845000d6df8..bcc67c7c15c 100644 --- a/.github/workflows/validate-rsis.yml +++ b/.github/workflows/validate-rsis.yml @@ -13,7 +13,7 @@ jobs: name: Validate RSIs runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v7 - name: Setup Submodule run: git submodule update --init - name: Pull engine updates diff --git a/.github/workflows/validate_mapfiles.yml b/.github/workflows/validate_mapfiles.yml index 53fa3b4ae45..026c1818003 100644 --- a/.github/workflows/validate_mapfiles.yml +++ b/.github/workflows/validate_mapfiles.yml @@ -12,7 +12,7 @@ jobs: if: github.actor != 'PJBot' && github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v7 - name: Setup Submodule run: git submodule update --init - name: Pull engine updates diff --git a/.github/workflows/yaml-linter.yml b/.github/workflows/yaml-linter.yml index d8ce557134f..dec069dfd71 100644 --- a/.github/workflows/yaml-linter.yml +++ b/.github/workflows/yaml-linter.yml @@ -13,7 +13,7 @@ jobs: if: github.actor != 'PJBot' && github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v7 - name: Setup submodule run: | git submodule update --init --recursive @@ -24,7 +24,7 @@ jobs: cd RobustToolbox/ git submodule update --init --recursive - name: Setup .NET Core - uses: actions/setup-dotnet@v4.1.0 + uses: actions/setup-dotnet@v5 with: dotnet-version: 10.0.x - name: Install dependencies diff --git a/.noai b/.noai new file mode 100644 index 00000000000..e69de29bb2d diff --git a/BuildChecker/git_helper.py b/BuildChecker/git_helper.py index 4e42b1e70e3..b5fe56a00a6 100644 --- a/BuildChecker/git_helper.py +++ b/BuildChecker/git_helper.py @@ -49,9 +49,6 @@ def update_submodules(): if os.path.isfile("DISABLE_SUBMODULE_AUTOUPDATE"): return - if shutil.which("git") is None: - raise FileNotFoundError("git not found in PATH") - # If the status doesn't match, force VS to reload the solution. # status = run_command(["git", "submodule", "status"], capture=True) run_command(["git", "submodule", "update", "--init", "--recursive"]) @@ -112,12 +109,19 @@ def check_for_zip_download(): "Such as information to download the engine or even the ability to even be able to create contributions. \n" "Please read and follow https://docs.spacestation14.com/en/general-development/setup/setting-up-a-development-environment.html \n" "If you just want a Sandbox Server, you are following the wrong guide! You can download a premade server following the instructions here:" - "https://docs.spacestation14.com/en/general-development/setup/server-hosting-tutorial.html \n" - "Closing automatically in 30 seconds.") - time.sleep(30) + "https://docs.spacestation14.com/en/general-development/setup/server-hosting-tutorial.html") + exit(1) + +def check_path_for_git(): + """ + Check git is invokable before trying to invoke it. + """ + if shutil.which("git") is None: + print("git not found in PATH. Ensure git is installed and in PATH and run this program again!") exit(1) if __name__ == '__main__': + check_path_for_git() check_for_zip_download() install_hooks() update_submodules() diff --git a/Content.Benchmarks/ColorInterpolateBenchmark.cs b/Content.Benchmarks/ColorInterpolateBenchmark.cs index eb182328d46..45f2c21b9a0 100644 --- a/Content.Benchmarks/ColorInterpolateBenchmark.cs +++ b/Content.Benchmarks/ColorInterpolateBenchmark.cs @@ -30,7 +30,8 @@ public class ColorInterpolateBenchmark [GlobalSetup] public void Setup() { - var random = new Random(3005); + var random = new RobustRandom(); + random.SetSeed(3005); _colors = new (Color, Color)[N]; _output = new Color[N]; diff --git a/Content.Benchmarks/DeltaPressureBenchmark.cs b/Content.Benchmarks/DeltaPressureBenchmark.cs index dac79e03760..f4ae62a4652 100644 --- a/Content.Benchmarks/DeltaPressureBenchmark.cs +++ b/Content.Benchmarks/DeltaPressureBenchmark.cs @@ -32,7 +32,7 @@ public class DeltaPressureBenchmark /// /// Number of entities (windows, really) to spawn with a . /// - [Params(1, 10, 100, 1000, 5000, 10000, 50000, 100000)] + [Params(100, 1000, 5000, 10000)] public int EntityCount; /// diff --git a/Content.Benchmarks/PvsBenchmark.cs b/Content.Benchmarks/PvsBenchmark.cs index af1ec8d9efc..ab4e7981d2f 100644 --- a/Content.Benchmarks/PvsBenchmark.cs +++ b/Content.Benchmarks/PvsBenchmark.cs @@ -97,7 +97,8 @@ await _pair.Server.WaitPost(() => // Repeatedly move players around so that they "explore" the map and see lots of entities. // This will populate their PVS data with out-of-view entities. - var rng = new Random(42); + var rng = new RobustRandom(); + rng.SetSeed(42); ShufflePlayers(rng, 100); _pair.Server.PvsTick(_players); @@ -107,7 +108,7 @@ await _pair.Server.WaitPost(() => _locations = ents.Select(x => _entMan.GetComponent(x).Coordinates).ToArray(); } - private void ShufflePlayers(Random rng, int count) + private void ShufflePlayers(IRobustRandom rng, int count) { while (count > 0) { @@ -116,7 +117,7 @@ private void ShufflePlayers(Random rng, int count) } } - private void ShufflePlayers(Random rng) + private void ShufflePlayers(IRobustRandom rng) { _pair.Server.PvsTick(_players); diff --git a/Content.Benchmarks/RaiseEventBenchmark.cs b/Content.Benchmarks/RaiseEventBenchmark.cs index 99e032d0b58..b8c6bd7d472 100644 --- a/Content.Benchmarks/RaiseEventBenchmark.cs +++ b/Content.Benchmarks/RaiseEventBenchmark.cs @@ -12,7 +12,7 @@ namespace Content.Benchmarks; [Virtual] -public class RaiseEventBenchmark +public partial class RaiseEventBenchmark { private TestPair _pair = default!; private BenchSystem _sys = default!; @@ -35,6 +35,48 @@ public void Setup() _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 async Task Cleanup() } [Benchmark(Baseline = true)] - public int RaiseEvent() + public int RaiseEvent1() + { + return _sys.RaiseEvent1(); + } + + [Benchmark] + public int RaiseEvent2() + { + return _sys.RaiseEvent2(); + } + + [Benchmark] + public int RaiseEvent4() + { + return _sys.RaiseEvent4(); + } + + [Benchmark] + public int RaiseEvent8() { - return _sys.RaiseEvent(); + return _sys.RaiseEvent8(); + } + + [Benchmark] + public int RaiseEvent16() + { + return _sys.RaiseEvent16(); } [Benchmark] @@ -77,10 +143,14 @@ public int RaiseCSharpEvent() 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 override void Initialize() { 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 int CSharpEvent() } [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 struct BenchEv { 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.Benchmarks/Vector2iOffsetBenchmark.cs b/Content.Benchmarks/Vector2iOffsetBenchmark.cs new file mode 100644 index 00000000000..adbc63459f4 --- /dev/null +++ b/Content.Benchmarks/Vector2iOffsetBenchmark.cs @@ -0,0 +1,117 @@ +using System; +using System.Runtime.CompilerServices; +using BenchmarkDotNet.Attributes; +using Content.Shared.Atmos; +using Robust.Shared.Analyzers; +using Robust.Shared.Maths; + +namespace Content.Benchmarks; + +// ReSharper disable once InconsistentNaming +/// +/// Benchmark for testing different ways of offsetting . +/// +/// This is an excuse for me to use Rider's new ASM viewer +/// and pretend that I know what I'm doing. +[Virtual] +[GcServer(true)] +public class Vector2iOffsetBenchmark +{ + private Vector2i[] _vecs = default!; + private AtmosDirection[] _dirs = default!; + + public const int Count = 1000; + + [GlobalSetup] + public void Setup() + { + _vecs = new Vector2i[Count]; + _dirs = new AtmosDirection[Count]; + var rand = new Random(); + for (var i = 0; i < Count; i++) + { + _vecs[i] = new Vector2i(rand.Next(-100, 100), rand.Next(-100, 100)); + _dirs[i] = (AtmosDirection)(1 << rand.Next(4)); + } + } + + [Benchmark(Baseline = true, OperationsPerInvoke = Count)] + public void OffsetOld() + { + for (var i = 0; i < Count; i++) + { + _vecs[i] = OffsetOld(_vecs[i], _dirs[i]); + } + } + + [Benchmark(OperationsPerInvoke = Count)] + public void OffsetNew1() + { + for (var i = 0; i < Count; i++) + { + _vecs[i] = Offset1(_vecs[i], _dirs[i]); + } + } + + [Benchmark(OperationsPerInvoke = Count)] + public void OffsetNew2() + { + for (var i = 0; i < Count; i++) + { + _vecs[i] = Offset2(_vecs[i], _dirs[i]); + } + } + + [Benchmark(OperationsPerInvoke = Count)] + public void OffsetFinal() + { + for (var i = 0; i < Count; i++) + { + _vecs[i] = _vecs[i].Offset(_dirs[i]); + } + } + + public static Vector2i OffsetOld(Vector2i pos, AtmosDirection dir) + { + return pos + OldCardinalToIntVec(dir); + } + + public static Vector2i OldCardinalToIntVec(AtmosDirection dir) + { + switch (dir) + { + case AtmosDirection.North: + return new Vector2i(0, 1); + case AtmosDirection.East: + return new Vector2i(1, 0); + case AtmosDirection.South: + return new Vector2i(0, -1); + case AtmosDirection.West: + return new Vector2i(-1, 0); + default: + throw new ArgumentException($"Direction dir {dir} is not a cardinal direction", nameof(dir)); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2i Offset1(Vector2i pos, AtmosDirection dir) + { + // offset by bitflags and then sub to get the final offset + var dx = (dir.IsFlagSet(AtmosDirection.East) ? 1 : 0) - (dir.IsFlagSet(AtmosDirection.West) ? 1 : 0); + var dy = (dir.IsFlagSet(AtmosDirection.North) ? 1 : 0) - (dir.IsFlagSet(AtmosDirection.South) ? 1 : 0); + return new Vector2i(pos.X + dx, pos.Y + dy); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2i Offset2(Vector2i pos, AtmosDirection dir) + { + // extract individual bits and compute delta = (positive side) - (negative side). + // literally 5% faster than .IsFlagSet + // works on my machine award + var b = (byte)dir; + var dx = ((b >> 2) & 1) - ((b >> 3) & 1); + var dy = ((b >> 0) & 1) - ((b >> 1) & 1); + + return new Vector2i(pos.X + dx, pos.Y + dy); + } +} diff --git a/Content.Client/Access/Systems/JobStatusSystem.cs b/Content.Client/Access/Systems/JobStatusSystem.cs index e651bfdb70d..0b4ef987200 100644 --- a/Content.Client/Access/Systems/JobStatusSystem.cs +++ b/Content.Client/Access/Systems/JobStatusSystem.cs @@ -10,7 +10,6 @@ public sealed partial class JobStatusSystem : SharedJobStatusSystem { [Dependency] private ShowJobIconsSystem _showJobIcons = default!; [Dependency] private ShowCrewIconsSystem _showCrewIcons = default!; - [Dependency] private IPrototypeManager _prototype = default!; private static readonly ProtoId CrewBorderIcon = "CrewBorderIcon"; private static readonly ProtoId CrewUncertainBorderIcon = "CrewUncertainBorderIcon"; @@ -26,14 +25,14 @@ public override void Initialize() private void OnGetStatusIconsEvent(Entity ent, ref GetStatusIconsEvent ev) { if (_showJobIcons.IsActive && ent.Comp.JobStatusIcon != null) - ev.StatusIcons.Add(_prototype.Index(ent.Comp.JobStatusIcon)); + ev.StatusIcons.Add(ProtoMan.Index(ent.Comp.JobStatusIcon)); if (_showCrewIcons.IsActive) { if (_showCrewIcons.UncertainCrewBorder) - ev.StatusIcons.Add(_prototype.Index(CrewUncertainBorderIcon)); + ev.StatusIcons.Add(ProtoMan.Index(CrewUncertainBorderIcon)); else if (ent.Comp.IsCrew) - ev.StatusIcons.Add(_prototype.Index(CrewBorderIcon)); + ev.StatusIcons.Add(ProtoMan.Index(CrewBorderIcon)); } } } diff --git a/Content.Client/Actions/ActionsSystem.cs b/Content.Client/Actions/ActionsSystem.cs index 90cf4190f2d..aac14a14200 100644 --- a/Content.Client/Actions/ActionsSystem.cs +++ b/Content.Client/Actions/ActionsSystem.cs @@ -17,7 +17,6 @@ using Robust.Shared.Serialization.Markdown.Mapping; using Robust.Shared.Serialization.Markdown.Sequence; using Robust.Shared.Serialization.Markdown.Value; -using Robust.Shared.Timing; using Robust.Shared.Utility; using YamlDotNet.RepresentationModel; @@ -30,7 +29,6 @@ public sealed partial class ActionsSystem : SharedActionsSystem [Dependency] private SharedChargesSystem _sharedCharges = default!; [Dependency] private IPlayerManager _playerManager = default!; - [Dependency] private IPrototypeManager _proto = default!; [Dependency] private IResourceManager _resources = default!; [Dependency] private MetaDataSystem _metaData = default!; [Dependency] private ISerializationManager _serialization = default!; @@ -257,7 +255,7 @@ public void LoadActionAssignments(string path, bool userData) else if (map.TryGet("entity", out var entityNode)) { var id = new EntProtoId(entityNode.Value); - var proto = _proto.Index(id); + var proto = ProtoMan.Index(id); actionId = Spawn(MappingEntityAction); SetIcon(actionId, new SpriteSpecifier.EntityPrototype(id)); SetEvent(actionId, new StartPlacementActionEvent() @@ -270,7 +268,7 @@ public void LoadActionAssignments(string path, bool userData) else if (map.TryGet("tileId", out var tileNode)) { var id = new ProtoId(tileNode.Value); - var proto = _proto.Index(id); + var proto = ProtoMan.Index(id); actionId = Spawn(MappingEntityAction); if (proto.Sprite is {} sprite) SetIcon(actionId, new SpriteSpecifier.Texture(sprite)); diff --git a/Content.Client/Administration/AdminOverlayCommand.cs b/Content.Client/Administration/AdminOverlayCommand.cs new file mode 100644 index 00000000000..afad7a4aefe --- /dev/null +++ b/Content.Client/Administration/AdminOverlayCommand.cs @@ -0,0 +1,46 @@ +using Content.Client.Administration.Systems; +using Content.Shared.Commands; +using Robust.Shared.Console; + +namespace Content.Client.Administration; + +/// +/// Enables or disables the admin "player info" overlay. +/// +/// +public sealed partial class AdminOverlayCommand : LocalizedEntityCommands +{ + [Dependency] private AdminSystem _adminSystem = null!; + + public override string Command => "admin_overlay"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (!CommandHelper.CheckExactlyOneArgument(Loc, shell, args)) + return; + + if (!CommandHelper.ParseArgumentBoolean(Loc, shell, args[0], out var boolean)) + return; + + if (boolean) + { + _adminSystem.AdminOverlayOn(); + } + else + { + _adminSystem.AdminOverlayOff(); + } + } + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + { + return CompletionResult.FromHintOptions( + CompletionHelper.Booleans, + Loc.GetString("cmd-admin_overlay-arg-state")); + } + + return CompletionResult.Empty; + } +} diff --git a/Content.Client/Administration/AdminQuickInfoSystem.cs b/Content.Client/Administration/AdminQuickInfoSystem.cs new file mode 100644 index 00000000000..846dd3f0f00 --- /dev/null +++ b/Content.Client/Administration/AdminQuickInfoSystem.cs @@ -0,0 +1,253 @@ +using System.Linq; +using Content.Client.Administration.Systems; +using Content.Client.Stylesheets; +using Content.Shared.Administration; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Console; +using Robust.Shared.Prototypes; +using Robust.Shared.RichText; +using Robust.Shared.Utility; + +namespace Content.Client.Administration; + +// Server-validated, AnyCommand is acceptable here. +[AnyCommand] +internal sealed partial class AdminQuickInfoCommand : LocalizedEntityCommands +{ + [Dependency] private AdminQuickInfoSystem _quickInfo = null!; + + public override string Command => QuickInfoShared.CommandName; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length != 1) + return; + + var idsArg = args[0].Split(','); + var ids = new NetEntity[idsArg.Length]; + for (var i = 0; i < ids.Length; i++) + { + if (!NetEntity.TryParse(idsArg[i], out ids[i])) + return; + } + + _quickInfo.OpenPopupFor(ids); + } +} + +internal sealed partial class AdminQuickInfoSystem : EntitySystem +{ + [Dependency] private AdminSystem _adminSystem = null!; + + public event Action? EntityResponseReceived; + + public override void Initialize() + { + base.Initialize(); + + SubscribeNetworkEvent(HandleInfoResponse); + } + + private void HandleInfoResponse(QuickInfoShared.Response ev) + { + foreach (var singleEntityInfo in ev.Entities) + { + EntityResponseReceived?.Invoke(singleEntityInfo); + } + } + + public void OpenPopupFor(NetEntity[] entities) + { + var vBox = new VBox() { SeparationOverride = 6 };; + var popup = new Popup + { + Children = + { + new PanelContainer + { + StyleClasses = { StyleClass.TooltipPanel }, + Children = + { + vBox, + }, + }, + }, + }; + 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(); + vBox.AddChild(control); + } + + popup.OpenAtMouse(); + + RaiseNetworkEvent(new QuickInfoShared.Request { Entities = entities }); + } + + private sealed class InfoControl : Control + { + private readonly AdminQuickInfoSystem _system; + private readonly NetEntity _entity; + 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; + Rebuild(); + } + + private void OnEntityResponseReceived(QuickInfoShared.SingleEntityInfo ev) + { + // Yes I know this is O(n^2) + // Realistically, n is going to be 30 (default cvar limit) or less. Not a big deal. + if (ev.Entity != _entity) + return; + + _response = ev; + _playerInfo ??= Admin.PlayerList.FirstOrDefault(p => p.SessionId == ev.LastPlayer); + Rebuild(); + } + + public void Unsubscribe() + { + // This, too, is O(n^2). I think. + _system.EntityResponseReceived -= OnEntityResponseReceived; + } + + private void Rebuild() + { + var sb = new FormattedStringBuilder(); + if (!_activeControl) + { + sb.AppendMarkupLine(Loc.GetString("admin-quick-info-inactive")); + } + + if (_playerInfo != null) + { + sb.AppendMarkupLine(Loc.GetString("admin-quick-info-username", + ("username", _playerInfo.Username), + ("playtime", _playerInfo.PlaytimeString))); + if (_playerInfo.CharacterName != _playerInfo.IdentityName) + { + sb.AppendMarkupLine(Loc.GetString("admin-quick-info-character-identity", + ("character", _playerInfo.CharacterName), + ("identity", _playerInfo.IdentityName))); + } + else + { + sb.AppendMarkupLine(Loc.GetString("admin-quick-info-character", + ("character", _playerInfo.CharacterName))); + } + + sb.AppendMarkupLine(Loc.GetString("admin-quick-info-job", ("job", _playerInfo.StartingJob))); + + if (_playerInfo.RoleProto is { } roleId && _system.ProtoMan.Resolve(roleId, out var role)) + { + var roleColor = role.Color.ToHex(); + var symbol = _playerInfo.Antag ? role.Symbol : ""; + + var roleTypeString = Loc.GetString(role.Name); + var roleSubtypeString = _playerInfo.Subtype is { } subtype ? Loc.GetString(subtype) : null; + + var roleString = roleSubtypeString != null + ? Loc.GetString("admin-quick-info-role-type-subtype", + ("color", roleColor), + ("symbol", symbol), + ("type", roleTypeString), + ("subtype", roleSubtypeString)) + : Loc.GetString("admin-quick-info-role-type", + ("color", roleColor), + ("symbol", symbol), + ("type", roleTypeString)); + sb.AppendMarkupLine(roleString); + } + else + { + sb.AppendMarkupLine(Loc.GetString("admin-quick-info-no-role")); + } + } + + if (_response == null) + { + sb.AppendMarkupLine(Loc.GetString("admin-quick-info-loading", ("entity", _entity))); + } + else + { + if (!_response.Exists) + { + sb.AppendMarkupLine(Loc.GetString("admin-quick-info-entity-missing", ("entity", _entity))); + } + else + { + sb.AppendMarkupLine(Loc.GetString("admin-quick-info-entity", + ("entity", _entity), + ("name", _response.Name), + ("prototype", _response.Prototype ?? Loc.GetString("admin-quick-info-no-prototype")))); + } + } + + // Link line at the bottom + if (_playerInfo != null) + { + // Player panel + sb.MakeCommandLinkTag( + Loc.GetString("admin-quick-link-playerpanel"), + CommandParsing.EscapeCommand( + AdminCommandSyntax.NamePlayerPanel, + _playerInfo.SessionId.ToString()), + Loc.GetString("admin-quick-link-playerpanel-tooltip")); + sb.AppendText(" "); + + // Send Message + sb.MakeCommandLinkTag( + Loc.GetString("admin-quick-link-message"), + CommandParsing.EscapeCommand( + AdminCommandSyntax.NameOpenAdminHelp, + _playerInfo.SessionId.ToString()), + Loc.GetString("admin-quick-link-message-tooltip")); + sb.AppendText(" "); + } + + if (_response is not { Exists: false }) + { + // Jump/follow + sb.MakeCommandLinkTag( + Loc.GetString("admin-quick-link-follow"), + CommandParsing.EscapeCommand( + AdminCommandSyntax.NameFollow, + _entity.ToString()), + Loc.GetString("admin-quick-link-follow-tooltip")); + sb.AppendText(" "); + } + + _contents.SetMessage(FormattedMessage.FromMarkupOrThrow(sb.ToString().Trim()), tagsAllowed: null); + } + } +} diff --git a/Content.Client/Administration/Systems/AdminSystem.Overlay.cs b/Content.Client/Administration/Systems/AdminSystem.Overlay.cs index a07abae90ce..3fb85b59d38 100644 --- a/Content.Client/Administration/Systems/AdminSystem.Overlay.cs +++ b/Content.Client/Administration/Systems/AdminSystem.Overlay.cs @@ -4,7 +4,6 @@ using Robust.Client.ResourceManagement; using Robust.Client.UserInterface; using Robust.Shared.Configuration; -using Robust.Shared.Prototypes; namespace Content.Client.Administration.Systems { @@ -18,7 +17,6 @@ public sealed partial class AdminSystem [Dependency] private IUserInterfaceManager _userInterfaceManager = default!; [Dependency] private IConfigurationManager _configurationManager = default!; [Dependency] private SharedRoleSystem _roles = default!; - [Dependency] private IPrototypeManager _proto = default!; private AdminNameOverlay _adminNameOverlay = default!; @@ -36,7 +34,7 @@ private void InitializeOverlay() _userInterfaceManager, _configurationManager, _roles, - _proto); + ProtoMan); _adminManager.AdminStatusUpdated += OnAdminStatusUpdated; } diff --git a/Content.Client/Administration/UI/Bwoink/BwoinkPanel.xaml.cs b/Content.Client/Administration/UI/Bwoink/BwoinkPanel.xaml.cs index 833514e4aa2..160d013d6d7 100644 --- a/Content.Client/Administration/UI/Bwoink/BwoinkPanel.xaml.cs +++ b/Content.Client/Administration/UI/Bwoink/BwoinkPanel.xaml.cs @@ -1,6 +1,7 @@ using Content.Shared.Administration; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.RichText; using Robust.Client.UserInterface.XAML; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -10,6 +11,17 @@ namespace Content.Client.Administration.UI.Bwoink [GenerateTypedNameReferences] public sealed partial class BwoinkPanel : BoxContainer { + private static readonly Type[] AllowedTags = new Type[] + { + typeof(CommandLinkTag), + typeof(BoldItalicTag), + typeof(BoldTag), + typeof(BulletTag), + typeof(ColorTag), + typeof(HeadingTag), + typeof(ItalicTag), + }; + private readonly Action _messageSender; public int Unread { get; private set; } = 0; @@ -60,7 +72,7 @@ public void ReceiveLine(SharedBwoinkSystem.BwoinkTextMessage message) var formatted = new FormattedMessage(1); formatted.AddMarkupOrThrow($"[color=gray]{message.SentAt.ToShortTimeString()}[/color] {message.Text}"); - TextOutput.AddMessage(formatted); + TextOutput.AddMessage(formatted, AllowedTags); LastMessage = message.SentAt; } diff --git a/Content.Client/Administration/UI/CustomControls/AdminLogLabel.cs b/Content.Client/Administration/UI/CustomControls/AdminLogLabel.cs index eb2281cf7b2..9be3578eed3 100644 --- a/Content.Client/Administration/UI/CustomControls/AdminLogLabel.cs +++ b/Content.Client/Administration/UI/CustomControls/AdminLogLabel.cs @@ -11,7 +11,9 @@ public AdminLogLabel(ref SharedAdminLog log, HSeparator separator) Log = log; Separator = separator; - SetMessage($"{log.Date:HH:mm:ss}: {log.Message}"); + var localTime = log.Date.ToLocalTime(); + + SetMessage($"{localTime:HH:mm:ss}: {log.Message}"); OnVisibilityChanged += VisibilityChanged; } diff --git a/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs b/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs index 5515600d1ef..0e7de8acbae 100644 --- a/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs +++ b/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs @@ -106,13 +106,35 @@ public NoteEdit(SharedAdminNote? note, string playerName, bool canCreate, bool c { 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 @@ private bool ParseExpiryTime() 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 @@ private bool ParseExpiryTime() (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 @@ protected override void Dispose(bool disposing) SubmitPressed = null; } -} +} \ No newline at end of file diff --git a/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml b/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml index 9c54049da36..76504334ef5 100644 --- a/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml +++ b/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml @@ -17,6 +17,8 @@ public sealed partial class GasCanisterAppearanceSystem : VisualizerSystem { - [Dependency] private IPrototypeManager _prototypeManager = default!; - protected override void OnAppearanceChange(EntityUid uid, GasCanisterComponent component, ref AppearanceChangeEvent args) { if (!AppearanceSystem.TryGetData(uid, PaintableVisuals.Prototype, out var protoName, args.Component) || args.Sprite is not { } old) return; - if (!_prototypeManager.HasIndex(protoName)) + if (!ProtoMan.HasIndex(protoName)) return; // Create the given prototype and get its first layer. diff --git a/Content.Client/Atmos/Overlays/AtmosDebugOverlay.cs b/Content.Client/Atmos/Overlays/AtmosDebugOverlay.cs index 33fec9d985b..448261e33c3 100644 --- a/Content.Client/Atmos/Overlays/AtmosDebugOverlay.cs +++ b/Content.Client/Atmos/Overlays/AtmosDebugOverlay.cs @@ -21,7 +21,6 @@ namespace Content.Client.Atmos.Overlays; public sealed partial class AtmosDebugOverlay : Overlay { [Dependency] private IEntityManager _entManager = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private IInputManager _input = default!; [Dependency] private IUserInterfaceManager _ui = default!; [Dependency] private IResourceCache _cache = default!; @@ -262,7 +261,7 @@ private void DrawTooltip(DrawingHandleScreen handle, Vector2 pos, AtmosDebugOver private void GetGrids(MapId mapId, Box2Rotated box) { _grids.Clear(); - _mapManager.FindGridsIntersecting( + _map.FindGridsIntersecting( mapId, box, ref _grids, diff --git a/Content.Client/Atmos/Overlays/GasTileDangerousTemperatureOverlay.cs b/Content.Client/Atmos/Overlays/GasTileDangerousTemperatureOverlay.cs index 65d1b11e539..bdd62db9a80 100644 --- a/Content.Client/Atmos/Overlays/GasTileDangerousTemperatureOverlay.cs +++ b/Content.Client/Atmos/Overlays/GasTileDangerousTemperatureOverlay.cs @@ -19,10 +19,10 @@ public sealed partial class GasTileDangerousTemperatureOverlay : Overlay public override bool RequestScreenTexture { get; set; } = false; [Dependency] private IEntityManager _entManager = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private IClyde _clyde = default!; private GasTileOverlaySystem? _gasTileOverlay; + private readonly SharedMapSystem _mapSys; private readonly SharedTransformSystem _xformSys; private EntityQuery _overlayQuery; @@ -37,6 +37,7 @@ public sealed partial class GasTileDangerousTemperatureOverlay : Overlay public GasTileDangerousTemperatureOverlay() { IoCManager.InjectDependencies(this); + _mapSys = _entManager.System(); _xformSys = _entManager.System(); _overlayQuery = _entManager.GetEntityQuery(); @@ -174,7 +175,7 @@ protected override bool BeforeDraw(in OverlayDrawArgs args) () => { _grids.Clear(); - _mapManager.FindGridsIntersecting(mapId, worldAABB, ref _grids); + _mapSys.FindGridsIntersecting(mapId, worldAABB, ref _grids); foreach (var grid in _grids) { diff --git a/Content.Client/Atmos/Overlays/GasTileFireOverlay.cs b/Content.Client/Atmos/Overlays/GasTileFireOverlay.cs index 45cebef98b1..370ed2a7927 100644 --- a/Content.Client/Atmos/Overlays/GasTileFireOverlay.cs +++ b/Content.Client/Atmos/Overlays/GasTileFireOverlay.cs @@ -23,7 +23,6 @@ public sealed partial class GasTileFireOverlay : Overlay [Dependency] private IPrototypeManager _protoMan = default!; [Dependency] private IResourceCache _resourceCache = default!; [Dependency] private IEntityManager _entManager = default!; - [Dependency] private IMapManager _mapManager = default!; public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities | OverlaySpace.WorldSpaceBelowWorld; private static readonly ProtoId UnshadedShader = "unshaded"; @@ -113,7 +112,7 @@ protected override void Draw(in OverlayDrawArgs args) return; // TODO: WorldBounds callback. - _mapManager.FindGridsIntersecting(args.MapId, args.WorldAABB, ref gridState, + _mapSystem.FindGridsIntersecting(args.MapId, args.WorldAABB, ref gridState, static (EntityUid uid, MapGridComponent grid, ref (Box2Rotated WorldBounds, DrawingHandleWorld drawHandle, diff --git a/Content.Client/Atmos/Overlays/GasTileHeatBlurOverlay.cs b/Content.Client/Atmos/Overlays/GasTileHeatBlurOverlay.cs index 7815bec6e4a..43b95b975a1 100644 --- a/Content.Client/Atmos/Overlays/GasTileHeatBlurOverlay.cs +++ b/Content.Client/Atmos/Overlays/GasTileHeatBlurOverlay.cs @@ -28,12 +28,12 @@ public sealed partial class GasTileHeatBlurOverlay : Overlay private static readonly ProtoId HeatOverlayShader = "HeatBlur"; [Dependency] private IEntityManager _entManager = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private IPrototypeManager _proto = default!; [Dependency] private IClyde _clyde = default!; [Dependency] private IConfigurationManager _configManager = default!; [Dependency] private IResourceCache _resourceCache = default!; + private readonly SharedMapSystem _maps; private readonly SharedTransformSystem _xformSys; private readonly ShaderInstance _shader; @@ -63,13 +63,14 @@ private const float public GasTileHeatBlurOverlay() { IoCManager.InjectDependencies(this); + _maps = _entManager.System(); _xformSys = _entManager.System(); _noiseTexture = _resourceCache.GetTexture("/Textures/Effects/HeatBlur/perlin_noise.png"); _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) @@ -126,7 +127,7 @@ protected override bool BeforeDraw(in OverlayDrawArgs args) () => { _intersectingGrids.Clear(); - _mapManager.FindGridsIntersecting(mapId, worldAABB, ref _intersectingGrids); + _maps.FindGridsIntersecting(mapId, worldAABB, ref _intersectingGrids); foreach (var grid in _intersectingGrids) { if (!overlayQuery.TryGetComponent(grid.Owner, out var comp)) @@ -226,7 +227,7 @@ protected override void DisposeBehavior() { _resources.Dispose(); - _configManager.UnsubValueChanged(CCVars.ReducedMotion, SetReducedMotion); + _configManager.UnsubValueChanged(CCVars.DisableHeatDistortion, SetReducedMotion); base.DisposeBehavior(); } diff --git a/Content.Client/Atmos/Overlays/GasTileVisibleGasOverlay.cs b/Content.Client/Atmos/Overlays/GasTileVisibleGasOverlay.cs index d5b21aeabe1..65d0b919d07 100644 --- a/Content.Client/Atmos/Overlays/GasTileVisibleGasOverlay.cs +++ b/Content.Client/Atmos/Overlays/GasTileVisibleGasOverlay.cs @@ -25,7 +25,6 @@ public sealed partial class GasTileVisibleGasOverlay : Overlay [Dependency] private IEntityManager _entManager = default!; [Dependency] private IResourceCache _resourceCache = default!; [Dependency] private IPrototypeManager _protoManager = default!; - [Dependency] private IMapManager _mapManager = default!; private static readonly ProtoId UnshadedShader = "unshaded"; @@ -142,7 +141,7 @@ protected override void Draw(in OverlayDrawArgs args) return; // TODO: WorldBounds callback. - _mapManager.FindGridsIntersecting(args.MapId, + _mapSystem.FindGridsIntersecting(args.MapId, args.WorldAABB, ref gridState, static (EntityUid uid, diff --git a/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs b/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs index b54513800e5..f7660b2ec87 100644 --- a/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs +++ b/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs @@ -22,16 +22,14 @@ 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 IPrototypeManager _proto = default!; [Dependency] private IRobustRandom _random = default!; [Dependency] private IStateManager _state = default!; [Dependency] private RulesSystem _rules = default!; [Dependency] private SharedAudioSystem _audio = default!; - private readonly TimeSpan _minAmbienceTime = TimeSpan.FromSeconds(30); - private readonly TimeSpan _maxAmbienceTime = TimeSpan.FromSeconds(60); + private readonly TimeSpan _minAmbienceTime = TimeSpan.FromSeconds(60); + private readonly TimeSpan _maxAmbienceTime = TimeSpan.FromSeconds(120); private const float AmbientMusicFadeTime = 10f; private static float _volumeSlider; @@ -41,6 +39,7 @@ public sealed partial class ContentAudioSystem private EntityUid? _ambientMusicStream; private AmbientMusicPrototype? _musicProto; + private AmbientMusicPrototype? _lastMusicProto; /// /// If we find a better ambient music proto can we interrupt this one. @@ -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; @@ -105,7 +104,7 @@ private void OnStateChange(StateChangedEventArgs obj) private void SetupAmbientSounds() { _ambientSounds.Clear(); - foreach (var ambience in _proto.EnumeratePrototypes()) + foreach (var ambience in ProtoMan.EnumeratePrototypes()) { var tracks = _ambientSounds.GetOrNew(ambience.ID); RefreshTracks(ambience.Sound, tracks, null); @@ -130,7 +129,7 @@ private void RefreshTracks(SoundSpecifier sound, List tracks, ResPath? if (collection.Collection == null) break; - var slothCud = _proto.Index(collection.Collection); + var slothCud = ProtoMan.Index(collection.Collection); tracks.AddRange(slothCud.PickFiles); break; case SoundPathSpecifier path: @@ -166,7 +165,7 @@ private void UpdateAmbientMusic() { var player = _player.LocalSession?.AttachedEntity; - if (player == null || _musicProto == null || !_rules.IsTrue(player.Value, _proto.Index(_musicProto.Rules))) + if (player == null || _musicProto == null || !_rules.IsTrue(player.Value, ProtoMan.Index(_musicProto.Rules))) { FadeOut(_ambientMusicStream, duration: AmbientMusicFadeTime); _musicProto = null; @@ -193,7 +192,7 @@ private void UpdateAmbientMusic() _musicProto = GetAmbience(); - if (_musicProto == null) + if (_musicProto == null || _musicProto == _lastMusicProto) { _interruptable = false; return; @@ -223,6 +222,8 @@ private void UpdateAmbientMusic() { RefreshTracks(_musicProto.Sound, tracks, track); } + + _lastMusicProto = _musicProto; } private AmbientMusicPrototype? GetAmbience() @@ -238,12 +239,12 @@ private void UpdateAmbientMusic() if (ev.Cancelled) return null; - var ambiences = _proto.EnumeratePrototypes().ToList(); + var ambiences = ProtoMan.EnumeratePrototypes().ToList(); ambiences.Sort((x, y) => y.Priority.CompareTo(x.Priority)); foreach (var amb in ambiences) { - if (!_rules.IsTrue(player.Value, _proto.Index(amb.Rules))) + if (!_rules.IsTrue(player.Value, ProtoMan.Index(amb.Rules))) continue; return amb; diff --git a/Content.Client/Audio/Jukebox/JukeboxSystem.cs b/Content.Client/Audio/Jukebox/JukeboxSystem.cs index 0194c1ccbdf..2caac59f643 100644 --- a/Content.Client/Audio/Jukebox/JukeboxSystem.cs +++ b/Content.Client/Audio/Jukebox/JukeboxSystem.cs @@ -8,7 +8,6 @@ namespace Content.Client.Audio.Jukebox; public sealed partial class JukeboxSystem : SharedJukeboxSystem { - [Dependency] private IPrototypeManager _protoManager = default!; [Dependency] private AnimationPlayerSystem _animationPlayer = default!; [Dependency] private SharedAppearanceSystem _appearanceSystem = default!; [Dependency] private SharedUserInterfaceSystem _uiSystem = default!; @@ -21,13 +20,13 @@ public override void Initialize() SubscribeLocalEvent(OnAnimationCompleted); SubscribeLocalEvent(OnJukeboxAfterState); - _protoManager.PrototypesReloaded += OnProtoReload; + ProtoMan.PrototypesReloaded += OnProtoReload; } public override void Shutdown() { base.Shutdown(); - _protoManager.PrototypesReloaded -= OnProtoReload; + ProtoMan.PrototypesReloaded -= OnProtoReload; } private void OnProtoReload(PrototypesReloadedEventArgs obj) diff --git a/Content.Client/Audio/Midi/MidiFileCollectionManager.cs b/Content.Client/Audio/Midi/MidiFileCollectionManager.cs new file mode 100644 index 00000000000..9f84fad30b9 --- /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/BarSign/BarSignVisualizerSystem.cs b/Content.Client/BarSign/BarSignVisualizerSystem.cs index e17d264cb51..029c4724dc6 100644 --- a/Content.Client/BarSign/BarSignVisualizerSystem.cs +++ b/Content.Client/BarSign/BarSignVisualizerSystem.cs @@ -1,14 +1,11 @@ using Content.Shared.BarSign; using Content.Shared.Power; using Robust.Client.GameObjects; -using Robust.Shared.Prototypes; namespace Content.Client.BarSign; public sealed partial class BarSignVisualizerSystem : VisualizerSystem { - [Dependency] private IPrototypeManager _prototypeManager = default!; - protected override void OnAppearanceChange(EntityUid uid, BarSignComponent component, ref AppearanceChangeEvent args) { AppearanceSystem.TryGetData(uid, PowerDeviceVisuals.Powered, out var powered, args.Component); @@ -16,7 +13,7 @@ protected override void OnAppearanceChange(EntityUid uid, BarSignComponent compo if (powered && currentSign != null - && _prototypeManager.Resolve(currentSign, out var proto)) + && ProtoMan.Resolve(currentSign, out var proto)) { SpriteSystem.LayerSetSprite((uid, args.Sprite), 0, proto.Icon); args.Sprite?.LayerSetShader(0, "unshaded"); diff --git a/Content.Client/Body/VisualBodySystem.cs b/Content.Client/Body/VisualBodySystem.cs index c7b5825d143..cf2cbd0c49f 100644 --- a/Content.Client/Body/VisualBodySystem.cs +++ b/Content.Client/Body/VisualBodySystem.cs @@ -2,12 +2,12 @@ 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; using Robust.Client.Graphics; using Robust.Shared.Configuration; -using Robust.Shared.Prototypes; using Robust.Shared.Utility; namespace Content.Client.Body; @@ -15,7 +15,6 @@ namespace Content.Client.Body; public sealed partial class VisualBodySystem : SharedVisualBodySystem { [Dependency] private IConfigurationManager _cfg = default!; - [Dependency] private IPrototypeManager _prototype = default!; [Dependency] private DisplacementMapSystem _displacement = default!; [Dependency] private MarkingManager _marking = default!; [Dependency] private SpriteSystem _sprite = default!; @@ -75,6 +74,16 @@ private void ApplyVisual(Entity ent, EntityUid target) 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) @@ -83,6 +92,8 @@ private void RemoveVisual(Entity ent, EntityUid target) return; _sprite.LayerSetRsiState(target, index, RSI.StateId.Invalid); + + _displacement.EnsureDisplacementIsNotOnSprite((target, Comp(target)), ent.Comp.Layer); } private void OnMarkingsGotInserted(Entity ent, ref OrganGotInsertedEvent args) @@ -149,7 +160,7 @@ private IEnumerable AllMarkings(Entity en if (!censorNudity) yield break; - var group = _prototype.Index(ent.Comp.MarkingData.Group); + var group = ProtoMan.Index(ent.Comp.MarkingData.Group); foreach (var layer in ent.Comp.MarkingData.Layers) { if (!group.Limits.TryGetValue(layer, out var layerLimits)) diff --git a/Content.Client/Cargo/BUI/CargoOrderConsoleBoundUserInterface.cs b/Content.Client/Cargo/BUI/CargoOrderConsoleBoundUserInterface.cs index b509b137676..e758f547639 100644 --- a/Content.Client/Cargo/BUI/CargoOrderConsoleBoundUserInterface.cs +++ b/Content.Client/Cargo/BUI/CargoOrderConsoleBoundUserInterface.cs @@ -81,6 +81,7 @@ protected override void Open() _orderMenu.Amount.Value = 1; _orderMenu.OpenCentered(); + _orderMenu.SetPositionLast(); }; _menu.OnOrderApproved += ApproveOrder; _menu.OnOrderCanceled += RemoveOrder; diff --git a/Content.Client/CartridgeLoader/CartridgeLoaderBoundUserInterface.cs b/Content.Client/CartridgeLoader/CartridgeLoaderBoundUserInterface.cs index e4d6a9fb8bd..41a46c9c6a2 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 @@ protected override void UpdateState(BoundUserInterfaceState state) 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 @@ protected void DeactivateActiveCartridge() 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 e06a547814f..00000000000 --- 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 12b678d5d47..183de499a88 100644 --- a/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs +++ b/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs @@ -22,7 +22,7 @@ public override void Setup(BoundUserInterface ui, EntityUid? fragmentOwner) { 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 85a5659c4b8..00000000000 --- 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 ac08051c899..ca29fe8474d 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 override void Setup(BoundUserInterface userInterface, EntityUid? fragment 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 override void Setup(BoundUserInterface userInterface, EntityUid? fragment }; _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 1018d88ccee..da7041f4dd0 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 @@ private void SendNotekeeperMessage(NotekeeperUiAction action, string note, Bound { 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 00000000000..62560c43101 --- /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 a9612add6e6..feb306fdffc 100644 --- a/Content.Client/Chasm/ChasmFallingVisualsSystem.cs +++ b/Content.Client/Chasm/ChasmFallingVisualsSystem.cs @@ -6,14 +6,17 @@ 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 override void Initialize() 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; - - if (!TryComp(uid, out var player)) - return; + entity.Comp.OriginalScale = sprite.Scale; - if (_anim.HasRunningAnimation(player, _chasmFallAnimationKey)) + if (!_animationPlayerQuery.TryComp(entity, out var player) || + _anim.HasRunningAnimation(player, ChasmFallAnimationKey)) + { return; + } - _anim.Play((uid, player), GetFallingAnimation(component), _chasmFallAnimationKey); + _anim.Play((entity, player), GetFallingAnimation(entity.Comp), ChasmFallAnimationKey); } - private void OnComponentRemove(EntityUid uid, ChasmFallingComponent component, ComponentRemove args) + private void OnComponentRemove(Entity entity, ref ComponentRemove args) { - if (!TryComp(uid, out var sprite)) + if (!_spriteQuery.TryComp(entity, out var sprite)) + { return; + } - _sprite.SetScale((uid, sprite), component.OriginalScale); + _sprite.SetScale((entity, sprite), entity.Comp.OriginalScale); - if (!TryComp(uid, out var player)) + if (!_animationPlayerQuery.TryComp(entity, out var player) || + !_anim.HasRunningAnimation(player, ChasmFallAnimationKey)) + { return; + } - if (_anim.HasRunningAnimation(player, _chasmFallAnimationKey)) - _anim.Stop((uid, player), _chasmFallAnimationKey); + _anim.Stop((entity, player), ChasmFallAnimationKey); } - private Animation GetFallingAnimation(ChasmFallingComponent component) + private static Animation GetFallingAnimation(ChasmFallingComponent component) { - var length = component.AnimationTime; - - return new Animation() + return new Animation { - Length = length, + 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/Chat/TypingIndicator/TypingIndicatorVisualizerSystem.cs b/Content.Client/Chat/TypingIndicator/TypingIndicatorVisualizerSystem.cs index ebb6538ab46..4e84ce8c109 100644 --- a/Content.Client/Chat/TypingIndicator/TypingIndicatorVisualizerSystem.cs +++ b/Content.Client/Chat/TypingIndicator/TypingIndicatorVisualizerSystem.cs @@ -1,13 +1,11 @@ using Content.Shared.Chat.TypingIndicator; using Robust.Client.GameObjects; -using Robust.Shared.Prototypes; using Content.Shared.Inventory; namespace Content.Client.Chat.TypingIndicator; public sealed partial class TypingIndicatorVisualizerSystem : VisualizerSystem { - [Dependency] private IPrototypeManager _prototypeManager = default!; [Dependency] private InventorySystem _inventory = default!; protected override void OnAppearanceChange(EntityUid uid, TypingIndicatorComponent component, ref AppearanceChangeEvent args) @@ -27,7 +25,7 @@ protected override void OnAppearanceChange(EntityUid uid, TypingIndicatorCompone if (overrideIndicator != null) currentTypingIndicator = overrideIndicator.Value; - if (!_prototypeManager.Resolve(currentTypingIndicator, out var proto)) + if (!ProtoMan.Resolve(currentTypingIndicator, out var proto)) { Log.Error($"Unknown typing indicator id: {component.TypingIndicatorPrototype}"); return; diff --git a/Content.Client/Chemistry/EntitySystems/ChemistryGuideDataSystem.cs b/Content.Client/Chemistry/EntitySystems/ChemistryGuideDataSystem.cs index 099a36754dc..d81a5cc373b 100644 --- a/Content.Client/Chemistry/EntitySystems/ChemistryGuideDataSystem.cs +++ b/Content.Client/Chemistry/EntitySystems/ChemistryGuideDataSystem.cs @@ -4,11 +4,9 @@ using Content.Shared.Body; using Content.Shared.Chemistry; using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.Reaction; using Content.Shared.Chemistry.Reagent; using Content.Shared.Kitchen.Components; -using Content.Shared.Prototypes; using Robust.Shared.Prototypes; namespace Content.Client.Chemistry.EntitySystems; @@ -53,12 +51,12 @@ private void OnPrototypesReloaded(PrototypesReloadedEventArgs? ev) { // this doesn't check what prototypes are being reloaded because, to be frank, we use a lot of them. _reagentSources.Clear(); - foreach (var reagent in PrototypeManager.EnumeratePrototypes()) + foreach (var reagent in ProtoMan.EnumeratePrototypes()) { _reagentSources.Add(reagent.ID, new()); } - foreach (var reaction in PrototypeManager.EnumeratePrototypes()) + foreach (var reaction in ProtoMan.EnumeratePrototypes()) { if (!reaction.Source) continue; @@ -72,7 +70,7 @@ private void OnPrototypesReloaded(PrototypesReloadedEventArgs? ev) } } - foreach (var gas in PrototypeManager.EnumeratePrototypes()) + foreach (var gas in ProtoMan.EnumeratePrototypes()) { if (gas.Reagent == null) continue; @@ -85,20 +83,20 @@ private void OnPrototypesReloaded(PrototypesReloadedEventArgs? ev) // store the names of the entities used so we don't get repeats in the guide. var usedNames = new List(); - foreach (var entProto in PrototypeManager.EnumeratePrototypes()) + foreach (var entProto in ProtoMan.EnumeratePrototypes()) { if (entProto.Abstract || usedNames.Contains(entProto.Name)) continue; - if (!entProto.TryGetComponent(out var extractableComponent, EntityManager.ComponentFactory)) + if (!entProto.TryComp(out ExtractableComponent? extractableComponent, Factory)) continue; //these bloat the hell out of blood/fat - if (entProto.HasComponent()) + if (entProto.HasComp(Factory)) continue; //these feel obvious... - if (entProto.HasComponent()) + if (entProto.HasComp(Factory)) continue; if (extractableComponent.JuiceSolution is { } juiceSolution) diff --git a/Content.Client/Chemistry/EntitySystems/InjectorStatusControlSystem.cs b/Content.Client/Chemistry/EntitySystems/InjectorStatusControlSystem.cs index 87983661f25..6a7bf0ac41e 100644 --- a/Content.Client/Chemistry/EntitySystems/InjectorStatusControlSystem.cs +++ b/Content.Client/Chemistry/EntitySystems/InjectorStatusControlSystem.cs @@ -2,19 +2,16 @@ using Content.Client.Items; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.EntitySystems; -using Robust.Shared.Prototypes; namespace Content.Client.Chemistry.EntitySystems; public sealed partial class InjectorStatusControlSystem : EntitySystem { [Dependency] private SharedSolutionContainerSystem _solutionContainers = default!; - [Dependency] private IPrototypeManager _prototypeManager = default!; - public override void Initialize() { base.Initialize(); - Subs.ItemStatus(injector => new InjectorStatusControl(injector, _solutionContainers, _prototypeManager)); + Subs.ItemStatus(injector => new InjectorStatusControl(injector, _solutionContainers, ProtoMan)); } } diff --git a/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs b/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs index 0891dd8710b..7d2874bd114 100644 --- a/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs +++ b/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs @@ -8,13 +8,11 @@ using Content.Shared.Item; using Content.Shared.Rounding; using Robust.Client.GameObjects; -using Robust.Shared.Prototypes; namespace Content.Client.Chemistry.Visualizers; public sealed partial class SolutionContainerVisualsSystem : VisualizerSystem { - [Dependency] private IPrototypeManager _prototype = default!; [Dependency] private ItemSystem _itemSystem = default!; public override void Initialize() @@ -131,7 +129,7 @@ protected override void OnAppearanceChange(EntityUid uid, SolutionContainerVisua args.Component)) return null; - var reagentProto = _prototype.Index(baseOverride); + var reagentProto = ProtoMan.Index(baseOverride); if (SpriteSystem.LayerMapTryGet(ent, component.OverlayLayer, out var overlayLayer, false)) SpriteSystem.LayerSetVisible(ent, overlayLayer, reagentProto.MetamorphicSprite is not null); diff --git a/Content.Client/Clothing/Systems/ChameleonClothingSystem.cs b/Content.Client/Clothing/Systems/ChameleonClothingSystem.cs index 08153c3b3ff..deffb0de3d5 100644 --- a/Content.Client/Clothing/Systems/ChameleonClothingSystem.cs +++ b/Content.Client/Clothing/Systems/ChameleonClothingSystem.cs @@ -1,8 +1,6 @@ -using System.Linq; -using Content.Client.PDA; +using Content.Client.PDA; using Content.Shared.Clothing.Components; using Content.Shared.Clothing.EntitySystems; -using Content.Shared.Inventory; using Robust.Client.GameObjects; using Robust.Shared.Prototypes; @@ -35,14 +33,14 @@ protected override void UpdateSprite(EntityUid uid, EntityPrototype proto) { base.UpdateSprite(uid, proto); if (TryComp(uid, out SpriteComponent? sprite) - && proto.TryGetComponent(out SpriteComponent? otherSprite, Factory)) + && proto.TryComp(out SpriteComponent? otherSprite, Factory)) { sprite.CopyFrom(otherSprite); } // Edgecase for PDAs to include visuals when UI is open if (TryComp(uid, out PdaBorderColorComponent? borderColor) - && proto.TryGetComponent(out PdaBorderColorComponent? otherBorderColor, Factory)) + && proto.TryComp(out PdaBorderColorComponent? otherBorderColor, Factory)) { borderColor.BorderColor = otherBorderColor.BorderColor; borderColor.AccentHColor = otherBorderColor.AccentHColor; diff --git a/Content.Client/Clothing/UI/ChameleonBoundUserInterface.cs b/Content.Client/Clothing/UI/ChameleonBoundUserInterface.cs index c46c84f42df..9fe36d683c5 100644 --- a/Content.Client/Clothing/UI/ChameleonBoundUserInterface.cs +++ b/Content.Client/Clothing/UI/ChameleonBoundUserInterface.cs @@ -48,7 +48,7 @@ protected override void UpdateState(BoundUserInterfaceState state) if (string.IsNullOrEmpty(target) || !_proto.Resolve(target, out EntityPrototype? proto)) continue; - if (!proto.TryGetComponent(out TagComponent? tag, EntMan.ComponentFactory) || !_tag.HasTag(tag, st.RequiredTag)) + if (!proto.TryComp(out TagComponent? tag, EntMan.ComponentFactory) || !_tag.HasTag(tag, st.RequiredTag)) continue; newTargets.Add(target); diff --git a/Content.Client/Commands/OpenAHelpCommand.cs b/Content.Client/Commands/OpenAHelpCommand.cs index c6b2c12d38d..af6ffa49243 100644 --- a/Content.Client/Commands/OpenAHelpCommand.cs +++ b/Content.Client/Commands/OpenAHelpCommand.cs @@ -11,7 +11,7 @@ public sealed partial class OpenAHelpCommand : LocalizedCommands { [Dependency] private IUserInterfaceManager _userInterfaceManager = default!; - public override string Command => "openahelp"; + public override string Command => AdminCommandSyntax.NameOpenAdminHelp; public override string Help => LocalizationManager.GetString($"cmd-{Command}-help", ("command", Command)); diff --git a/Content.Client/Construction/ConstructionSystem.cs b/Content.Client/Construction/ConstructionSystem.cs index 5e4020365cd..08fa809a2b9 100644 --- a/Content.Client/Construction/ConstructionSystem.cs +++ b/Content.Client/Construction/ConstructionSystem.cs @@ -14,7 +14,6 @@ using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Prototypes; -using Robust.Shared.Utility; namespace Content.Client.Construction { @@ -45,6 +44,7 @@ public override void Initialize() WarmupRecipesCache(); UpdatesOutsidePrediction = true; + SubscribeLocalEvent(OnPrototypeReload); SubscribeLocalEvent(HandlePlayerAttached); SubscribeNetworkEvent(HandleAckStructure); SubscribeNetworkEvent(OnConstructionGuideReceived); @@ -76,11 +76,19 @@ public bool TryGetRecipePrototype(string constructionProtoId, [NotNullWhen(true) return false; } + private void OnPrototypeReload(PrototypesReloadedEventArgs obj) + { + if (obj.WasModified()) + WarmupRecipesCache(); + } + private void WarmupRecipesCache() { - foreach (var constructionProto in PrototypeManager.EnumeratePrototypes()) + _recipesMetadataCache.Clear(); + + foreach (var constructionProto in ProtoMan.EnumeratePrototypes()) { - if (!PrototypeManager.Resolve(constructionProto.Graph, out var graphProto)) + if (!ProtoMan.Resolve(constructionProto.Graph, out var graphProto)) continue; if (constructionProto.TargetNode is not { } targetNodeId) @@ -121,7 +129,7 @@ private void WarmupRecipesCache() // If we got the id of the prototype, we exit the “recursion” by clearing the stack. stack.Clear(); - if (!PrototypeManager.Resolve(entityId, out var proto)) + if (!ProtoMan.Resolve(entityId, out var proto)) continue; var name = constructionProto.SetName.HasValue ? Loc.GetString(constructionProto.SetName) : proto.Name; @@ -169,7 +177,7 @@ private void HandleConstructionGhostExamined(EntityUid uid, ConstructionGhostCom "construction-ghost-examine-message", ("name", component.Prototype.Name))); - if (!PrototypeManager.Resolve(component.Prototype.Graph, out var graph)) + if (!ProtoMan.Resolve(component.Prototype.Graph, out var graph)) return; var startNode = graph.Nodes[component.Prototype.StartNode]; @@ -270,7 +278,7 @@ public bool TrySpawnGhost( return false; } - if (!TryGetRecipePrototype(prototype.ID, out var targetProtoId) || !PrototypeManager.TryIndex(targetProtoId, out EntityPrototype? targetProto)) + if (!TryGetRecipePrototype(prototype.ID, out var targetProtoId) || !ProtoMan.TryIndex(targetProtoId, out EntityPrototype? targetProto)) return false; if (GhostPresent(loc)) @@ -292,7 +300,7 @@ public bool TrySpawnGhost( var sprite = Comp(ghost.Value); - if (targetProto.TryGetComponent(out IconComponent? icon, EntityManager.ComponentFactory)) + if (targetProto.TryComp(out IconComponent? icon, EntityManager.ComponentFactory)) { _sprite.AddBlankLayer((ghost.Value, sprite), 0); _sprite.LayerSetSprite((ghost.Value, sprite), 0, icon.Icon); @@ -304,9 +312,9 @@ public bool TrySpawnGhost( var dummy = EntityManager.SpawnEntity(targetProtoId, MapCoordinates.Nullspace); var targetSprite = EnsureComp(dummy); EntityManager.System().OnChangeData(dummy, targetSprite); - + var ghostDrawDepth = sprite.DrawDepth; _sprite.CopySprite((dummy, targetSprite), (ghost.Value, sprite)); - + _sprite.SetDrawDepth((ghost.Value, sprite), ghostDrawDepth); for (var i = 0; i < sprite.AllLayers.Count(); i++) { sprite.LayerSetShader(i, "unshaded"); diff --git a/Content.Client/Construction/FlatpackSystem.cs b/Content.Client/Construction/FlatpackSystem.cs index 4aea5282986..5cce2e22f65 100644 --- a/Content.Client/Construction/FlatpackSystem.cs +++ b/Content.Client/Construction/FlatpackSystem.cs @@ -25,10 +25,10 @@ private void OnAppearanceChange(Entity ent, ref AppearanceCha if (!_appearance.TryGetData(ent, FlatpackVisuals.Machine, out var machineBoardId) || args.Sprite == null) return; - if (!PrototypeManager.TryIndex(machineBoardId, out var machineBoardPrototype)) + if (!ProtoMan.TryIndex(machineBoardId, out var machineBoardPrototype)) return; - if (!machineBoardPrototype.TryGetComponent(out var sprite, EntityManager.ComponentFactory)) + if (!machineBoardPrototype.TryComp(out SpriteComponent? sprite, EntityManager.ComponentFactory)) return; Color? color = null; diff --git a/Content.Client/Construction/UI/ConstructionMenu.xaml b/Content.Client/Construction/UI/ConstructionMenu.xaml index 4020ca43326..29c1dfaf5ba 100644 --- a/Content.Client/Construction/UI/ConstructionMenu.xaml +++ b/Content.Client/Construction/UI/ConstructionMenu.xaml @@ -2,13 +2,36 @@ xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"> - + - - - + + + + + + + diff --git a/Content.Client/Construction/UI/ConstructionMenu.xaml.cs b/Content.Client/Construction/UI/ConstructionMenu.xaml.cs index d94150d5cd9..ba1c36b4de1 100644 --- a/Content.Client/Construction/UI/ConstructionMenu.xaml.cs +++ b/Content.Client/Construction/UI/ConstructionMenu.xaml.cs @@ -1,11 +1,15 @@ +using System.Linq; using System.Numerics; +using Content.Client.Stylesheets; using Content.Client.UserInterface.Controls; using Content.Shared.Construction.Prototypes; using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Prototypes; +using Robust.Shared.Utility; namespace Content.Client.Construction.UI { @@ -25,12 +29,12 @@ public interface IConstructionMenuView : IDisposable bool GridViewButtonPressed { get; set; } bool BuildButtonPressed { get; set; } - ListContainer Recipes { get; } - ItemList RecipeStepList { get; } + ListContainer ListViewRecipes { get; } + ScrollContainer GridViewRecipesScrollContainer { get; } + GridContainer GridViewRecipes { get; } - ScrollContainer RecipesGridScrollContainer { get; } - GridContainer RecipesGrid { get; } + ItemList RecipeStepList { get; } event EventHandler<(string search, string catagory)> PopulateRecipes; event EventHandler RecipeSelected; @@ -38,10 +42,18 @@ public interface IConstructionMenuView : IDisposable event EventHandler BuildButtonToggled; event EventHandler EraseButtonToggled; event EventHandler ClearAllGhosts; + event EventHandler PreviousRecipeInHistoryButtonPressed; + event EventHandler NextRecipeInHistoryButtonPressed; void ClearRecipeInfo(); void SetRecipeInfo(string name, string description, EntityPrototype? targetPrototype, bool isItem, bool isFavorite); void ResetPlacement(); + void TrySelectCategory(int categoryId); + void TrySelectListViewButton(ProtoId constructionProtoId); + void TrySelectGridViewButton(ContainerButton button, ContainerButton? previousButton); + void TogglePreviousRecipeButton(bool enabled); + void ToggleNextRecipeButton(bool enabled); + void UpdateGridViewButtonStyle(BaseButton button, bool selected); #region Window Control @@ -91,9 +103,9 @@ public ConstructionMenu() Title = Loc.GetString("construction-menu-title"); BuildButton.Text = Loc.GetString("construction-menu-place-ghost"); - Recipes.ItemPressed += (_, data) => RecipeSelected?.Invoke(this, data as ConstructionMenuListData); - Recipes.NoItemSelected += () => RecipeSelected?.Invoke(this, null); - Recipes.GenerateItem += (data, button) => + ListViewRecipes.ItemPressed += (_, data) => RecipeSelected?.Invoke(this, data as ConstructionMenuListData); + ListViewRecipes.NoItemSelected += () => RecipeSelected?.Invoke(this, null); + ListViewRecipes.GenerateItem += (data, button) => { if (data is not ConstructionMenuListData (var prototype, var targetPrototype)) return; @@ -124,12 +136,7 @@ public ConstructionMenu() SearchBar.OnTextChanged += _ => PopulateRecipes?.Invoke(this, (SearchBar.Text, Categories[OptionCategories.SelectedId])); - OptionCategories.OnItemSelected += obj => - { - OptionCategories.SelectId(obj.Id); - SearchBar.SetText(string.Empty); - PopulateRecipes?.Invoke(this, (SearchBar.Text, Categories[obj.Id])); - }; + OptionCategories.OnItemSelected += obj => OnCategorySelected(obj.Id); BuildButton.Text = Loc.GetString("construction-menu-place-ghost"); BuildButton.OnToggled += args => BuildButtonToggled?.Invoke(this, args.Pressed); @@ -138,6 +145,9 @@ public ConstructionMenu() EraseButton.Text = Loc.GetString("construction-menu-eraser-mode"); EraseButton.OnToggled += args => EraseButtonToggled?.Invoke(this, args.Pressed); + NextRecipeInHistoryButton.OnPressed += _ => NextRecipeInHistoryButtonPressed?.Invoke(this, EventArgs.Empty); + PreviousRecipeInHistoryButton.OnPressed += _ => PreviousRecipeInHistoryButtonPressed?.Invoke(this, EventArgs.Empty); + FavoriteButton.OnPressed += args => RecipeFavorited?.Invoke(this, EventArgs.Empty); MenuGridViewButton.OnPressed += _ => @@ -150,6 +160,8 @@ public ConstructionMenu() public event EventHandler? RecipeFavorited; public event EventHandler? BuildButtonToggled; public event EventHandler? EraseButtonToggled; + public event EventHandler? PreviousRecipeInHistoryButtonPressed; + public event EventHandler? NextRecipeInHistoryButtonPressed; public void ResetPlacement() { @@ -184,6 +196,102 @@ public void ClearRecipeInfo() RecipeStepList.Clear(); } - public sealed record ConstructionMenuListData(ConstructionPrototype Prototype, EntityPrototype TargetPrototype) : ListData; + public sealed record ConstructionMenuListData(ConstructionPrototype ConstructionProto, EntityPrototype EntityProto) : ListData; + + /// + /// Handle category selection. + /// + private void OnCategorySelected(int categoryId) + { + OptionCategories.SelectId(categoryId); + SearchBar.SetText(string.Empty); + PopulateRecipes?.Invoke(this, (SearchBar.Text, Categories[categoryId])); + } + + /// + /// Attempts to select category by its ID. + /// + /// Whether it was successful. + public void TrySelectCategory(int categoryId) + { + // do nothing if category is already selected + if (OptionCategories.SelectedId == categoryId) + { + return; + } + + OnCategorySelected(categoryId); + } + + /// + /// Attempts to select specified list view button using recipe ID. Only works when in list view. + /// + /// Whether it was successful. + public void TrySelectListViewButton(ProtoId constructionProtoId) + { + var isGridView = GridViewButtonPressed; + if (isGridView) + return; + + if (!ListViewRecipes.Data.TryFirstOrDefault( + data => ((ConstructionMenuListData)data).ConstructionProto.ID == constructionProtoId, + out var matchingData)) + { + return; + } + + ListViewRecipes.Select(matchingData); + } + + /// + /// Attempts to select specified grid view button using recipe ID. Only works when in grid view. + /// + /// Whether it was successful. + public void TrySelectGridViewButton(ContainerButton button, ContainerButton? previousButton) + { + var isGridView = GridViewButtonPressed; + if (!isGridView) + { + return; + } + + if (previousButton is not null && previousButton != button) + { + previousButton.Pressed = false; + UpdateGridViewButtonStyle(previousButton, false); + } + + button.Pressed = true; + UpdateGridViewButtonStyle(button, true); + } + + /// + /// Toggles previous recipe button on/off. + /// + /// Desired toggle state. + public void TogglePreviousRecipeButton(bool enabled) + { + PreviousRecipeInHistoryButton.Disabled = !enabled; + } + + /// d + /// + /// Desired toggle state. + public void ToggleNextRecipeButton(bool enabled) + { + NextRecipeInHistoryButton.Disabled = !enabled; + } + + + + public void UpdateGridViewButtonStyle(BaseButton button, bool selected) + { + if (button.Parent is not PanelContainer buttonPanel) + return; + + button.Children.Single().Modulate = selected ? Color.Green : Color.White; + var buttonColor = selected ? StyleNano.ButtonColorDefault : Color.Transparent; + buttonPanel.PanelOverride = new StyleBoxFlat { BackgroundColor = buttonColor }; + } } } diff --git a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs index 48c7ba93b37..450c3d1eaa9 100644 --- a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs +++ b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs @@ -41,11 +41,17 @@ internal sealed partial class ConstructionMenuPresenter : IDisposable private ConstructionSystem? _constructionSystem; private ConstructionPrototype? _selected; private List _favoritedRecipes = []; - private readonly Dictionary _recipeButtons = new(); + // map recipes to grid buttons since grid buttons can't hold metadata + private Dictionary, ContainerButton> _gridRecipeButtons = new(); private string _selectedCategory = string.Empty; + private List _recipeHistory = []; + /// Index of the selected recipe history record. -1 means no record is selected. + private int _recipeHistoryIndex = -1; private const string FavoriteCatName = "construction-category-favorites"; private const string ForAllCategoryName = "construction-category-all"; + /// How many recipes to remember in recipe history. After the limit is reached, older recipes past the limit will get discarded. + private const int RecipeHistoryLimit = 50; private bool CraftingAvailable { @@ -108,7 +114,8 @@ public ConstructionMenuPresenter() () => _uiManager.GetActiveUIWidget().CraftingButton.Pressed = false; _constructionView.ClearAllGhosts += (_, _) => _constructionSystem?.ClearAllGhosts(); _constructionView.PopulateRecipes += OnViewPopulateRecipes; - _constructionView.RecipeSelected += OnViewRecipeSelected; + _constructionView.RecipeSelected += + ((sender, data) => OnViewRecipeSelected(sender, data?.ConstructionProto)); _constructionView.BuildButtonToggled += (_, b) => BuildButtonToggled(b); _constructionView.EraseButtonToggled += (_, b) => { @@ -122,6 +129,11 @@ public ConstructionMenuPresenter() _constructionView.RecipeFavorited += (_, _) => OnViewFavoriteRecipe(); + _constructionView.PreviousRecipeInHistoryButtonPressed += + (sender, args) => TrySelectRecipeFromHistory(_recipeHistoryIndex - 1); + _constructionView.NextRecipeInHistoryButtonPressed += + (sender, args) => TrySelectRecipeFromHistory(_recipeHistoryIndex + 1); + SetFavorites(_preferencesManager.Preferences?.ConstructionFavorites ?? []); OnViewPopulateRecipes(_constructionView, (string.Empty, string.Empty)); } @@ -131,57 +143,11 @@ public void OnHudCraftingButtonToggled(BaseButton.ButtonToggledEventArgs args) WindowOpen = args.Pressed; } - /// - public void Dispose() - { - _constructionView.Dispose(); - - SystemBindingChanged(null); - _systemManager.SystemLoaded -= OnSystemLoaded; - _systemManager.SystemUnloaded -= OnSystemUnloaded; - - _placementManager.PlacementChanged -= OnPlacementChanged; - } - private void OnPlacementChanged(object? sender, EventArgs e) { _constructionView.ResetPlacement(); } - private void OnViewRecipeSelected(object? sender, ConstructionMenu.ConstructionMenuListData? item) - { - if (item is null) - { - _selected = null; - _constructionView.ClearRecipeInfo(); - return; - } - - _selected = item.Prototype; - - if (_placementManager is { IsActive: true, Eraser: false }) - UpdateGhostPlacement(); - - PopulateInfo(_selected); - } - - private void OnGridViewRecipeSelected(object? _, ConstructionPrototype? recipe) - { - if (recipe is null) - { - _selected = null; - _constructionView.ClearRecipeInfo(); - return; - } - - _selected = recipe; - - if (_placementManager is { IsActive: true, Eraser: false }) - UpdateGhostPlacement(); - - PopulateInfo(_selected); - } - private void OnViewPopulateRecipes(object? sender, (string search, string catagory) args) { if (_constructionSystem is null) @@ -189,17 +155,18 @@ private void OnViewPopulateRecipes(object? sender, (string search, string catago var actualRecipes = GetAndSortRecipes(args); - var recipesList = _constructionView.Recipes; - var recipesGrid = _constructionView.RecipesGrid; + var recipesList = _constructionView.ListViewRecipes; + var recipesGrid = _constructionView.GridViewRecipes; recipesGrid.RemoveAllChildren(); + _gridRecipeButtons.Clear(); - _constructionView.RecipesGridScrollContainer.Visible = _constructionView.GridViewButtonPressed; - _constructionView.Recipes.Visible = !_constructionView.GridViewButtonPressed; + _constructionView.GridViewRecipesScrollContainer.Visible = _constructionView.GridViewButtonPressed; + _constructionView.ListViewRecipes.Visible = !_constructionView.GridViewButtonPressed; if (_constructionView.GridViewButtonPressed) { recipesList.PopulateList([]); - PopulateGrid(recipesGrid, actualRecipes); + PopulateGridView(recipesGrid, actualRecipes); } else { @@ -207,55 +174,6 @@ private void OnViewPopulateRecipes(object? sender, (string search, string catago } } - private void PopulateGrid(GridContainer recipesGrid, - IEnumerable actualRecipes) - { - foreach (var recipe in actualRecipes) - { - var protoView = new EntityPrototypeView() - { - Scale = new Vector2(1.2f), - }; - protoView.SetPrototype(recipe.TargetPrototype); - - var itemButton = new ContainerButton() - { - VerticalAlignment = Control.VAlignment.Center, - Name = recipe.Prototype.Name, - ToolTip = recipe.Prototype.Name, - ToggleMode = true, - Children = { protoView }, - }; - - var itemButtonPanelContainer = new PanelContainer - { - PanelOverride = new StyleBoxFlat { BackgroundColor = StyleNano.ButtonColorDefault }, - Children = { itemButton }, - }; - - itemButton.OnToggled += buttonToggledEventArgs => - { - SelectGridButton(itemButton, buttonToggledEventArgs.Pressed); - - if (buttonToggledEventArgs.Pressed && - _selected != null && - _recipeButtons.TryGetValue(_selected.ID, out var oldButton)) - { - oldButton.Pressed = false; - SelectGridButton(oldButton, false); - } - - OnGridViewRecipeSelected(this, buttonToggledEventArgs.Pressed ? recipe.Prototype : null); - }; - - recipesGrid.AddChild(itemButtonPanelContainer); - _recipeButtons[recipe.Prototype.ID] = itemButton; - var isCurrentButtonSelected = _selected == recipe.Prototype; - itemButton.Pressed = isCurrentButtonSelected; - SelectGridButton(itemButton, isCurrentButtonSelected); - } - } - private List GetAndSortRecipes((string, string) args) { var recipes = new List(); @@ -301,66 +219,11 @@ private void PopulateGrid(GridContainer recipesGrid, } recipes.Sort( - (a, b) => string.Compare(a.Prototype.Name, b.Prototype.Name, StringComparison.InvariantCulture)); + (a, b) => string.Compare(a.ConstructionProto.Name, b.ConstructionProto.Name, StringComparison.InvariantCulture)); return recipes; } - private void SelectGridButton(BaseButton button, bool select) - { - if (button.Parent is not PanelContainer buttonPanel) - return; - - button.Children.Single().Modulate = select ? Color.Green : Color.White; - var buttonColor = select ? StyleNano.ButtonColorDefault : Color.Transparent; - buttonPanel.PanelOverride = new StyleBoxFlat { BackgroundColor = buttonColor }; - } - - private void PopulateCategories(string? selectCategory = null) - { - var uniqueCategories = new HashSet(); - - foreach (var prototype in _prototypeManager.EnumeratePrototypes()) - { - var category = prototype.Category; - - if (!string.IsNullOrEmpty(category)) - uniqueCategories.Add(category); - } - - var isFavorites = _favoritedRecipes.Count > 0; - var categoriesArray = new string[isFavorites ? uniqueCategories.Count + 2 : uniqueCategories.Count + 1]; - - // hard-coded to show all recipes - var idx = 0; - categoriesArray[idx++] = ForAllCategoryName; - - // hard-coded to show favorites if it need - if (isFavorites) - { - categoriesArray[idx++] = FavoriteCatName; - } - - var sortedProtoCategories = uniqueCategories.OrderBy(Loc.GetString); - - foreach (var cat in sortedProtoCategories) - { - categoriesArray[idx++] = cat; - } - - _constructionView.OptionCategories.Clear(); - - for (var i = 0; i < categoriesArray.Length; i++) - { - _constructionView.OptionCategories.AddItem(Loc.GetString(categoriesArray[i]), i); - - if (!string.IsNullOrEmpty(selectCategory) && selectCategory == categoriesArray[i]) - _constructionView.OptionCategories.SelectId(i); - } - - _constructionView.Categories = categoriesArray; - } - private void PopulateInfo(ConstructionPrototype? prototype) { if (_constructionSystem is null) @@ -450,6 +313,285 @@ private void BuildButtonToggled(bool pressed) _constructionView.BuildButtonPressed = pressed; } + private void OnViewRecipeSelected(object? sender, ConstructionPrototype? constructionProto, bool appendToHistory = true) + { + if (constructionProto is null) + { + _selected = null; + _constructionView.ClearRecipeInfo(); + return; + } + + _selected = constructionProto; + if (appendToHistory) + { + AppendToRecipeHistory(_selected, _constructionView.OptionCategories.SelectedId); + } + + if (_placementManager is { IsActive: true, Eraser: false }) + UpdateGhostPlacement(); + + PopulateInfo(_selected); + } + + #region View mode: Grid + + private void PopulateGridView(GridContainer recipesGrid, + IEnumerable actualRecipes) + { + foreach (var recipe in actualRecipes) + { + var protoView = new EntityPrototypeView() + { + Scale = new Vector2(1.2f), + }; + protoView.SetPrototype(recipe.EntityProto); + + var itemButton = new ContainerButton() + { + VerticalAlignment = Control.VAlignment.Center, + Name = recipe.ConstructionProto.Name, + ToolTip = recipe.ConstructionProto.Name, + ToggleMode = true, + Children = { protoView }, + }; + + var itemButtonPanelContainer = new PanelContainer + { + PanelOverride = new StyleBoxFlat { BackgroundColor = StyleNano.ButtonColorDefault }, + Children = { itemButton }, + }; + + itemButton.OnToggled += _ => + OnGridViewButtonToggled(recipe.ConstructionProto, itemButton); + + recipesGrid.AddChild(itemButtonPanelContainer); + _gridRecipeButtons[recipe.ConstructionProto.ID] = itemButton; + var isCurrentButtonSelected = _selected == recipe.ConstructionProto; + itemButton.Pressed = isCurrentButtonSelected; + _constructionView.UpdateGridViewButtonStyle(itemButton, isCurrentButtonSelected); + } + } + + /// + /// Handles grid view button toggle. + /// + private void OnGridViewButtonToggled(ConstructionPrototype constructionProto, ContainerButton button) + { + ContainerButton? previousButton = null; + if (_selected != null) + { + _gridRecipeButtons.TryGetValue(_selected, out previousButton); + } + + _constructionView.TrySelectGridViewButton(button, previousButton); + OnViewRecipeSelected(this, constructionProto); + } + + #endregion + + #region Categories + + private void PopulateCategories(string? selectCategory = null) + { + var uniqueCategories = new HashSet(); + + foreach (var prototype in _prototypeManager.EnumeratePrototypes()) + { + var category = prototype.Category; + + if (!string.IsNullOrEmpty(category)) + uniqueCategories.Add(category); + } + + var isFavorites = _favoritedRecipes.Count > 0; + var categoriesArray = new string[isFavorites ? uniqueCategories.Count + 2 : uniqueCategories.Count + 1]; + + // hard-coded to show all recipes + var idx = 0; + categoriesArray[idx++] = ForAllCategoryName; + + // hard-coded to show favorites if it need + if (isFavorites) + { + categoriesArray[idx++] = FavoriteCatName; + } + + var sortedProtoCategories = uniqueCategories.OrderBy(Loc.GetString); + + foreach (var cat in sortedProtoCategories) + { + categoriesArray[idx++] = cat; + } + + _constructionView.OptionCategories.Clear(); + + for (var i = 0; i < categoriesArray.Length; i++) + { + _constructionView.OptionCategories.AddItem(Loc.GetString(categoriesArray[i]), i); + + if (!string.IsNullOrEmpty(selectCategory) && selectCategory == categoriesArray[i]) + _constructionView.OptionCategories.SelectId(i); + } + + _constructionView.Categories = categoriesArray; + + // keep category indices in recipe history aligned to categories + for (var i = 0; i < _recipeHistory.Count; i++) + { + var entry = _recipeHistory[i]; + + // category index = category display ID, see "OptionCategories.AddItem" above. + if (entry.CategoryId == 0) + { + // 0 = "all" category. these should remain as is. + continue; + } + + var newCategoryDisplayId = Array.IndexOf(categoriesArray, entry.Category); + if (newCategoryDisplayId == -1) + { + // set to "all" if category wasn't found + newCategoryDisplayId = 0; + } + + if (newCategoryDisplayId != entry.CategoryId) + { + entry.CategoryId = newCategoryDisplayId; + _recipeHistory[i] = entry; + } + } + } + + #endregion + + #region Recipe history + + /// + /// Adds recipe to recipe history. + /// + /// If the recipe is duplicate of previous recipe in the history, it will be ignored. + /// If there are recipes that follow, they will be discarded. + /// + private void AppendToRecipeHistory(ConstructionPrototype constructionProto, int categoryId) + { + var previous = _recipeHistory.ElementAtOrDefault(_recipeHistoryIndex); + + // check if previous is the same as current + if (previous != default && previous.ConstructionProtoId.Id == constructionProto.ID) + { + return; + } + + // discard recipes that follow, if any + if (_recipeHistory.Count > _recipeHistoryIndex + 1) + { + var removeFromIdx = _recipeHistoryIndex + 1; + var numElementsToRemove = _recipeHistory.Count - (removeFromIdx + 1) + 1; + _recipeHistory.RemoveRange(removeFromIdx, numElementsToRemove); + } + + _recipeHistory.Add(new RecipeHistoryRecord() + { + ConstructionProtoId = constructionProto, + Category = constructionProto.Category, + CategoryId = categoryId + }); + _recipeHistoryIndex++; + + if (RecipeHistoryLimit > 0 && _recipeHistory.Count > RecipeHistoryLimit) + { + _recipeHistory.RemoveAt(0); + _recipeHistoryIndex--; + ClampRecipeHistoryIndex(); + } + + SyncRecipeHistoryButtons(); + } + + /// + /// Attempts to select recipe from history with specified index. The index is clamped first to be within history bounds. + /// + private void TrySelectRecipeFromHistory(int historyIndex) + { + _recipeHistoryIndex = historyIndex; + if (ClampRecipeHistoryIndex() < 0) + { + return; + } + + var record = _recipeHistory[_recipeHistoryIndex]; + TrySelectRecipeFromHistory(record); + } + + /// + /// Attempts to select recipe from history using a history entry. + /// + private void TrySelectRecipeFromHistory(RecipeHistoryRecord record) + { + if(!_prototypeManager.TryIndex(record.ConstructionProtoId, out var constructionProto)) + return; + + _constructionView.TrySelectCategory(record.CategoryId); + + var isGridView = _constructionView.GridViewButtonPressed; + if (isGridView) + { + if (_gridRecipeButtons.TryGetValue(constructionProto.ID, out var button)) + { + // use the press handler here it does exactly what we want. + OnGridViewButtonToggled(constructionProto, button); + } + } + else + { + _constructionView.TrySelectListViewButton(constructionProto.ID); + } + + OnViewRecipeSelected(this, constructionProto, appendToHistory:false); + SyncRecipeHistoryButtons(); + } + + /// + /// Toggle recipe history buttons based to recipy history state. + /// + private void SyncRecipeHistoryButtons() + { + if (ClampRecipeHistoryIndex() < 0) + { + _constructionView.TogglePreviousRecipeButton(false); + _constructionView.ToggleNextRecipeButton(false); + return; + } + + _constructionView.TogglePreviousRecipeButton(_recipeHistoryIndex > 0); + _constructionView.ToggleNextRecipeButton(_recipeHistoryIndex < _recipeHistory.Count - 1); + } + + /// + /// Clamps recipe history index to bounds of the recipes history list and writes it. + /// If there are no history entries, the index is set to -1. + /// + /// Resulting index. + private int ClampRecipeHistoryIndex() + { + switch (_recipeHistory.Count) + { + case 0: + _recipeHistoryIndex = -1; + break; + case >0: + _recipeHistoryIndex = int.Clamp(_recipeHistoryIndex, 0, _recipeHistory.Count - 1); + break; + } + + return _recipeHistoryIndex; + } + + #endregion + + #region Ghosts + private void UpdateGhostPlacement() { if (_selected == null) @@ -473,17 +615,9 @@ private void UpdateGhostPlacement() _constructionView.BuildButtonPressed = true; } - private void OnSystemLoaded(object? sender, SystemChangedArgs args) - { - if (args.System is ConstructionSystem system) - SystemBindingChanged(system); - } + #endregion - private void OnSystemUnloaded(object? sender, SystemChangedArgs args) - { - if (args.System is ConstructionSystem) - SystemBindingChanged(null); - } + #region Favorites private void OnViewFavoriteRecipe() { @@ -527,6 +661,32 @@ public void SetFavorites(IReadOnlyList> favorites PopulateCategories(_selectedCategory); } + #endregion + + /// + public void Dispose() + { + _constructionView.Dispose(); + + SystemBindingChanged(null); + _systemManager.SystemLoaded -= OnSystemLoaded; + _systemManager.SystemUnloaded -= OnSystemUnloaded; + + _placementManager.PlacementChanged -= OnPlacementChanged; + } + + private void OnSystemLoaded(object? sender, SystemChangedArgs args) + { + if (args.System is ConstructionSystem system) + SystemBindingChanged(system); + } + + private void OnSystemUnloaded(object? sender, SystemChangedArgs args) + { + if (args.System is ConstructionSystem) + SystemBindingChanged(null); + } + private void SystemBindingChanged(ConstructionSystem? newSystem) { if (newSystem is null) @@ -641,3 +801,42 @@ private void SystemGuideAvailable(object? sender, string e) } } } + +/// +/// Represents a construction menu recipe history entry. +/// +internal struct RecipeHistoryRecord : IEquatable +{ + public ProtoId ConstructionProtoId; + + /// + /// An ID to uniquely indentify category without relying on its label. + /// + public int CategoryId; + public string Category; + + public bool Equals(RecipeHistoryRecord other) + { + return this == other; + } + + public override bool Equals(object? obj) + { + return obj is RecipeHistoryRecord other && Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(ConstructionProtoId.GetHashCode(), CategoryId.GetHashCode()); + } + public static bool operator == (RecipeHistoryRecord left, RecipeHistoryRecord right) + { + return left.ConstructionProtoId.Equals(right.ConstructionProtoId) + && left.CategoryId == right.CategoryId; + } + + public static bool operator != (RecipeHistoryRecord left, RecipeHistoryRecord right) + { + return !left.ConstructionProtoId.Equals(right.ConstructionProtoId); + } +} diff --git a/Content.Client/Construction/UI/ConstructionMenuSheetlet.cs b/Content.Client/Construction/UI/ConstructionMenuSheetlet.cs new file mode 100644 index 00000000000..bc59d4b5eb7 --- /dev/null +++ b/Content.Client/Construction/UI/ConstructionMenuSheetlet.cs @@ -0,0 +1,26 @@ +using Content.Client.Stylesheets; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using static Content.Client.Stylesheets.StylesheetHelpers; + +namespace Content.Client.Construction.UI; +[CommonSheetlet] +public sealed class ConstructionMenuSheetlet : Sheetlet +{ + public override StyleRule[] GetRules(PalettedStylesheet sheet, object config) + { + return + [ + E [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 a2182dd93ce..13ff8a01376 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; @@ -28,8 +30,8 @@ namespace Content.Client.Damage; /// public sealed partial class DamageVisualsSystem : VisualizerSystem { - [Dependency] private IPrototypeManager _prototypeManager = default!; [Dependency] private DamageableSystem _damageable = default!; + [Dependency] private DisplacementMapSystem _displacement = default!; public override void Initialize() { @@ -153,7 +155,7 @@ private void InitializeVisualizer(EntityUid entity, DamageVisualsComponent damag // If the damage container on our entity's DamageableComponent // is not null, we can try to check through its groups. if (injurableComponent.DamageContainer != null - && _prototypeManager.Resolve(injurableComponent.DamageContainer, out var damageContainer)) + && ProtoMan.Resolve(injurableComponent.DamageContainer, out var damageContainer)) { // Are we using damage overlay sprites by group? // Check if the container matches the supported groups, @@ -189,7 +191,7 @@ private void InitializeVisualizer(EntityUid entity, DamageVisualsComponent damag // Ditto above, but instead we go through every group. else // oh boy! time to enumerate through every single group! { - var damagePrototypeIdList = _prototypeManager.EnumeratePrototypes() + var damagePrototypeIdList = ProtoMan.EnumeratePrototypes() .Select((p, _) => p.ID) .ToList(); if (damageVisComp.DamageOverlayGroups != null) @@ -327,10 +329,14 @@ private void InitializeVisualizer(EntityUid entity, DamageVisualsComponent damag /// 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 ); @@ -355,6 +361,12 @@ protected override void OnAppearanceChange(EntityUid uid, DamageVisualsComponent if (damageVisComp.Disabled) return; + if (AppearanceSystem.TryGetData(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); } @@ -504,7 +516,7 @@ private void UpdateDamageVisuals(Entity> delta, Enti if (!damageVisComp.Overlay && damageGroup != damageVisComp.DamageGroup) continue; - if (!_prototypeManager.TryIndex(damageGroup, out var damageGroupPrototype) + if (!ProtoMan.TryIndex(damageGroup, out var damageGroupPrototype) || !damage.TryGetDamageInGroup(damageGroupPrototype, out var damageTotal)) continue; @@ -543,7 +555,7 @@ private void UpdateDamageVisuals(List> delta, Enti } else { - UpdateOverlay((entity, spriteComponent, damageVisComp), damageGroup, threshold); + UpdateOverlay((entity, spriteComponent, damageVisComp), damageGroup, threshold, entity.Comp3.Displacement); } } @@ -615,7 +627,9 @@ private void UpdateTargetLayer(Entity spriteEnt, DamageVisualsC UpdateDamageLayerState(spriteEnt, spriteLayer, $"{layerState}", - threshold); + threshold, + $"{layerMapKey}trackDamage", + damageVisComp.Displacement); } } else if (!damageVisComp.Overlay) @@ -626,7 +640,9 @@ private void UpdateTargetLayer(Entity spriteEnt, DamageVisualsC UpdateDamageLayerState(spriteEnt, spriteLayer, $"{layerState}", - threshold); + threshold, + $"{layerMapKey}", + damageVisComp.Displacement); } } @@ -649,7 +665,9 @@ private void UpdateTargetLayer(Entity e (entity, spriteComponent), spriteLayer, $"{layerState}_{damageGroup}", - threshold); + threshold, + $"{layerMapKey}{damageGroup}", + entity.Comp2.Displacement); } } else if (!damageVisComp.Overlay) @@ -661,27 +679,31 @@ private void UpdateTargetLayer(Entity e (entity, spriteComponent), spriteLayer, $"{layerState}_{damageGroup}", - threshold); + threshold, + $"{layerMapKey}", + entity.Comp2.Displacement); } } /// /// 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; @@ -696,7 +718,9 @@ private void UpdateOverlay(Entity entit (entity, spriteComponent), spriteLayer, $"DamageOverlay_{damageGroup}", - threshold); + threshold, + $"DamageOverlay{damageGroup}", + displacement); } } } @@ -707,7 +731,7 @@ private void UpdateOverlay(Entity entit /// function calls it), and what threshold /// was passed into it. /// - 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) { @@ -720,6 +744,14 @@ private void UpdateDamageLayerState(Entity spriteEnt, int sprit SpriteSystem.LayerSetVisible(spriteEnt.AsNullable(), spriteLayer, true); } SpriteSystem.LayerSetRsiState(spriteEnt.AsNullable(), spriteLayer, $"{statePrefix}_{threshold}"); + if (displacement != null) + { + _displacement.TryAddDisplacement(displacement, spriteEnt, spriteLayer, layerKey, out _); + } + else + { + _displacement.EnsureDisplacementIsNotOnSprite(spriteEnt, layerKey); + } } } } diff --git a/Content.Client/Decals/DecalPlacementSystem.cs b/Content.Client/Decals/DecalPlacementSystem.cs index bab9c9b6888..7bbb05b29f2 100644 --- a/Content.Client/Decals/DecalPlacementSystem.cs +++ b/Content.Client/Decals/DecalPlacementSystem.cs @@ -19,10 +19,10 @@ public sealed partial class DecalPlacementSystem : EntitySystem { [Dependency] private IInputManager _inputManager = default!; [Dependency] private IOverlayManager _overlay = default!; - [Dependency] private IPrototypeManager _protoMan = default!; [Dependency] private InputSystem _inputSystem = default!; [Dependency] private MetaDataSystem _metaData = default!; [Dependency] private SharedActionsSystem _actions = default!; + [Dependency] private SharedMapSystem _maps = default!; [Dependency] private SharedTransformSystem _transform = default!; [Dependency] private SpriteSystem _sprite = default!; @@ -42,14 +42,14 @@ public sealed partial class DecalPlacementSystem : EntitySystem public (DecalPrototype? Decal, bool Snap, Angle Angle, Color Color) GetActiveDecal() { return _active && _decalId != null ? - (_protoMan.Index(_decalId), _snap, _decalAngle, _decalColor) : + (ProtoMan.Index(_decalId), _snap, _decalAngle, _decalColor) : (null, false, Angle.Zero, Color.Wheat); } public override void Initialize() { base.Initialize(); - _overlay.AddOverlay(new DecalPlacementOverlay(this, _transform, _sprite)); + _overlay.AddOverlay(new DecalPlacementOverlay(this, _maps, _transform, _sprite)); CommandBinds.Builder.Bind(EngineKeyFunctions.EditorPlaceObject, new PointerStateInputCmdHandler( (session, coords, uid) => @@ -143,7 +143,7 @@ private void OnFillSlot(FillActionSlotEvent ev) if (ev.Action != null) return; - if (_decalId == null || !_protoMan.TryIndex(_decalId, out var decalProto)) + if (_decalId == null || !ProtoMan.TryIndex(_decalId, out var decalProto)) return; var actionEvent = new PlaceDecalActionEvent() diff --git a/Content.Client/Decals/DecalSystem.cs b/Content.Client/Decals/DecalSystem.cs index 2b4b7c65834..54fcacf97e6 100644 --- a/Content.Client/Decals/DecalSystem.cs +++ b/Content.Client/Decals/DecalSystem.cs @@ -22,7 +22,7 @@ public override void Initialize() { base.Initialize(); - _overlay = new DecalOverlay(_sprites, EntityManager, PrototypeManager); + _overlay = new DecalOverlay(_sprites, EntityManager, ProtoMan); _overlayManager.AddOverlay(_overlay); SubscribeLocalEvent(OnHandleState); diff --git a/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs b/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs index e0e73af5f79..a9e68b32f6b 100644 --- a/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs +++ b/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs @@ -3,8 +3,6 @@ using Robust.Client.Graphics; using Robust.Client.Input; using Robust.Shared.Enums; -using Robust.Shared.Map; -using Robust.Shared.Prototypes; namespace Content.Client.Decals.Overlays; @@ -12,17 +10,18 @@ public sealed partial class DecalPlacementOverlay : Overlay { [Dependency] private IEyeManager _eyeManager = default!; [Dependency] private IInputManager _inputManager = default!; - [Dependency] private IMapManager _mapManager = default!; private readonly DecalPlacementSystem _placement; + private readonly SharedMapSystem _maps; private readonly SharedTransformSystem _transform; private readonly SpriteSystem _sprite; public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities; - public DecalPlacementOverlay(DecalPlacementSystem placement, SharedTransformSystem transform, SpriteSystem sprite) + public DecalPlacementOverlay(DecalPlacementSystem placement, SharedMapSystem maps, SharedTransformSystem transform, SpriteSystem sprite) { IoCManager.InjectDependencies(this); _placement = placement; + _maps = maps; _transform = transform; _sprite = sprite; ZIndex = 1000; @@ -42,7 +41,7 @@ protected override void Draw(in OverlayDrawArgs args) return; // No map support for decals - if (!_mapManager.TryFindGridAt(mousePos, out var gridUid, out var grid)) + if (!_maps.TryFindGridAt(mousePos, out var gridUid, out var grid)) { return; } diff --git a/Content.Client/Delivery/DeliveryVisualizerSystem.cs b/Content.Client/Delivery/DeliveryVisualizerSystem.cs index 10935f0faaf..6b6e93d6a27 100644 --- a/Content.Client/Delivery/DeliveryVisualizerSystem.cs +++ b/Content.Client/Delivery/DeliveryVisualizerSystem.cs @@ -8,7 +8,6 @@ namespace Content.Client.Delivery; public sealed partial class DeliveryVisualizerSystem : VisualizerSystem { [Dependency] private SharedAppearanceSystem _appearance = default!; - [Dependency] private IPrototypeManager _prototype = default!; private static readonly ProtoId UnknownIcon = "JobIconUnknown"; @@ -22,9 +21,9 @@ protected override void OnAppearanceChange(EntityUid uid, DeliveryComponent comp if (string.IsNullOrEmpty(job)) job = UnknownIcon; - if (!_prototype.TryIndex(job, out var icon)) + if (!ProtoMan.TryIndex(job, out var icon)) { - SpriteSystem.LayerSetTexture((uid, args.Sprite), DeliveryVisualLayers.JobStamp, SpriteSystem.Frame0(_prototype.Index(UnknownIcon).Icon)); + SpriteSystem.LayerSetTexture((uid, args.Sprite), DeliveryVisualLayers.JobStamp, SpriteSystem.Frame0(ProtoMan.Index(UnknownIcon).Icon)); return; } diff --git a/Content.Client/DirectionalArrowIndicator/DirectionalArrowIndicatorComponent.cs b/Content.Client/DirectionalArrowIndicator/DirectionalArrowIndicatorComponent.cs new file mode 100644 index 00000000000..4cff5fb501c --- /dev/null +++ b/Content.Client/DirectionalArrowIndicator/DirectionalArrowIndicatorComponent.cs @@ -0,0 +1,49 @@ +using Robust.Shared.Prototypes; +using System.Numerics; + +namespace Content.Client.DirectionalArrowIndicator; + +/// +/// Component that manages directional arrow indicators that spawn clientside when an entity is examined. +/// Handled by +/// +[RegisterComponent] +public sealed partial class DirectionalArrowIndicatorComponent : Component +{ + /// + /// The lifetime of the arrow indicator in seconds. + /// + [DataField] + public float Lifetime = 2f; + + /// + /// List of arrows to spawn. + /// + [DataField] + public List Arrows = new(); +} + +/// +/// Data for spawning a single directional arrow. +/// +[DataDefinition] +public sealed partial class ArrowSpawnData +{ + /// + /// Offset relative to entity's position where the arrow should appear. + /// + [DataField] + public Vector2 Offset = Vector2.Zero; + + /// + /// Rotation of the arrow. + /// + [DataField] + public Angle Rotation = Angle.Zero; + + /// + /// Prototype ID of the arrow entity to spawn. + /// + [DataField] + public EntProtoId ArrowType = "RedDirectionalArrowIndicator"; +} diff --git a/Content.Client/DirectionalArrowIndicator/DirectionalArrowIndicatorSystem.cs b/Content.Client/DirectionalArrowIndicator/DirectionalArrowIndicatorSystem.cs new file mode 100644 index 00000000000..aed285d7d5c --- /dev/null +++ b/Content.Client/DirectionalArrowIndicator/DirectionalArrowIndicatorSystem.cs @@ -0,0 +1,42 @@ +using Content.Shared.Examine; +using Robust.Client.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Spawners; + +namespace Content.Client.DirectionalArrowIndicator; + +/// +/// System responsible for handling s, +/// spawning directional arrow indicators clientside when an entity is examined. +/// +public sealed partial class DirectionalArrowIndicatorSystem : EntitySystem +{ + [Dependency] private TransformSystem _transform = default!; + + private const float EdgeOffset = 0.78125f; // Used for moving arrow to the edge of the tile by default. + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnExamined); + } + + private void OnExamined(Entity ent, ref ExaminedEvent args) + { + var arrows = ent.Comp.Arrows; + var lifetime = ent.Comp.Lifetime; + + if (arrows.Count == 0) + arrows.Add(new ArrowSpawnData()); + + foreach (var arrowData in ent.Comp.Arrows) + { + var spawnedEnt = Spawn(arrowData.ArrowType, new EntityCoordinates(ent, arrowData.Offset.X, arrowData.Offset.Y + EdgeOffset)); + + _transform.SetLocalRotation(spawnedEnt, arrowData.Rotation); + + EnsureComp(spawnedEnt, out var timedDespawn); + timedDespawn.Lifetime = lifetime; + } + } +} diff --git a/Content.Client/DisplacementMap/DisplacementMapSystem.cs b/Content.Client/DisplacementMap/DisplacementMapSystem.cs index be44b8fedb0..738bf690cd4 100644 --- a/Content.Client/DisplacementMap/DisplacementMapSystem.cs +++ b/Content.Client/DisplacementMap/DisplacementMapSystem.cs @@ -41,7 +41,8 @@ public bool TryAddDisplacement( if (displacementKey is null) return false; - EnsureDisplacementIsNotOnSprite(sprite, key); + if (EnsureDisplacementIsNotOnSprite(sprite, key)) + index--; if (data.ShaderOverride is not null) { @@ -91,10 +92,21 @@ public bool TryAddDisplacement( var displacementLayer = _serialization.CreateCopy(displacementDataLayer, notNullableOverride: true); - // This previously assigned a string reading "this is impossible" if key.ToString eval'd to false. - // However, for the sake of sanity, we've changed this to assert non-null - !. - // If this throws an error, we're not sorry. Nanotrasen thanks you for your service fixing this bug. - displacementLayer.CopyToShaderParameters!.LayerKey = key.ToString()!; + if (key is Enum) + { + // We are doing this enum-to-string conversion here because CopyToShaderParameters.LayerKey only takes a string, + // but LayerMap keys are stored as objects, and therefore can take enums. + // There is a key parser in SpriteComponent but it requires the qualified (i.e. full) enum name. + // It feels like CopyToShaderParameters should be able to just take objects, but until then: + displacementLayer.CopyToShaderParameters!.LayerKey = $"enum.{key.GetType().Name}.{key}"; + } + else + { + // This previously assigned a string reading "this is impossible" if key.ToString eval'd to false. + // However, for the sake of sanity, we've changed this to assert non-null - !. + // If this throws an error, we're not sorry. Nanotrasen thanks you for your service fixing this bug. + displacementLayer.CopyToShaderParameters!.LayerKey = key.ToString()!; + } _sprite.AddLayer(sprite.AsNullable(), displacementLayer, index); _sprite.LayerMapSet(sprite.AsNullable(), displacementKey, index); @@ -107,13 +119,13 @@ public bool TryAddDisplacement( /// /// 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/DoAfter/DoAfterSystem.cs b/Content.Client/DoAfter/DoAfterSystem.cs index 0c6bf1fb199..7bf968115a5 100644 --- a/Content.Client/DoAfter/DoAfterSystem.cs +++ b/Content.Client/DoAfter/DoAfterSystem.cs @@ -1,9 +1,7 @@ using System.Diagnostics.CodeAnalysis; using Content.Shared.DoAfter; -using Content.Shared.Hands.Components; using Robust.Client.Graphics; using Robust.Client.Player; -using Robust.Shared.Prototypes; namespace Content.Client.DoAfter; @@ -15,13 +13,12 @@ public sealed partial class DoAfterSystem : SharedDoAfterSystem { [Dependency] private IOverlayManager _overlay = default!; [Dependency] private IPlayerManager _player = default!; - [Dependency] private IPrototypeManager _prototype = default!; [Dependency] private MetaDataSystem _metadata = default!; public override void Initialize() { base.Initialize(); - _overlay.AddOverlay(new DoAfterOverlay(EntityManager, _prototype, GameTiming, _player)); + _overlay.AddOverlay(new DoAfterOverlay(EntityManager, ProtoMan, GameTiming, _player)); } public override void Shutdown() diff --git a/Content.Client/Doors/DoorSystem.cs b/Content.Client/Doors/DoorSystem.cs index 7b2ac182808..d72589fed61 100644 --- a/Content.Client/Doors/DoorSystem.cs +++ b/Content.Client/Doors/DoorSystem.cs @@ -3,15 +3,12 @@ using Content.Shared.SprayPainter.Prototypes; using Robust.Client.Animations; using Robust.Client.GameObjects; -using Robust.Shared.Prototypes; namespace Content.Client.Doors; public sealed partial class DoorSystem : SharedDoorSystem { [Dependency] private AnimationPlayerSystem _animationSystem = default!; - [Dependency] private IComponentFactory _componentFactory = default!; - [Dependency] private IPrototypeManager _prototypeManager = default!; [Dependency] private SpriteSystem _sprite = default!; public override void Initialize() @@ -209,10 +206,10 @@ private void UpdateAppearanceForDoorState(Entity entity, SpriteCo private void UpdateSpriteLayers(Entity sprite, string targetProto) { - if (!_prototypeManager.Resolve(targetProto, out var target)) + if (!ProtoMan.Resolve(targetProto, out var target)) return; - if (!target.TryGetComponent(out SpriteComponent? targetSprite, _componentFactory)) + if (!target.TryComp(out SpriteComponent? targetSprite, Factory)) return; _sprite.SetBaseRsi(sprite.AsNullable(), targetSprite.BaseRSI); diff --git a/Content.Client/Doors/TurnstileSystem.cs b/Content.Client/Doors/TurnstileSystem.cs index 9b201547a3c..b9c4af26aee 100644 --- a/Content.Client/Doors/TurnstileSystem.cs +++ b/Content.Client/Doors/TurnstileSystem.cs @@ -15,8 +15,6 @@ public sealed partial class TurnstileSystem : SharedTurnstileSystem [Dependency] private AnimationPlayerSystem _animationPlayer = default!; [Dependency] private SpriteSystem _sprite = default!; - private static readonly EntProtoId ExamineArrow = "TurnstileArrow"; - private const string AnimationKey = "Turnstile"; public override void Initialize() @@ -24,7 +22,6 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnAnimationCompleted); - SubscribeLocalEvent(OnExamined); } private void OnAnimationCompleted(Entity ent, ref AnimationCompletedEvent args) @@ -37,11 +34,6 @@ private void OnAnimationCompleted(Entity ent, ref AnimationC _sprite.LayerSetRsiState((ent.Owner, sprite), TurnstileVisualLayers.Base, new RSI.StateId(ent.Comp.DefaultState)); } - private void OnExamined(Entity ent, ref ExaminedEvent args) - { - Spawn(ExamineArrow, new EntityCoordinates(ent, 0, 0)); - } - protected override void PlayAnimation(EntityUid uid, string stateId) { if (!TryComp(uid, out var animation) || !TryComp(uid, out var sprite)) diff --git a/Content.Client/Drugs/RainbowOverlay.cs b/Content.Client/Drugs/RainbowOverlay.cs index 68a00c050c3..1884ead4bb1 100644 --- a/Content.Client/Drugs/RainbowOverlay.cs +++ b/Content.Client/Drugs/RainbowOverlay.cs @@ -44,7 +44,7 @@ public RainbowOverlay() _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 12974049f54..80917c89706 100644 --- a/Content.Client/Drunk/DrunkOverlay.cs +++ b/Content.Client/Drunk/DrunkOverlay.cs @@ -50,7 +50,7 @@ public DrunkOverlay() 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/Eui/EuiManager.cs b/Content.Client/Eui/EuiManager.cs index 03ab3d2f7ea..0682d6f8794 100644 --- a/Content.Client/Eui/EuiManager.cs +++ b/Content.Client/Eui/EuiManager.cs @@ -18,6 +18,9 @@ public sealed partial class EuiManager private readonly Dictionary _openUis = new(); + /// + /// Initialisation of the EuiManager. + /// public void Initialize() { _net.RegisterNetMessage(RxMsgCtl); diff --git a/Content.Client/Examine/ExamineSystem.cs b/Content.Client/Examine/ExamineSystem.cs index 1fce277251f..9e6fbb0e490 100644 --- a/Content.Client/Examine/ExamineSystem.cs +++ b/Content.Client/Examine/ExamineSystem.cs @@ -386,6 +386,10 @@ private void AddVerbsToTooltip(IEnumerable verbs) vbox.AddChild(buttonsHBox); } + /// + /// Handler for the OnClick Event of a Verb Button. + /// + /// The event args public void VerbButtonPressed(BaseButton.ButtonEventArgs obj) { if (obj.Button is ExamineButton button) @@ -396,6 +400,12 @@ public void VerbButtonPressed(BaseButton.ButtonEventArgs obj) } } + /// + /// Open and fills by query to server an examine menu with the correct tooltip and menu items. + /// + /// The entity examined. + /// If the popup should be on the cursor or the entity's screen position. + /// An optional user object; if null we assume the client's player entity. public void DoExamine(EntityUid entity, bool centeredOnCursor = true, EntityUid? userOverride = null) { var playerEnt = userOverride ?? _playerManager.LocalEntity; diff --git a/Content.Client/Explosion/ClusterGrenadeVisualsComponent.cs b/Content.Client/Explosion/ClusterGrenadeVisualsComponent.cs index 3c97f557b6a..59db451b9ac 100644 --- a/Content.Client/Explosion/ClusterGrenadeVisualsComponent.cs +++ b/Content.Client/Explosion/ClusterGrenadeVisualsComponent.cs @@ -1,9 +1,18 @@ +using Content.Shared.Explosion.EntitySystems; namespace Content.Client.Explosion; +/// +/// The Visual Component for the Cluster Grenade. +/// +/// [RegisterComponent] [Access(typeof(ClusterGrenadeVisualizerSystem))] public sealed partial class ClusterGrenadeVisualsComponent : Component { + /// + /// Used to select the correct layer from the rsi together with the grenade amount. + /// + /// [DataField("state")] public string? State; } diff --git a/Content.Client/Explosion/ExplosionOverlaySystem.cs b/Content.Client/Explosion/ExplosionOverlaySystem.cs index 98923a48dd7..5cde7b5f4ef 100644 --- a/Content.Client/Explosion/ExplosionOverlaySystem.cs +++ b/Content.Client/Explosion/ExplosionOverlaySystem.cs @@ -4,7 +4,6 @@ using Robust.Client.ResourceManagement; using Robust.Shared.GameStates; using Robust.Shared.Graphics.RSI; -using Robust.Shared.Prototypes; namespace Content.Client.Explosion; @@ -14,7 +13,6 @@ namespace Content.Client.Explosion; /// public sealed partial class ExplosionOverlaySystem : EntitySystem { - [Dependency] private IPrototypeManager _protoMan = default!; [Dependency] private IResourceCache _resCache = default!; [Dependency] private IOverlayManager _overlayMan = default!; [Dependency] private SharedPointLightSystem _lights = default!; @@ -61,7 +59,7 @@ private void OnExplosionInit(EntityUid uid, ExplosionVisualsComponent component, { EnsureComp(uid); - if (!_protoMan.TryIndex(component.ExplosionType, out ExplosionPrototype? type) || + if (!ProtoMan.TryIndex(component.ExplosionType, out ExplosionPrototype? type) || !TryComp(uid, out ExplosionVisualsTexturesComponent? textures)) { return; diff --git a/Content.Client/Explosion/ExplosionVisualsTexturesComponent.cs b/Content.Client/Explosion/ExplosionVisualsTexturesComponent.cs index b8641e6cee4..7a58fed4133 100644 --- a/Content.Client/Explosion/ExplosionVisualsTexturesComponent.cs +++ b/Content.Client/Explosion/ExplosionVisualsTexturesComponent.cs @@ -3,6 +3,9 @@ namespace Content.Client.Explosion; +/// +/// The client side visuals system for explosions. +/// [RegisterComponent] public sealed partial class ExplosionVisualsTexturesComponent : Component { diff --git a/Content.Client/Eye/Blinding/BlurryVisionOverlay.cs b/Content.Client/Eye/Blinding/BlurryVisionOverlay.cs index ee7c9eef018..1c14dfd6d58 100644 --- a/Content.Client/Eye/Blinding/BlurryVisionOverlay.cs +++ b/Content.Client/Eye/Blinding/BlurryVisionOverlay.cs @@ -92,7 +92,7 @@ protected override void Draw(in OverlayDrawArgs args) // 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/Flash/FlashOverlay.cs b/Content.Client/Flash/FlashOverlay.cs index c833ad69ac0..844c1c3f0cc 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.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"; + 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!; + [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 SharedFlashSystem _flash; - private readonly StatusEffectsSystem _statusSys; + private readonly StatusEffectsSystem _statusSys; - public override OverlaySpace Space => OverlaySpace.WorldSpace; - private readonly ShaderInstance _shader; - private bool _reducedMotion; - public float PercentComplete; - public Texture? ScreenshotTexture; + public override OverlaySpace Space => OverlaySpace.WorldSpace; + private readonly ShaderInstance _shader; + private bool _reducedMotion; + public float PercentComplete; + public Texture? ScreenshotTexture; - public FlashOverlay() - { - IoCManager.InjectDependencies(this); - _shader = _prototypeManager.Index(FlashedEffectShader).InstanceUnique(); - _flash = _entityManager.System(); - _statusSys = _entityManager.System(); + public FlashOverlay() + { + IoCManager.InjectDependencies(this); + _shader = _prototypeManager.Index(FlashedEffectShader).InstanceUnique(); + _statusSys = _entityManager.System(); - _configManager.OnValueChanged(CCVars.ReducedMotion, (b) => { _reducedMotion = b; }, invokeImmediately: true); - } + _configManager.OnValueChanged(CCVars.DisableFlashEffect, (b) => { _reducedMotion = b; }, invokeImmediately: true); + } - protected override void FrameUpdate(FrameEventArgs args) - { - var playerEntity = _playerManager.LocalEntity; + protected override void FrameUpdate(FrameEventArgs args) + { + var playerEntity = _playerManager.LocalEntity; - if (playerEntity == null) - return; + if (playerEntity == null) + return; - if (!_entityManager.HasComponent(playerEntity) - || !_entityManager.TryGetComponent(playerEntity, out var status)) - return; + if (!_statusSys.HasEffectComp(playerEntity)) + return; - if (!_statusSys.TryGetTime(playerEntity.Value, _flash.FlashedKey, out var time, status)) - return; + if (!_statusSys.TryGetTime(playerEntity.Value, SharedFlashSystem.FlashedKey, out var time)) + return; - var curTime = _timing.CurTime; - var lastsFor = (float)(time.Value.Item2 - time.Value.Item1).TotalSeconds; - var timeDone = (float)(curTime - time.Value.Item1).TotalSeconds; + var curTime = _timing.CurTime; + if (time.StartEffectTime == null || time.EndEffectTime == null) + return; - PercentComplete = timeDone / lastsFor; - } + var lastsFor = (float)(time.EndEffectTime.Value - time.StartEffectTime.Value).TotalSeconds; + var timeDone = (float)(curTime - time.StartEffectTime.Value).TotalSeconds; - 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; + PercentComplete = timeDone / lastsFor; + } - return PercentComplete < 1.0f; - } + 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; - protected override void Draw(in OverlayDrawArgs args) + return PercentComplete < 1.0f; + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (RequestScreenTexture && ScreenTexture != null) { - 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); - } + 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 DisposeBehavior() + var worldHandle = args.WorldHandle; + if (_reducedMotion) { - base.DisposeBehavior(); - ScreenshotTexture = null; + // 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; } } diff --git a/Content.Client/Flash/FlashSystem.cs b/Content.Client/Flash/FlashSystem.cs index 22631b7ec53..59db0f9605f 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) - { - _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) + [SubscribeLocalEvent] + private void OnApplied(Entity ent, ref StatusEffectAppliedEvent 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/Gameplay/GameplayStateBase.cs b/Content.Client/Gameplay/GameplayStateBase.cs index 97c6e8a8b23..975164722f1 100644 --- a/Content.Client/Gameplay/GameplayStateBase.cs +++ b/Content.Client/Gameplay/GameplayStateBase.cs @@ -37,7 +37,6 @@ public partial class GameplayStateBase : State, IEntityEventSubscriber [Dependency] private IPlayerManager _playerManager = default!; [Dependency] private IEntitySystemManager _entitySystemManager = default!; [Dependency] private IGameTiming _timing = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] protected IUserInterfaceManager UserInterfaceManager = default!; [Dependency] private IEntityManager _entityManager = default!; [Dependency] private IViewVariablesManager _vvm = default!; @@ -240,7 +239,7 @@ protected virtual void OnKeyBindStateChanged(ViewportBoundKeyEventArgs args) var transformSystem = _entitySystemManager.GetEntitySystem(); var mapSystem = _entitySystemManager.GetEntitySystem(); - coordinates = _mapManager.TryFindGridAt(mousePosWorld, out var uid, out _) ? + coordinates = mapSystem.TryFindGridAt(mousePosWorld, out var uid, out _) ? mapSystem.MapToGrid(uid, mousePosWorld) : transformSystem.ToCoordinates(mousePosWorld); } diff --git a/Content.Client/Ghost/GhostSystem.cs b/Content.Client/Ghost/GhostSystem.cs index d4b1dfca0e6..ab60e186545 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 @@ public sealed partial class GhostSystem : SharedGhostSystem [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 @@ private void OnToggleLighting(EntityUid uid, EyeComponent component, ToggleLight 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/GuidebookWindow.xaml b/Content.Client/Guidebook/Controls/GuidebookWindow.xaml index 69534af7f6a..9ee62e503b2 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 GuidebookWindow() _sawmill = Logger.GetSawmill("guidebook"); Tree.OnSelectedItemChanged += OnSelectionChanged; + TableOfContents.OnSelectedItemChanged += OnTableOfContentsSelectionChanged; SearchBar.OnTextChanged += _ => { @@ -76,7 +77,7 @@ public void HandleAnchor(IPrototypeLinkControl prototypeLinkControl) UserInterfaceManager.DeferAction(() => { - if (control.GetControlScrollPosition() is not {} position) + if (control.GetControlScrollPosition() is not { } position) return; Scroll.HScrollTarget = position.X; @@ -91,6 +92,10 @@ private void OnSelectionChanged(TreeItem? item) { 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 @@ private void OnSelectionChanged(TreeItem? item) 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 @@ private void ShowGuide(GuideEntry entry) _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 @@ private void ShowGuide(GuideEntry entry) if (prototype != null && availablePrototypeLinks.Contains(prototype)) linkControl.EnablePrototypeLink(); } + + RepopulateTableOfContents(); + } + + 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