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 @@
+
+
diff --git a/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs b/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs
index 62cb64050ea..25e40d40aa2 100644
--- a/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs
+++ b/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs
@@ -99,6 +99,16 @@ public void SetSharedConnections(int sharedConnections)
SharedConnections.Text = Loc.GetString("player-panel-shared-connections", ("sharedConnections", sharedConnections));
}
+ public void SetTrustScore(float trustScore)
+ {
+ TrustScore.Text = Loc.GetString("player-panel-trust-score", ("trustScore", trustScore));
+ }
+
+ public void SetAccountCreationDate(DateTime? date)
+ {
+ AccountCreationDate.Text = date != null ? Loc.GetString("player-panel-account-creation", ("date", date.Value)) : null;
+ }
+
public void SetPlaytime(TimeSpan playtime)
{
Playtime.Text = Loc.GetString("player-panel-playtime",
diff --git a/Content.Client/Administration/UI/PlayerPanel/PlayerPanelEui.cs b/Content.Client/Administration/UI/PlayerPanel/PlayerPanelEui.cs
index 1828d34f2f2..8a4d92169fa 100644
--- a/Content.Client/Administration/UI/PlayerPanel/PlayerPanelEui.cs
+++ b/Content.Client/Administration/UI/PlayerPanel/PlayerPanelEui.cs
@@ -69,6 +69,8 @@ public override void HandleState(EuiStateBase state)
PlayerPanel.SetSharedConnections(s.SharedConnections);
PlayerPanel.SetFrozen(s.CanFreeze, s.Frozen);
PlayerPanel.SetAhelp(s.CanAhelp);
+ PlayerPanel.SetTrustScore(s.TrustScore);
+ PlayerPanel.SetAccountCreationDate(s.AccountCreationDate);
PlayerPanel.SetButtons();
}
}
diff --git a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs
index 3691ae18524..0c705a48919 100644
--- a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs
+++ b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs
@@ -9,6 +9,7 @@
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
+using Robust.Shared.Prototypes;
using static Content.Client.Administration.UI.Tabs.PlayerTab.PlayerTabHeader;
using static Robust.Client.UserInterface.Controls.BaseButton;
@@ -20,6 +21,7 @@ public sealed partial class PlayerTab : Control
[Dependency] private IEntityManager _entManager = default!;
[Dependency] private IConfigurationManager _config = default!;
[Dependency] private IPlayerManager _playerMan = default!;
+ [Dependency] private IPrototypeManager _proto = default!;
private const string ArrowUp = "↑";
private const string ArrowDown = "↓";
@@ -155,7 +157,12 @@ private void RefreshPlayerList(IReadOnlyList players)
UpdateHeaderSymbols();
SearchList.PopulateList(sortedPlayers.Select(info => new PlayerListData(info,
- $"{info.Username} {info.CharacterName} {info.IdentityName} {info.StartingJob}"))
+ $"{info.Username} " +
+ $"{info.CharacterName} " +
+ $"{info.IdentityName} " +
+ $"{info.StartingJob} " +
+ $"{Loc.GetString(info.Subtype ?? string.Empty)} " +
+ $"{(_proto.TryIndex(info.RoleProto, out var proto) ? Loc.GetString(proto.Name) : string.Empty)}"))
.ToList());
}
@@ -238,7 +245,9 @@ private int Compare(PlayerInfo x, PlayerInfo y)
Header.Username => Compare(x.Username, y.Username),
Header.Character => Compare(x.CharacterName, y.CharacterName),
Header.Job => Compare(x.StartingJob, y.StartingJob),
- Header.RoleType => y.SortWeight - x.SortWeight,
+ Header.RoleType => y.SortWeight != x.SortWeight
+ ? y.SortWeight - x.SortWeight
+ : string.Compare(x.RoleProto?.Id ?? "", y.RoleProto?.Id ?? "", StringComparison.Ordinal),
Header.Playtime => TimeSpan.Compare(x.OverallPlaytime ?? default, y.OverallPlaytime ?? default),
_ => 1
};
diff --git a/Content.Client/Administration/UI/Tabs/ServerTab.xaml b/Content.Client/Administration/UI/Tabs/ServerTab.xaml
index 80c186f7fd0..f3a6421aea7 100644
--- a/Content.Client/Administration/UI/Tabs/ServerTab.xaml
+++ b/Content.Client/Administration/UI/Tabs/ServerTab.xaml
@@ -4,10 +4,31 @@
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Margin="4"
MinSize="50 50">
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Administration/UI/Tabs/ServerTab.xaml.cs b/Content.Client/Administration/UI/Tabs/ServerTab.xaml.cs
index c025a93d585..e8ba41a3fe9 100644
--- a/Content.Client/Administration/UI/Tabs/ServerTab.xaml.cs
+++ b/Content.Client/Administration/UI/Tabs/ServerTab.xaml.cs
@@ -1,4 +1,5 @@
-using Content.Shared.CCVar;
+using System.Globalization;
+using Content.Shared.CCVar;
using Robust.Client.AutoGenerated;
using Robust.Client.Console;
using Robust.Client.UserInterface;
@@ -20,8 +21,31 @@ public ServerTab()
_config.OnValueChanged(CCVars.OocEnabled, OocEnabledChanged, true);
_config.OnValueChanged(CCVars.LoocEnabled, LoocEnabledChanged, true);
+ _config.OnValueChanged(CCVars.DeadChatEnabled, DeadChatEnabledChanged, true);
+ _config.OnValueChanged(CCVars.AfkTime, AfkTimeChanged, true);
+ _config.OnValueChanged(CCVars.AdminAfkTime, AdminAfkTimeChanged, true);
+ _config.OnValueChanged(CCVars.AfkConfirmTimeout, AfkConfirmTimeoutChanged, true);
ServerShutdownButton.OnPressed += _ => _console.ExecuteCommand("shutdown");
+
+ AfkTime.OnTextEntered += args => SendFloatCVar(CCVars.AfkTime.Name, args.Text);
+ AfkTime.OnFocusExit += args => SendFloatCVar(CCVars.AfkTime.Name, args.Text);
+ AdminAfkTime.OnTextEntered += args => SendFloatCVar(CCVars.AdminAfkTime.Name, args.Text);
+ AdminAfkTime.OnFocusExit += args => SendFloatCVar(CCVars.AdminAfkTime.Name, args.Text);
+ AfkConfirmTimeout.OnTextEntered += args => SendFloatCVar(CCVars.AfkConfirmTimeout.Name, args.Text);
+ AfkConfirmTimeout.OnFocusExit += args => SendFloatCVar(CCVars.AfkConfirmTimeout.Name, args.Text);
+ }
+
+ private void SendFloatCVar(string cvar, string text)
+ {
+ if (string.IsNullOrWhiteSpace(text) ||
+ !float.TryParse(text, CultureInfo.InvariantCulture, out var value) ||
+ value < 0f)
+ {
+ return;
+ }
+
+ _console.ExecuteCommand($"changecvar {cvar} {value.ToString(CultureInfo.InvariantCulture)}");
}
private void OocEnabledChanged(bool value)
@@ -34,6 +58,26 @@ private void LoocEnabledChanged(bool value)
SetLoocButton.Pressed = value;
}
+ private void DeadChatEnabledChanged(bool value)
+ {
+ SetDeadChatButton.Pressed = value;
+ }
+
+ private void AfkTimeChanged(float value)
+ {
+ AfkTime.PlaceHolder = value.ToString(CultureInfo.InvariantCulture);
+ }
+
+ private void AdminAfkTimeChanged(float value)
+ {
+ AdminAfkTime.PlaceHolder = value.ToString(CultureInfo.InvariantCulture);
+ }
+
+ private void AfkConfirmTimeoutChanged(float value)
+ {
+ AfkConfirmTimeout.PlaceHolder = value.ToString(CultureInfo.InvariantCulture);
+ }
+
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
@@ -42,6 +86,9 @@ protected override void Dispose(bool disposing)
{
_config.UnsubValueChanged(CCVars.OocEnabled, OocEnabledChanged);
_config.UnsubValueChanged(CCVars.LoocEnabled, LoocEnabledChanged);
+ _config.UnsubValueChanged(CCVars.AfkTime, AfkTimeChanged);
+ _config.UnsubValueChanged(CCVars.AdminAfkTime, AdminAfkTimeChanged);
+ _config.UnsubValueChanged(CCVars.AfkConfirmTimeout, AfkConfirmTimeoutChanged);
}
}
}
diff --git a/Content.Client/Afk/AfkConfirmEui.cs b/Content.Client/Afk/AfkConfirmEui.cs
new file mode 100644
index 00000000000..1214be70be8
--- /dev/null
+++ b/Content.Client/Afk/AfkConfirmEui.cs
@@ -0,0 +1,85 @@
+using System.Numerics;
+using Content.Client.Eui;
+using Content.Shared.Afk;
+using Content.Shared.CCVar;
+using Content.Shared.Eui;
+using JetBrains.Annotations;
+using Robust.Client.Audio;
+using Robust.Client.Graphics;
+using Robust.Shared.Audio;
+using Robust.Shared.Configuration;
+using Robust.Shared.Player;
+using Robust.Shared.Random;
+
+namespace Content.Client.Afk;
+
+[UsedImplicitly]
+public sealed partial class AfkConfirmEui : BaseEui
+{
+ private const float MaxWindowOffset = 64f;
+
+ [Dependency] private IClyde _clyde = default!;
+ [Dependency] private IConfigurationManager _cfg = default!;
+ [Dependency] private IEntityManager _entManager = default!;
+ [Dependency] private IRobustRandom _random = default!;
+ private AudioSystem _audio;
+ private SoundSpecifier _confirmSound;
+ private EntityUid? _confirmSoundStream;
+
+ private readonly AfkConfirmWindow _window = new();
+
+ public AfkConfirmEui()
+ {
+ _audio = _entManager.System();
+ _confirmSound = new SoundPathSpecifier(_cfg.GetCVar(CCVars.AfkConfirmSound));
+ _cfg.OnValueChanged(CCVars.AfkConfirmSound, OnConfirmSoundChanged);
+
+ _window.OnConfirm += () =>
+ {
+ SendMessage(new AfkConfirmMessage());
+ _window.Close();
+ };
+
+ _window.OnClose += () => SendMessage(new CloseEuiMessage());
+ }
+
+ public override void Opened()
+ {
+ _clyde.RequestWindowAttention();
+ _confirmSoundStream = _audio.PlayGlobal(_confirmSound, Filter.Local(), false)?.Entity;
+
+ var screenSize = _clyde.ScreenSize;
+ var screenSizeVector = new Vector2(screenSize.X, screenSize.Y);
+ var offset = new Vector2(
+ RandomOffset(),
+ RandomOffset());
+ var relativePosition = new Vector2(0.5f) + offset / screenSizeVector;
+
+ _window.OpenCenteredAt(relativePosition);
+ }
+
+ private float RandomOffset()
+ {
+ return _random.NextFloat() * MaxWindowOffset * 2 - MaxWindowOffset;
+ }
+
+ private void OnConfirmSoundChanged(string path)
+ {
+ _confirmSound = new SoundPathSpecifier(path);
+ }
+
+ public override void Closed()
+ {
+ _cfg.UnsubValueChanged(CCVars.AfkConfirmSound, OnConfirmSoundChanged);
+ _confirmSoundStream = _audio.Stop(_confirmSoundStream);
+ _window.Close();
+ }
+
+ public override void HandleState(EuiStateBase state)
+ {
+ if (state is not AfkConfirmEuiState afkState)
+ return;
+
+ _window.SetTimeRemaining(afkState.TimeRemaining);
+ }
+}
diff --git a/Content.Client/Afk/AfkConfirmWindow.cs b/Content.Client/Afk/AfkConfirmWindow.cs
new file mode 100644
index 00000000000..c87be1bf22a
--- /dev/null
+++ b/Content.Client/Afk/AfkConfirmWindow.cs
@@ -0,0 +1,42 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Afk;
+
+[GenerateTypedNameReferences]
+public sealed partial class AfkConfirmWindow : DefaultWindow
+{
+ [Dependency] private IGameTiming _timing = default!;
+
+ private TimeSpan _deadline;
+
+ public event Action? OnConfirm;
+
+ public AfkConfirmWindow()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ ConfirmButton.OnPressed += _ => OnConfirm?.Invoke();
+ }
+
+ public void SetTimeRemaining(TimeSpan timeRemaining)
+ {
+ _deadline = _timing.RealTime + timeRemaining;
+ UpdateTimer();
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+ UpdateTimer();
+ }
+
+ private void UpdateTimer()
+ {
+ var remaining = Math.Max(0, (int)Math.Ceiling((_deadline - _timing.RealTime).TotalSeconds));
+ TimerLabel.Text = Loc.GetString("afk-confirm-window-seconds", ("seconds", remaining));
+ }
+}
diff --git a/Content.Client/Afk/AfkConfirmWindow.xaml b/Content.Client/Afk/AfkConfirmWindow.xaml
new file mode 100644
index 00000000000..1878f53049d
--- /dev/null
+++ b/Content.Client/Afk/AfkConfirmWindow.xaml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/Content.Client/Alerts/ClientAlertsSystem.cs b/Content.Client/Alerts/ClientAlertsSystem.cs
index 29cd5da1903..a42380180ee 100644
--- a/Content.Client/Alerts/ClientAlertsSystem.cs
+++ b/Content.Client/Alerts/ClientAlertsSystem.cs
@@ -15,7 +15,6 @@ public sealed partial class ClientAlertsSystem : AlertsSystem
public AlertOrderPrototype? AlertOrder { get; set; }
[Dependency] private IPlayerManager _playerManager = default!;
- [Dependency] private IPrototypeManager _prototypeManager = default!;
[Dependency] private IUserInterfaceManager _ui = default!;
public event EventHandler? ClearAlerts;
@@ -39,7 +38,7 @@ protected override void LoadPrototypes()
{
base.LoadPrototypes();
- AlertOrder = _prototypeManager.EnumeratePrototypes().FirstOrDefault();
+ AlertOrder = ProtoMan.EnumeratePrototypes().FirstOrDefault();
if (AlertOrder == null)
Log.Error("No alertOrder prototype found, alerts will be in random order");
}
diff --git a/Content.Client/Anomaly/Effects/ClientInnerBodySystem.cs b/Content.Client/Anomaly/Effects/ClientInnerBodySystem.cs
index 6d48254c190..173aca24bf6 100644
--- a/Content.Client/Anomaly/Effects/ClientInnerBodySystem.cs
+++ b/Content.Client/Anomaly/Effects/ClientInnerBodySystem.cs
@@ -1,3 +1,4 @@
+using Content.Client.DisplacementMap;
using Content.Shared.Anomaly.Components;
using Content.Shared.Anomaly.Effects;
using Content.Shared.Humanoid;
@@ -8,6 +9,9 @@ namespace Content.Client.Anomaly.Effects;
public sealed partial class ClientInnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
{
[Dependency] private SpriteSystem _sprite = default!;
+ [Dependency] private DisplacementMapSystem _displacement = default!;
+
+ [Dependency] private EntityQuery _visualsQuery = default!;
public override void Initialize()
{
@@ -37,6 +41,22 @@ private void OnAfterHandleState(Entity ent, ref After
_sprite.LayerSetVisible((ent.Owner, sprite), index, true);
sprite.LayerSetShader(index, "unshaded");
+
+ if (_visualsQuery.TryGetComponent(ent, out var visuals) && visuals.Displacement != null)
+ {
+ if (ProtoMan.Resolve(visuals.Displacement, out var displacement))
+ {
+ _displacement.TryAddDisplacement(displacement.Displacement,
+ (ent.Owner, sprite),
+ index,
+ ent.Comp.LayerMap,
+ out _);
+ }
+ else
+ {
+ _displacement.EnsureDisplacementIsNotOnSprite((ent.Owner, sprite), ent.Comp.LayerMap);
+ }
+ }
}
private void OnCompShutdown(Entity ent, ref ComponentShutdown args)
@@ -46,5 +66,7 @@ private void OnCompShutdown(Entity ent, ref Component
var index = _sprite.LayerMapGet((ent.Owner, sprite), ent.Comp.LayerMap);
_sprite.LayerSetVisible((ent.Owner, sprite), index, false);
+
+ _displacement.EnsureDisplacementIsNotOnSprite((ent.Owner, sprite), ent.Comp.LayerMap);
}
}
diff --git a/Content.Client/Atmos/AlignAtmosPipeLayers.cs b/Content.Client/Atmos/AlignAtmosPipeLayers.cs
index c8e69bc17f6..5df861e9d86 100644
--- a/Content.Client/Atmos/AlignAtmosPipeLayers.cs
+++ b/Content.Client/Atmos/AlignAtmosPipeLayers.cs
@@ -6,7 +6,6 @@
using Robust.Client.Graphics;
using Robust.Client.Placement;
using Robust.Client.Placement.Modes;
-using Robust.Client.Utility;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
@@ -27,7 +26,6 @@ public sealed partial class AlignAtmosPipeLayers : SnapgridCenter
{
[Dependency] private IEntityManager _entityManager = default!;
[Dependency] private IPrototypeManager _protoManager = default!;
- [Dependency] private IMapManager _mapManager = default!;
[Dependency] private IEyeManager _eyeManager = default!;
private readonly SharedMapSystem _mapSystem;
@@ -87,7 +85,7 @@ public override void AlignPlacementMode(ScreenCoordinates mouseScreen)
if (pManager.PlacementType != PlacementTypes.None)
return;
- MouseCoords = _unalignedMouseCoords.AlignWithClosestGridTile(SearchBoxSize, _entityManager, _mapManager);
+ MouseCoords = _unalignedMouseCoords.AlignWithClosestGridTile(SearchBoxSize, _entityManager);
var gridId = _transformSystem.GetGrid(MouseCoords);
@@ -169,7 +167,7 @@ private void UpdatePlacer(AtmosPipeLayer layer)
if (!_protoManager.TryIndex(pManager.CurrentPermission.EntityType, out var currentProto))
return;
- if (!currentProto.TryGetComponent(out var atmosPipeLayers, _entityManager.ComponentFactory))
+ if (!currentProto.TryComp(out var atmosPipeLayers, _entityManager.ComponentFactory))
return;
if (!_pipeLayersSystem.TryGetAlternativePrototype(atmosPipeLayers, layer, out var newProtoId))
@@ -181,7 +179,7 @@ private void UpdatePlacer(AtmosPipeLayer layer)
pManager.CurrentPermission.EntityType = newProtoId;
// Update the appearance of the ghost sprite
- if (newProto.TryGetComponent(out var sprite, _entityManager.ComponentFactory))
+ if (newProto.TryComp(out var sprite, _entityManager.ComponentFactory))
{
var textures = new List();
diff --git a/Content.Client/Atmos/Components/FireVisualsComponent.cs b/Content.Client/Atmos/Components/FireVisualsComponent.cs
index 02278e94791..ac7a78edcf5 100644
--- a/Content.Client/Atmos/Components/FireVisualsComponent.cs
+++ b/Content.Client/Atmos/Components/FireVisualsComponent.cs
@@ -1,3 +1,6 @@
+using Content.Shared.DisplacementMap;
+using Robust.Shared.Prototypes;
+
namespace Content.Client.Atmos.Components;
///
@@ -39,4 +42,10 @@ public sealed partial class FireVisualsComponent : Component
/// the burning entity as entities don't support having multiple point-lights.
///
public EntityUid? LightEntity;
+
+ ///
+ /// Tracks the currently applied displacement, so that only new changes update the layer.
+ ///
+ [DataField]
+ public ProtoId? CurrentDisplacement;
}
diff --git a/Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs b/Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs
index 24f3ffaa261..0619e26c4b2 100644
--- a/Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs
+++ b/Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs
@@ -1,3 +1,4 @@
+using System.Numerics.Tensors;
using System.Runtime.CompilerServices;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Reactions;
@@ -26,15 +27,15 @@ public override ReactionResult React(GasMixture mixture, IGasMixtureHolder? hold
public override bool IsMixtureFuel(GasMixture mixture, float epsilon = Atmospherics.Epsilon)
{
var tmp = new float[Atmospherics.AdjustedNumberOfGases];
- NumericsHelpers.Multiply(mixture.Moles, GasFuelMask, tmp);
- return NumericsHelpers.HorizontalAdd(tmp) > epsilon;
+ TensorPrimitives.Multiply(mixture.Moles, GasFuelMask, tmp);
+ return TensorPrimitives.Sum(tmp) > epsilon;
}
public override bool IsMixtureOxidizer(GasMixture mixture, float epsilon = Atmospherics.Epsilon)
{
var tmp = new float[Atmospherics.AdjustedNumberOfGases];
- NumericsHelpers.Multiply(mixture.Moles, GasOxidizerMask, tmp);
- return NumericsHelpers.HorizontalAdd(tmp) > epsilon;
+ TensorPrimitives.Multiply(mixture.Moles, GasOxidizerMask, tmp);
+ return TensorPrimitives.Sum(tmp) > epsilon;
}
public override float GetMass(GasMixture mix)
@@ -45,17 +46,17 @@ public override float GetMass(GasMixture mix)
public override float GetMass(float[] moles)
{
var tmp = new float[moles.Length];
- NumericsHelpers.Multiply(moles, GasMolarMasses, tmp);
+ TensorPrimitives.Multiply(moles, GasMolarMasses, tmp);
// Conversion of grams to kilograms.
- return NumericsHelpers.HorizontalAdd(tmp) * Atmospherics.gToKg;
+ return TensorPrimitives.Sum(tmp) * Atmospherics.gToKg;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override float GetHeatCapacityCalculation(float[] moles, bool space)
{
// Little hack to make space gas mixtures have heat capacity, therefore allowing them to cool down rooms.
- if (space && MathHelper.CloseTo(NumericsHelpers.HorizontalAdd(moles), 0f))
+ if (space && MathHelper.CloseTo(TensorPrimitives.Sum(moles), 0f))
{
return Atmospherics.SpaceHeatCapacity;
}
@@ -65,9 +66,9 @@ protected override float GetHeatCapacityCalculation(float[] moles, bool space)
// though this isnt the hottest code path so it should be fine
// the gc can eat a little as a treat
var tmp = new float[moles.Length];
- NumericsHelpers.Multiply(moles, GasMolarHeatCapacities, tmp);
+ TensorPrimitives.Multiply(moles, GasMolarHeatCapacities, tmp);
// Adjust heat capacity by speedup, because this is primarily what
// determines how quickly gases heat up/cool.
- return MathF.Max(NumericsHelpers.HorizontalAdd(tmp), Atmospherics.MinimumHeatCapacity);
+ return MathF.Max(TensorPrimitives.Sum(tmp), Atmospherics.MinimumHeatCapacity);
}
}
diff --git a/Content.Client/Atmos/EntitySystems/FireVisualizerSystem.cs b/Content.Client/Atmos/EntitySystems/FireVisualizerSystem.cs
index ec4500935cc..f308e0da03c 100644
--- a/Content.Client/Atmos/EntitySystems/FireVisualizerSystem.cs
+++ b/Content.Client/Atmos/EntitySystems/FireVisualizerSystem.cs
@@ -1,5 +1,7 @@
using Content.Client.Atmos.Components;
+using Content.Client.DisplacementMap;
using Content.Shared.Atmos;
+using Content.Shared.DisplacementMap;
using Robust.Client.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Utility;
@@ -12,6 +14,7 @@ namespace Content.Client.Atmos.EntitySystems;
public sealed partial class FireVisualizerSystem : VisualizerSystem
{
[Dependency] private PointLightSystem _lights = default!;
+ [Dependency] private DisplacementMapSystem _displacement = default!;
public override void Initialize()
{
@@ -65,6 +68,7 @@ private void UpdateAppearance(EntityUid uid, FireVisualsComponent component, Spr
AppearanceSystem.TryGetData(uid, FireVisuals.OnFire, out var onFire, appearance);
AppearanceSystem.TryGetData(uid, FireVisuals.FireStacks, out var fireStacks, appearance);
+ AppearanceSystem.TryGetData(uid, FireVisuals.FireDisplacement, out var fireDisplacement, appearance);
SpriteSystem.LayerSetVisible((uid, sprite), index, onFire);
if (!onFire)
@@ -83,6 +87,16 @@ private void UpdateAppearance(EntityUid uid, FireVisualsComponent component, Spr
else
SpriteSystem.LayerSetRsiState((uid, sprite), index, component.NormalState);
+ if (component.CurrentDisplacement != fireDisplacement)
+ {
+ if (fireDisplacement != null && ProtoMan.Resolve(fireDisplacement, out var displacementProto))
+ _displacement.TryAddDisplacement(displacementProto.Displacement, (uid, sprite), index, FireVisualLayers.Fire, out _);
+ else
+ _displacement.EnsureDisplacementIsNotOnSprite((uid, sprite), FireVisualLayers.Fire);
+
+ component.CurrentDisplacement = fireDisplacement;
+ }
+
component.LightEntity ??= Spawn(null, new EntityCoordinates(uid, default));
var light = EnsureComp(component.LightEntity.Value);
diff --git a/Content.Client/Atmos/EntitySystems/GasCanisterAppearanceSystem.cs b/Content.Client/Atmos/EntitySystems/GasCanisterAppearanceSystem.cs
index d54f4d20a64..1d0bf5d3153 100644
--- a/Content.Client/Atmos/EntitySystems/GasCanisterAppearanceSystem.cs
+++ b/Content.Client/Atmos/EntitySystems/GasCanisterAppearanceSystem.cs
@@ -1,7 +1,6 @@
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.SprayPainter.Prototypes;
using Robust.Client.GameObjects;
-using Robust.Shared.Prototypes;
namespace Content.Client.Atmos.EntitySystems;
@@ -10,14 +9,12 @@ namespace Content.Client.Atmos.EntitySystems;
///
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(true);
+ var stack = new Stack<(TreeItem Item, int Depth)>();
+
+ foreach (var label in labels)
+ {
+ if (HeadingDepth(label) is not { } depth)
+ continue;
+
+ while (stack.TryPeek(out var previous) && previous.Depth >= depth)
+ {
+ stack.Pop();
+ }
+
+ var item = stack.TryPeek(out var parent)
+ ? TableOfContents.AddItem(parent.Item)
+ : TableOfContents.AddItem(firstEntry);
+ item.Label.Text = label.Text;
+ item.Metadata = label;
+
+ stack.Push((item, depth));
+ }
+
+ // Expand all entries, but collapse the first one
+ TableOfContents.SetAllExpanded(true);
+ firstEntry.SetExpanded(false);
}
- public void UpdateGuides(
+ ///
+ /// Updates the guides used in the window.
+ /// Returns whether the guides changed.
+ ///
+ public bool UpdateGuides(
Dictionary, GuideEntry> entries,
List>? rootEntries = null,
ProtoId? forceRoot = null,
ProtoId? selected = null)
{
- _entries = entries;
- RepopulateTree(rootEntries, forceRoot);
- ClearSelectedGuide();
-
- Split.State = SplitContainer.SplitState.Auto;
- if (entries.Count == 1)
- {
- TreeBox.Visible = false;
- Split.ResizeMode = SplitContainer.SplitResizeMode.NotResizable;
- selected = entries.Keys.First();
- }
- else
+ // check if old and new entries are equal
+ var sameAsLastUpdate = entries.Count != _entries.Count || !entries.All(_entries.Contains);
+ if (sameAsLastUpdate)
{
- TreeBox.Visible = true;
- Split.ResizeMode = SplitContainer.SplitResizeMode.RespectChildrenMinSize;
+ _entries = entries;
+ RepopulateTree(rootEntries, forceRoot);
+ Split.State = SplitContainer.SplitState.Auto;
+ if (entries.Count == 1)
+ {
+ TreeBox.Visible = false;
+ Split.ResizeMode = SplitContainer.SplitResizeMode.NotResizable;
+ selected = entries.Keys.First();
+ }
+ else
+ {
+ TreeBox.Visible = true;
+ Split.ResizeMode = SplitContainer.SplitResizeMode.RespectChildrenMinSize;
+ }
}
- if (selected != null)
+ if (selected == null)
+ ClearSelectedGuide();
+ else
{
var item = Tree.Items.FirstOrDefault(x => x.Metadata is GuideEntry entry && entry.Id == selected);
Tree.SetSelectedIndex(item?.Index);
}
+
+ return sameAsLastUpdate;
}
private IEnumerable GetSortedEntries(List>? rootEntries)
{
if (rootEntries == null)
{
- HashSet> entries = new(_entries.Keys);
+ HashSet> entries = [.. _entries.Keys];
foreach (var entry in _entries.Values)
{
entries.ExceptWith(entry.Children);
}
- rootEntries = entries.ToList();
+
+ rootEntries = [.. entries];
}
// Only roots need to be sorted.
@@ -203,7 +285,7 @@ private void RepopulateTree(List>? roots = null,
{
Tree.Clear();
- HashSet> addedEntries = new();
+ HashSet> addedEntries = [];
var parent = forcedRoot == null ? null : AddEntry(forcedRoot.Value, null, addedEntries);
foreach (var entry in GetSortedEntries(roots))
@@ -248,8 +330,6 @@ private void RepopulateTree(List>? roots = null,
private void HandleFilter()
{
- var emptySearch = SearchBar.Text.Trim().Length == 0;
-
if (Tree.SelectedItem != null && Tree.SelectedItem.Metadata is GuideEntry entry && entry.FilterEnabled)
{
var foundElements = EntryContainer.GetSearchableControls();
@@ -263,8 +343,8 @@ private void HandleFilter()
private static (List, List) GetLinkableControlsAndLinks(Control parent)
{
- List linkableList = new();
- List linkList = new();
+ List linkableList = [];
+ List linkList = [];
foreach (var child in parent.Children)
{
diff --git a/Content.Client/Hands/Systems/HandsSystem.cs b/Content.Client/Hands/Systems/HandsSystem.cs
index dc33db2fc26..9461af62d0a 100644
--- a/Content.Client/Hands/Systems/HandsSystem.cs
+++ b/Content.Client/Hands/Systems/HandsSystem.cs
@@ -15,6 +15,7 @@
using Robust.Client.UserInterface;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
+using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Utility;
@@ -92,9 +93,11 @@ public void ReloadHandButtons()
public override void DoDrop(Entity ent,
string handId,
bool doDropInteraction = true,
- bool log = true)
+ bool log = true,
+ EntityCoordinates? targetDropLocation = null
+ )
{
- base.DoDrop(ent, handId, doDropInteraction, log);
+ base.DoDrop(ent, handId, doDropInteraction, log, targetDropLocation);
if (TryGetHeldItem(ent, handId, out var held) && TryComp(held, out SpriteComponent? sprite))
sprite.RenderOrder = EntityManager.CurrentTick.Value;
diff --git a/Content.Client/Holopad/HolopadSystem.cs b/Content.Client/Holopad/HolopadSystem.cs
index 81a04cf36bd..2f22c66d4d7 100644
--- a/Content.Client/Holopad/HolopadSystem.cs
+++ b/Content.Client/Holopad/HolopadSystem.cs
@@ -2,7 +2,6 @@
using Content.Shared.Holopad;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
-using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using System.Linq;
using System.Numerics;
@@ -12,7 +11,6 @@ namespace Content.Client.Holopad;
public sealed partial class HolopadSystem : SharedHolopadSystem
{
- [Dependency] private IPrototypeManager _prototypeManager = default!;
[Dependency] private IGameTiming _timing = default!;
[Dependency] private SpriteSystem _sprite = default!;
@@ -120,7 +118,7 @@ private void UpdateHologramShader(EntityUid uid, SpriteComponent sprite, Holopad
// Find the texture height of the largest layer
float texHeight = sprite.AllLayers.Max(x => x.PixelSize.Y);
- var instance = _prototypeManager.Index(holopadHologram.ShaderName).InstanceUnique();
+ var instance = ProtoMan.Index(holopadHologram.ShaderName).InstanceUnique();
instance.SetParameter("color1", new Vector3(holopadHologram.Color1.R, holopadHologram.Color1.G, holopadHologram.Color1.B));
instance.SetParameter("color2", new Vector3(holopadHologram.Color2.R, holopadHologram.Color2.G, holopadHologram.Color2.B));
instance.SetParameter("alpha", holopadHologram.Alpha);
diff --git a/Content.Client/Holopad/HolopadWindow.xaml.cs b/Content.Client/Holopad/HolopadWindow.xaml.cs
index 6a19e46d039..c1c39171010 100644
--- a/Content.Client/Holopad/HolopadWindow.xaml.cs
+++ b/Content.Client/Holopad/HolopadWindow.xaml.cs
@@ -1,4 +1,5 @@
using Content.Client.Popups;
+using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Shared.Access.Systems;
using Content.Shared.Holopad;
@@ -61,8 +62,8 @@ public HolopadWindow()
// XML formatting
AnswerCallButton.AddStyleClass("ButtonAccept");
- EndCallButton.AddStyleClass("Caution");
- StartBroadcastButton.AddStyleClass("Caution");
+ EndCallButton.AddStyleClass(StyleClass.Negative);
+ StartBroadcastButton.AddStyleClass(StyleClass.Negative);
HolopadContactListPanel.PanelOverride = new StyleBoxFlat
{
diff --git a/Content.Client/Instruments/InstrumentComponent.cs b/Content.Client/Instruments/InstrumentComponent.cs
index 068dc26ac5f..a621392f0b1 100644
--- a/Content.Client/Instruments/InstrumentComponent.cs
+++ b/Content.Client/Instruments/InstrumentComponent.cs
@@ -39,7 +39,7 @@ public sealed partial class InstrumentComponent : SharedInstrumentComponent
///
/// Whether this instrument is handheld or not.
///
- [DataField("handheld")]
+ [DataField]
public bool Handheld { get; set; } // TODO: Replace this by simply checking if the entity has an ItemComponent.
///
diff --git a/Content.Client/Interaction/DragDropSystem.cs b/Content.Client/Interaction/DragDropSystem.cs
index 467c138b2a6..c38bf138ada 100644
--- a/Content.Client/Interaction/DragDropSystem.cs
+++ b/Content.Client/Interaction/DragDropSystem.cs
@@ -37,7 +37,6 @@ public sealed partial class DragDropSystem : SharedDragDropSystem
[Dependency] private IInputManager _inputManager = default!;
[Dependency] private IEyeManager _eyeManager = default!;
[Dependency] private IPlayerManager _playerManager = default!;
- [Dependency] private IPrototypeManager _prototypeManager = default!;
[Dependency] private IConfigurationManager _cfgMan = default!;
[Dependency] private InteractionOutlineSystem _outline = default!;
[Dependency] private SharedInteractionSystem _interactionSystem = default!;
@@ -112,8 +111,8 @@ public override void Initialize()
Subs.CVar(_cfgMan, CCVars.DragDropDeadZone, SetDeadZone, true);
- _dropTargetInRangeShader = _prototypeManager.Index(ShaderDropTargetInRange).Instance();
- _dropTargetOutOfRangeShader = _prototypeManager.Index(ShaderDropTargetOutOfRange).Instance();
+ _dropTargetInRangeShader = ProtoMan.Index(ShaderDropTargetInRange).Instance();
+ _dropTargetOutOfRangeShader = ProtoMan.Index(ShaderDropTargetOutOfRange).Instance();
// needs to fire on mouseup and mousedown so we can detect a drag / drop
CommandBinds.Builder
.BindBefore(EngineKeyFunctions.Use, new PointerInputCmdHandler(OnUse, false, true), new[] { typeof(SharedInteractionSystem) })
diff --git a/Content.Client/Inventory/ClientInventorySystem.cs b/Content.Client/Inventory/ClientInventorySystem.cs
index 00b9548aa9b..ac606a39b2c 100644
--- a/Content.Client/Inventory/ClientInventorySystem.cs
+++ b/Content.Client/Inventory/ClientInventorySystem.cs
@@ -1,6 +1,7 @@
using System.Linq;
using Content.Client.Clothing;
using Content.Client.Examine;
+using Content.Client.Pointing;
using Content.Client.Verbs.UI;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
@@ -23,6 +24,7 @@ public sealed partial class ClientInventorySystem : InventorySystem
[Dependency] private IUserInterfaceManager _ui = default!;
[Dependency] private ClientClothingSystem _clothingVisualsSystem = default!;
[Dependency] private ExamineSystem _examine = default!;
+ [Dependency] private PointingSystem _pointing = default!;
public Action? EntitySlotUpdate = null;
public Action? OnSlotAdded = null;
@@ -237,6 +239,19 @@ public void UIInventoryAltActivateItem(string slot, EntityUid uid)
RaisePredictiveEvent(new InteractInventorySlotEvent(GetNetEntity(item.Value), altInteract: true));
}
+ ///
+ /// Points at an item in the inventory
+ ///
+ /// The slot to point at
+ /// The inventory entity containing the slot
+ public void UIInventoryPointAt(string slot, EntityUid uid)
+ {
+ if (!TryGetSlotEntity(uid, slot, out var item))
+ return;
+
+ _pointing.TryPointAtEntity(GetNetEntity(item.Value));
+ }
+
protected override void UpdateInventoryTemplate(Entity ent)
{
base.UpdateInventoryTemplate(ent);
diff --git a/Content.Client/IoC/ClientContentIoC.cs b/Content.Client/IoC/ClientContentIoC.cs
index efaf88b0522..b777245c9fd 100644
--- a/Content.Client/IoC/ClientContentIoC.cs
+++ b/Content.Client/IoC/ClientContentIoC.cs
@@ -1,4 +1,5 @@
using Content.Client.Administration.Managers;
+using Content.Client.Audio.Midi;
using Content.Client.Changelog;
using Content.Client.Chat.Managers;
using Content.Client.Clickable;
@@ -66,6 +67,7 @@ public static void Register(IDependencyCollection collection)
collection.Register();
collection.Register();
collection.Register();
+ collection.Register();
}
}
}
diff --git a/Content.Client/Kitchen/EntitySystems/ReagentGrinderSystem.cs b/Content.Client/Kitchen/EntitySystems/ReagentGrinderSystem.cs
index 3dafecbc896..7a438d69dec 100644
--- a/Content.Client/Kitchen/EntitySystems/ReagentGrinderSystem.cs
+++ b/Content.Client/Kitchen/EntitySystems/ReagentGrinderSystem.cs
@@ -4,6 +4,7 @@
namespace Content.Client.Kitchen.EntitySystems;
+///
public sealed partial class ReagentGrinderSystem : SharedReagentGrinderSystem
{
[Dependency] private SharedUserInterfaceSystem _userInterface = default!;
diff --git a/Content.Client/Kitchen/UI/GrinderMenu.xaml.cs b/Content.Client/Kitchen/UI/GrinderMenu.xaml.cs
index 2770e163d38..3638794a09d 100644
--- a/Content.Client/Kitchen/UI/GrinderMenu.xaml.cs
+++ b/Content.Client/Kitchen/UI/GrinderMenu.xaml.cs
@@ -58,12 +58,20 @@ private void OnChamberBoxContentsItemSelected(ItemList.ItemListSelectedEventArgs
OnEjectChamber?.Invoke(_chamberVisualContents[args.ItemIndex]);
}
+ ///
+ /// Set the reagent grinder entity associated with this menu.
+ ///
+ /// The reagent grinder.
public void SetEntity(EntityUid owner)
{
_owner = owner;
UpdateUi();
}
+ ///
+ /// Update the UI state of this reagent grinder, including its contents,
+ /// current grinding/juicing status, and button toggle states.
+ ///
public void UpdateUi()
{
if (!_entityManager.TryGetComponent(_owner, out var grinderComp))
diff --git a/Content.Client/Kitchen/UI/LabelledContentBox.xaml.cs b/Content.Client/Kitchen/UI/LabelledContentBox.xaml.cs
index bcad17ae977..ff8ce8a74e6 100644
--- a/Content.Client/Kitchen/UI/LabelledContentBox.xaml.cs
+++ b/Content.Client/Kitchen/UI/LabelledContentBox.xaml.cs
@@ -1,12 +1,22 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
-namespace Content.Client.Kitchen.UI
+namespace Content.Client.Kitchen.UI;
+
+///
+/// A box representing the input or output contents of a reagent grinder,
+/// with a button to eject the contents on the input / output side.
+///
+[GenerateTypedNameReferences]
+public sealed partial class LabelledContentBox : BoxContainer
{
- [GenerateTypedNameReferences]
- public sealed partial class LabelledContentBox : BoxContainer
- {
- public string? LabelText { get => Label.Text; set => Label.Text = value; }
- public string? ButtonText { get => EjectButton.Text; set => EjectButton.Text = value; }
- }
+ ///
+ /// The name of the container, e.g. "Chamber" or "Beaker".
+ ///
+ public string? LabelText { get => Label.Text; set => Label.Text = value; }
+
+ ///
+ /// The text for the eject button.
+ ///
+ public string? ButtonText { get => EjectButton.Text; set => EjectButton.Text = value; }
}
diff --git a/Content.Client/Kitchen/UI/ReagentGrinderBoundUserInterface.cs b/Content.Client/Kitchen/UI/ReagentGrinderBoundUserInterface.cs
index f7a31e1e583..6055d74e2c1 100644
--- a/Content.Client/Kitchen/UI/ReagentGrinderBoundUserInterface.cs
+++ b/Content.Client/Kitchen/UI/ReagentGrinderBoundUserInterface.cs
@@ -31,31 +31,50 @@ public override void Update()
_menu?.UpdateUi();
}
+ ///
+ /// Send a message to toggle automatic grinding mode for this grinder.
+ ///
public void ToggleAutoMode()
{
SendPredictedMessage(new ReagentGrinderToggleAutoModeMessage());
}
+ ///
+ /// Send a message to begin grinding the grinder's contents.
+ ///
public void StartGrinding()
{
SendPredictedMessage(new ReagentGrinderStartMessage(GrinderProgram.Grind));
}
+ ///
+ /// Send a message to begin juicing the grinder's contents.
+ ///
public void StartJuicing()
{
SendPredictedMessage(new ReagentGrinderStartMessage(GrinderProgram.Juice));
}
+ ///
+ /// Send a message to request all entities be ejected from the grinder.
+ ///
public void EjectAll()
{
SendPredictedMessage(new ReagentGrinderEjectChamberAllMessage());
}
+ ///
+ /// Send a message to remove the reagent container from the grinder.
+ ///
public void EjectBeaker()
{
SendPredictedMessage(new ItemSlotButtonPressedEvent(ReagentGrinderComponent.BeakerSlotId));
}
+ ///
+ /// Send a message to request a specific entity be ejected from the grinder.
+ ///
+ /// The entity to eject.
public void EjectChamberContent(EntityUid uid)
{
SendPredictedMessage(new ReagentGrinderEjectChamberContentMessage(EntMan.GetNetEntity(uid)));
diff --git a/Content.Client/Kudzu/KudzuVisualizerComponent.cs b/Content.Client/Kudzu/KudzuVisualizerComponent.cs
index 868f2841f8f..5f48a163208 100644
--- a/Content.Client/Kudzu/KudzuVisualizerComponent.cs
+++ b/Content.Client/Kudzu/KudzuVisualizerComponent.cs
@@ -1,10 +1,18 @@
-namespace Content.Client.Kudzu
-{
- [RegisterComponent]
- public sealed partial class KudzuVisualsComponent : Component
- {
- [DataField("layer")]
- public int Layer { get; private set; } = 0;
- }
+using Content.Shared.Spreader;
+
+namespace Content.Client.Kudzu;
+///
+/// Entities that have a visual state corresponding to current growth level
+/// and random variation, for kudzu tiles.
+///
+///
+[RegisterComponent]
+public sealed partial class KudzuVisualsComponent : Component
+{
+ ///
+ /// The index of the sprite layer that is reflecting the kudzu's growth state.
+ ///
+ [DataField]
+ public int Layer { get; private set; } = 0;
}
diff --git a/Content.Client/Lathe/UI/LatheMenu.xaml b/Content.Client/Lathe/UI/LatheMenu.xaml
index 84f1d7835a9..f8e58403e50 100644
--- a/Content.Client/Lathe/UI/LatheMenu.xaml
+++ b/Content.Client/Lathe/UI/LatheMenu.xaml
@@ -120,7 +120,7 @@
HorizontalAlignment="Right"
ToolTip="{Loc 'lathe-menu-delete-fabricating-tooltip'}">
- Caution
+ negativeOpenLeft
diff --git a/Content.Client/Lathe/UI/QueuedRecipeControl.xaml b/Content.Client/Lathe/UI/QueuedRecipeControl.xaml
index b1d4b496a14..09b1883e1b4 100644
--- a/Content.Client/Lathe/UI/QueuedRecipeControl.xaml
+++ b/Content.Client/Lathe/UI/QueuedRecipeControl.xaml
@@ -27,7 +27,7 @@
Text="✖"
ToolTip="{Loc 'lathe-menu-delete-item-tooltip'}">
- Caution
+ negativeOpenLeft
diff --git a/Content.Client/Light/AmbientOcclusionOverlay.cs b/Content.Client/Light/AmbientOcclusionOverlay.cs
index ef510a2d26d..68b8bc72a3b 100644
--- a/Content.Client/Light/AmbientOcclusionOverlay.cs
+++ b/Content.Client/Light/AmbientOcclusionOverlay.cs
@@ -5,7 +5,7 @@
using Robust.Client.Graphics;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
-using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -23,9 +23,10 @@ public sealed partial class AmbientOcclusionOverlay : Overlay
[Dependency] private IClyde _clyde = default!;
[Dependency] private IConfigurationManager _cfgManager = default!;
[Dependency] private IEntityManager _entManager = default!;
- [Dependency] private IMapManager _mapManager = default!;
[Dependency] private IPrototypeManager _proto = default!;
+ private List> _cachedGrids = new();
+
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowEntities;
private readonly OverlayResourceCache _resources = new ();
@@ -115,7 +116,9 @@ protected override void Draw(in OverlayDrawArgs args)
// Don't want lighting affecting it.
worldHandle.UseShader(_proto.Index(UnshadedShader).Instance());
- foreach (var grid in _mapManager.FindGridsIntersecting(mapId, worldBounds))
+ _cachedGrids.Clear();
+ maps.FindGridsIntersecting(mapId, worldBounds, ref _cachedGrids);
+ foreach (var grid in _cachedGrids)
{
var transform = xformSystem.GetWorldMatrix(grid.Owner);
var worldToTextureMatrix = Matrix3x2.Multiply(transform, invMatrix);
@@ -126,7 +129,7 @@ protected override void Draw(in OverlayDrawArgs args)
if (turfSystem.IsSpace(tileRef))
continue;
- var bounds = lookups.GetLocalBounds(tileRef, grid.TileSize);
+ var bounds = lookups.GetLocalBounds(tileRef, grid.Comp.TileSize);
worldHandle.DrawRect(bounds, Color.White);
}
}
diff --git a/Content.Client/Light/RoofOverlay.cs b/Content.Client/Light/RoofOverlay.cs
index 7923601fa92..934c2bc001c 100644
--- a/Content.Client/Light/RoofOverlay.cs
+++ b/Content.Client/Light/RoofOverlay.cs
@@ -14,7 +14,6 @@ namespace Content.Client.Light;
public sealed partial class RoofOverlay : Overlay
{
private readonly IEntityManager _entManager;
- [Dependency] private IMapManager _mapManager = default!;
[Dependency] private IOverlayManager _overlay = default!;
private readonly EntityLookupSystem _lookup;
@@ -56,7 +55,7 @@ protected override void Draw(in OverlayDrawArgs args)
var target = lightRes.EnlargedLightTarget;
_grids.Clear();
- _mapManager.FindGridsIntersecting(args.MapId, bounds, ref _grids, approx: true, includeMap: true);
+ _mapSystem.FindGridsIntersecting(args.MapId, bounds, ref _grids, approx: true, includeMap: true);
var lightScale = viewport.LightRenderTarget.Size / (Vector2) viewport.Size;
var scale = viewport.RenderScale / (Vector2.One / lightScale);
diff --git a/Content.Client/Light/SunShadowOverlay.cs b/Content.Client/Light/SunShadowOverlay.cs
index abdd69d6673..4f16f63624b 100644
--- a/Content.Client/Light/SunShadowOverlay.cs
+++ b/Content.Client/Light/SunShadowOverlay.cs
@@ -18,9 +18,9 @@ public sealed partial class SunShadowOverlay : Overlay
[Dependency] private IClyde _clyde = default!;
[Dependency] private IEntityManager _entManager = default!;
- [Dependency] private IMapManager _mapManager = default!;
[Dependency] private IPrototypeManager _protoManager = default!;
private readonly EntityLookupSystem _lookup;
+ private readonly SharedMapSystem _mapSys;
private readonly SharedTransformSystem _xformSys;
private readonly HashSet> _shadows = new();
@@ -31,6 +31,7 @@ public SunShadowOverlay()
{
IoCManager.InjectDependencies(this);
_xformSys = _entManager.System();
+ _mapSys = _entManager.System();
_lookup = _entManager.System();
ZIndex = AfterLightTargetOverlay.ContentZIndex + 1;
}
@@ -46,7 +47,7 @@ protected override void Draw(in OverlayDrawArgs args)
return;
_grids.Clear();
- _mapManager.FindGridsIntersecting(args.MapId,
+ _mapSys.FindGridsIntersecting(args.MapId,
args.WorldBounds.Enlarged(SunShadowComponent.MaxLength),
ref _grids);
diff --git a/Content.Client/Light/TileEmissionOverlay.cs b/Content.Client/Light/TileEmissionOverlay.cs
index 448a5b36390..743dd9d513c 100644
--- a/Content.Client/Light/TileEmissionOverlay.cs
+++ b/Content.Client/Light/TileEmissionOverlay.cs
@@ -11,7 +11,6 @@ public sealed partial class TileEmissionOverlay : Overlay
{
public override OverlaySpace Space => OverlaySpace.BeforeLighting;
- [Dependency] private IMapManager _mapManager = default!;
[Dependency] private IOverlayManager _overlay = default!;
private SharedMapSystem _mapSystem;
@@ -50,7 +49,7 @@ protected override void Draw(in OverlayDrawArgs args)
var target = lightoverlay.GetCachedForViewport(args.Viewport).EnlargedLightTarget;
var viewport = args.Viewport;
_grids.Clear();
- _mapManager.FindGridsIntersecting(mapId, bounds, ref _grids, approx: true);
+ _mapSystem.FindGridsIntersecting(mapId, bounds, ref _grids, approx: true);
if (_grids.Count == 0)
return;
diff --git a/Content.Client/Lobby/UI/CharacterSetupGui.xaml.cs b/Content.Client/Lobby/UI/CharacterSetupGui.xaml.cs
index 922605eba51..4bcb89e69fd 100644
--- a/Content.Client/Lobby/UI/CharacterSetupGui.xaml.cs
+++ b/Content.Client/Lobby/UI/CharacterSetupGui.xaml.cs
@@ -54,7 +54,7 @@ public CharacterSetupGui(HumanoidProfileEditor profileEditor)
_createNewCharacterButton.OnPressed += args =>
{
- _preferencesManager.CreateCharacter(HumanoidCharacterProfile.Random());
+ _preferencesManager.CreateCharacter(HumanoidCharacterProfile.Random().WithJobFromCvar(_cfg));
ReloadCharacterPickers();
args.Event.Handle();
};
@@ -65,6 +65,7 @@ public CharacterSetupGui(HumanoidProfileEditor profileEditor)
StatsButton.OnPressed += _ => new PlaytimeStatsWindow().OpenCentered();
_cfg.OnValueChanged(CCVars.SeeOwnNotes, p => AdminRemarksButton.Visible = p, true);
+ _cfg.OnValueChanged(CCVars.GameMaxCharacterSlots, _ => ReloadCharacterPickers());
}
///
@@ -83,9 +84,11 @@ public void ReloadCharacterPickers()
return;
}
+ var maxCharactersSlots = _cfg.GetCVar(CCVars.GameMaxCharacterSlots);
+
_createNewCharacterButton.ToolTip =
Loc.GetString("character-setup-gui-create-new-character-button-tooltip",
- ("maxCharacters", _preferencesManager.Settings!.MaxCharacterSlots));
+ ("maxCharacters", maxCharactersSlots));
var selectedSlot = _preferencesManager.Preferences?.SelectedCharacterIndex;
@@ -98,6 +101,14 @@ public void ReloadCharacterPickers()
character,
slot == selectedSlot);
+ if (slot >= maxCharactersSlots)
+ {
+ characterPickerButton.SetOnlyStyleClass(ContainerButton.StylePseudoClassDisabled);
+ characterPickerButton.ToolTip =
+ Loc.GetString("character-setup-gui-create-new-character-button-tooltip",
+ ("maxCharacters", maxCharactersSlots));
+ }
+
Characters.AddChild(characterPickerButton);
characterPickerButton.OnPressed += args =>
@@ -111,7 +122,7 @@ public void ReloadCharacterPickers()
};
}
- _createNewCharacterButton.Disabled = numberOfFullSlots >= _preferencesManager.Settings.MaxCharacterSlots;
+ _createNewCharacterButton.Disabled = numberOfFullSlots >= maxCharactersSlots;
Characters.AddChild(_createNewCharacterButton);
}
}
diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.Appearance.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.Appearance.cs
index ddc9752b1ed..26720918f7e 100644
--- a/Content.Client/Lobby/UI/HumanoidProfileEditor.Appearance.cs
+++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.Appearance.cs
@@ -1,9 +1,11 @@
using System.Linq;
using Content.Client.UserInterface.Systems.Guidebook;
+using Content.Shared.Chat.Prototypes;
using Content.Shared.Guidebook;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
+using Content.Shared.Speech.Components;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
@@ -16,6 +18,7 @@ public sealed partial class HumanoidProfileEditor
private ColorSelectorSliders _rgbSkinColorSelector;
private List _species = new();
+ private List _voices = new();
private static readonly ProtoId DefaultSpeciesGuidebook = "Species";
public void UpdateSpeciesGuidebookIcon()
@@ -97,6 +100,34 @@ private void UpdateEyePickers()
EyeColorPicker.SetData(Profile.Appearance.EyeColor);
}
+ private void UpdateVoiceControls()
+ {
+ if (Profile == null)
+ return;
+
+ VoiceButton.Clear();
+ _voices.Clear();
+
+ var speciesPrototype = _prototypeManager.Index(Profile.Species);
+ var availableVoices = speciesPrototype.Voices;
+
+ _voices.AddRange(availableVoices.Select(protoId => _prototypeManager.Index(protoId)));
+
+ if (_voices.All(proto => Profile?.Voice != proto.ID))
+ SetVoice(speciesPrototype.DefaultSoundsBySex[(int)Profile.Sex]);
+
+ for (var i = 0; i < availableVoices.Count; i++)
+ {
+ var name = Loc.GetString(_voices[i].VoiceSelectorName);
+ VoiceButton.AddItem(name, i);
+
+ if (Profile?.Voice.Equals(_voices[i].ID) == true)
+ {
+ VoiceButton.SelectId(i);
+ }
+ }
+ }
+
private void UpdateSkinColor()
{
if (Profile == null)
@@ -188,6 +219,7 @@ private void SetSpecies(string newSpecies)
// In case there's species restrictions for loadouts
RefreshLoadouts();
UpdateSexControls(); // update sex for new species
+ UpdateVoiceControls();
UpdateSpeciesGuidebookIcon();
ReloadPreview();
}
@@ -215,11 +247,22 @@ private void SetSex(Sex newSex)
break;
}
+ // this does the same as above but for voice
+ if (_prototypeManager.TryIndex(Profile?.Species, out var prototype))
+ SetVoice(prototype.DefaultSoundsBySex[(int)newSex]);
+
UpdateGenderControls();
+ UpdateVoiceControls();
_markingsModel.SetOrganSexes(newSex);
ReloadPreview();
}
+ private void SetVoice(ProtoId newVoice)
+ {
+ Profile = Profile?.WithVoice(newVoice);
+ SetDirty();
+ }
+
private void SetGender(Gender newGender)
{
Profile = Profile?.WithGender(newGender);
diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.BasicInfo.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.BasicInfo.cs
index b5ce8d0d6d7..f9cc43065aa 100644
--- a/Content.Client/Lobby/UI/HumanoidProfileEditor.BasicInfo.cs
+++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.BasicInfo.cs
@@ -21,18 +21,15 @@ private void UpdateNameEdit()
NameEdit.Text = Profile?.Name ?? "";
}
- private void RandomizeEverything()
+ ///
+ /// Randomize values selectively while respecting locked values.
+ ///
+ private void RandomizeProfile()
{
- Profile = HumanoidCharacterProfile.Random();
+ Profile = Profile == null
+ ? HumanoidCharacterProfile.Random()
+ : HumanoidCharacterProfile.Random(RandomizeLockButton.RandomizeCfg, Profile!);
SetProfile(Profile, CharacterSlot);
SetDirty();
}
-
- private void RandomizeName()
- {
- if (Profile == null) return;
- var name = HumanoidCharacterProfile.GetName(Profile.Species, Profile.Gender);
- SetName(name);
- UpdateNameEdit();
- }
}
diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
index 84c5b75d1cb..3b3ecacb489 100644
--- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
+++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
@@ -13,21 +13,34 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -74,6 +87,12 @@
+
+
+
+
+
+
diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
index 3b444bf4162..0f6fe340488 100644
--- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
+++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
@@ -144,8 +144,7 @@ public HumanoidProfileEditor(
NameEdit.OnTextChanged += args => { SetName(args.Text); };
NameEdit.IsValid = args => args.Length <= _maxNameLength;
- NameRandomize.OnPressed += args => RandomizeName();
- RandomizeEverythingButton.OnPressed += args => { RandomizeEverything(); };
+ RandomizeUnlockedButton.OnPressed += args => { RandomizeProfile(); };
WarningLabel.SetMarkup($"[color=red]{Loc.GetString("humanoid-profile-editor-naming-rules-warning")}[/color]");
#endregion Name
@@ -164,6 +163,16 @@ public HumanoidProfileEditor(
#endregion Sex
+ #region Voice
+
+ VoiceButton.OnItemSelected += args =>
+ {
+ VoiceButton.SelectId(args.Id);
+ SetVoice(_voices[args.Id]);
+ };
+
+ #endregion
+
#region Age
AgeEdit.OnTextChanged += args =>
@@ -369,6 +378,7 @@ public void SetProfile(HumanoidCharacterProfile? profile, int? slot)
UpdateNameEdit();
UpdateFlavorTextEdit();
UpdateSexControls();
+ UpdateVoiceControls();
UpdateGenderControls();
UpdateSkinColor();
UpdateSpawnPriorityControls();
diff --git a/Content.Client/Lobby/UI/RandomizeLockButton.xaml b/Content.Client/Lobby/UI/RandomizeLockButton.xaml
new file mode 100644
index 00000000000..c28d25ca61f
--- /dev/null
+++ b/Content.Client/Lobby/UI/RandomizeLockButton.xaml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/Content.Client/Lobby/UI/RandomizeLockButton.xaml.cs b/Content.Client/Lobby/UI/RandomizeLockButton.xaml.cs
new file mode 100644
index 00000000000..6d79ff2b21e
--- /dev/null
+++ b/Content.Client/Lobby/UI/RandomizeLockButton.xaml.cs
@@ -0,0 +1,91 @@
+using System.Numerics;
+using Content.Client.Resources;
+using Content.Client.Stylesheets;
+using Content.Shared.Preferences;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using static Content.Client.Stylesheets.StylesheetHelpers;
+
+namespace Content.Client.Lobby.UI;
+
+///
+/// A toggle button with a lock icon meant to lock values for randomizing in the character editor screen.
+///
+[GenerateTypedNameReferences]
+public sealed partial class RandomizeLockButton : BoxContainer
+{
+ ///
+ /// Which value this button locks.
+ ///
+ public HumanoidCharacterProfile.RandomizeCfg For { get; set; }
+
+ ///
+ /// Name of the button
+ ///
+ public string? LabelText { get => LockName.Text; set => LockName.Text = value; }
+
+ ///
+ /// Tracks which values are allowed to be randomized.
+ ///
+ public static HumanoidCharacterProfile.RandomizeCfg RandomizeCfg = HumanoidCharacterProfile.RandomizeConfigAll;
+
+ public RandomizeLockButton()
+ {
+ RobustXamlLoader.Load(this);
+
+ // Ensure we reset this value to default every time the button is created
+ // Otherwise toggling, disconnecting and reconnecting will leave the cfg in an inconsistent state
+ RandomizeCfg = HumanoidCharacterProfile.RandomizeConfigAll;
+
+ LockButton.OnToggled += args => HandleToggle(args.Pressed);
+ }
+
+ private void HandleToggle(bool toggle)
+ {
+ if (For == default)
+ return;
+ if (toggle)
+ {
+ // if pressed, disable specific randomize by setting its bit to 0
+ RandomizeCfg &= ~For;
+ }
+ else
+ {
+ // if not pressed, enable specific randomize by setting its bit to 1
+ RandomizeCfg |= For;
+ }
+ }
+}
+
+[CommonSheetlet]
+public sealed class RandomizeLockButtonStyles : Sheetlet
+{
+ public override StyleRule[] GetRules(PalettedStylesheet sheet, object config)
+ {
+ var textureLocked = ResCache.GetTexture("/Textures/Interface/VerbIcons/lock.svg.192dpi.png");
+ var textureUnlocked = ResCache.GetTexture("/Textures/Interface/VerbIcons/unlock.svg.192dpi.png");
+
+ return
+ [
+ E()
+ .Identifier("RandomizerLockButton")
+ .Modulate(Color.FromHsl(new Vector4(0f, 0f, .65f, 1f)))
+ .Prop(TextureButton.StylePropertyTexture, textureUnlocked)
+ .Margin(new Thickness(0f, 0f, 3f, 0f)),
+
+ E()
+ .Identifier("RandomizerLockButton")
+ .PseudoHovered()
+ .Modulate(Color.WhiteSmoke)
+ .Prop(TextureButton.StylePropertyTexture, textureLocked),
+
+ E()
+ .Identifier("RandomizerLockButton")
+ .PseudoPressed()
+ .Modulate(Color.WhiteSmoke)
+ .Prop(TextureButton.StylePropertyTexture, textureLocked),
+ ];
+ }
+}
diff --git a/Content.Client/Machines/EntitySystems/MultipartMachineSystem.cs b/Content.Client/Machines/EntitySystems/MultipartMachineSystem.cs
index 8c3ed88ee44..2d5a69fbb64 100644
--- a/Content.Client/Machines/EntitySystems/MultipartMachineSystem.cs
+++ b/Content.Client/Machines/EntitySystems/MultipartMachineSystem.cs
@@ -21,9 +21,8 @@ public sealed partial class MultipartMachineSystem : SharedMultipartMachineSyste
private readonly Color _partiallyTransparent = new Color(255, 255, 255, 180);
[Dependency] private SpriteSystem _sprite = default!;
- [Dependency] private IPrototypeManager _prototype = default!;
[Dependency] private MetaDataSystem _metaData = default!;
- [Dependency] private ISerializationManager _serialization= default!;
+ [Dependency] private ISerializationManager _serialization = default!;
public override void Initialize()
{
@@ -70,7 +69,7 @@ private void OnMachineExamined(Entity ent, ref Client
if (part.GhostProto == null)
continue;
- var entProto = _prototype.Index(part.GhostProto.Value);
+ var entProto = ProtoMan.Index(part.GhostProto.Value);
if (!entProto.Components.TryGetComponent("Sprite", out var s) || s is not SpriteComponent protoSprite)
return;
diff --git a/Content.Client/MapText/MapTextSystem.cs b/Content.Client/MapText/MapTextSystem.cs
index c94969ad1b1..909b72b46d7 100644
--- a/Content.Client/MapText/MapTextSystem.cs
+++ b/Content.Client/MapText/MapTextSystem.cs
@@ -5,7 +5,6 @@
using Robust.Client.UserInterface.RichText;
using Robust.Shared.Configuration;
using Robust.Shared.GameStates;
-using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.MapText;
@@ -17,7 +16,6 @@ public sealed partial class MapTextSystem : SharedMapTextSystem
[Dependency] private IUserInterfaceManager _uiManager = default!;
[Dependency] private SharedTransformSystem _transform = default!;
[Dependency] private IResourceCache _resourceCache = default!;
- [Dependency] private IPrototypeManager _prototypeManager = default!;
[Dependency] private IOverlayManager _overlayManager = default!;
private MapTextOverlay _overlay = default!;
@@ -28,18 +26,18 @@ public override void Initialize()
SubscribeLocalEvent(OnComponentStartup);
SubscribeLocalEvent(HandleCompState);
- _overlay = new MapTextOverlay(_configManager, EntityManager, _uiManager, _transform, _resourceCache, _prototypeManager);
+ _overlay = new MapTextOverlay(_configManager, EntityManager, _uiManager, _transform, _resourceCache, ProtoMan);
_overlayManager.AddOverlay(_overlay);
// TODO move font prototype to robust.shared, then use ProtoId
- DebugTools.Assert(_prototypeManager.HasIndex(SharedMapTextComponent.DefaultFont));
+ DebugTools.Assert(ProtoMan.HasIndex(SharedMapTextComponent.DefaultFont));
}
private void OnComponentStartup(Entity ent, ref ComponentStartup args)
{
CacheText(ent.Comp);
// TODO move font prototype to robust.shared, then use ProtoId
- DebugTools.Assert(_prototypeManager.HasIndex(ent.Comp.FontId));
+ DebugTools.Assert(ProtoMan.HasIndex(ent.Comp.FontId));
}
private void HandleCompState(Entity ent, ref ComponentHandleState args)
@@ -65,12 +63,12 @@ private void CacheText(MapTextComponent component)
? Loc.GetString(component.LocText)
: component.Text;
- if (!_prototypeManager.TryIndex(component.FontId, out var fontPrototype))
+ if (!ProtoMan.TryIndex(component.FontId, out var fontPrototype))
{
component.CachedText = Loc.GetString("map-text-font-error");
component.Color = Color.Red;
- if(_prototypeManager.TryIndex(SharedMapTextComponent.DefaultFont, out var @default))
+ if (ProtoMan.TryIndex(SharedMapTextComponent.DefaultFont, out var @default))
component.CachedFont = new VectorFont(_resourceCache.GetResource(@default.Path), 14);
return;
}
diff --git a/Content.Client/Mapping/MappingScreen.xaml.cs b/Content.Client/Mapping/MappingScreen.xaml.cs
index 6ef9ce578bd..1608a1f54e3 100644
--- a/Content.Client/Mapping/MappingScreen.xaml.cs
+++ b/Content.Client/Mapping/MappingScreen.xaml.cs
@@ -6,6 +6,7 @@
using Content.Client.UserInterface.Systems.Chat.Widgets;
using Content.Shared.Decals;
using Robust.Client.AutoGenerated;
+using Robust.Client.Placement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
@@ -18,6 +19,7 @@ namespace Content.Client.Mapping;
[GenerateTypedNameReferences]
public sealed partial class MappingScreen : InGameScreen
{
+ [Dependency] private IPlacementManager _placement = default!;
[Dependency] private IPrototypeManager _prototype = default!;
public DecalPlacementSystem DecalSystem = default!;
@@ -89,9 +91,10 @@ public MappingScreen()
UpdateDecal();
};
- for (var i = 0; i < EntitySpawnWindow.InitOpts.Length; i++)
+ var modes = _placement.AllModeNames;
+ for (var i = 0; i < modes.Length; i++)
{
- EntityPlacementMode.AddItem(EntitySpawnWindow.InitOpts[i], i);
+ EntityPlacementMode.AddItem(modes[i], i);
}
Pick.Texture.TexturePath = "/Textures/Interface/eyedropper.svg.png";
diff --git a/Content.Client/Mapping/MappingState.cs b/Content.Client/Mapping/MappingState.cs
index bddfe27d807..e6633e0a452 100644
--- a/Content.Client/Mapping/MappingState.cs
+++ b/Content.Client/Mapping/MappingState.cs
@@ -45,7 +45,6 @@ public sealed partial class MappingState : GameplayStateBase
[Dependency] private IEntityNetworkManager _entityNetwork = default!;
[Dependency] private IInputManager _input = default!;
[Dependency] private ILogManager _log = default!;
- [Dependency] private IMapManager _mapMan = default!;
[Dependency] private MappingManager _mapping = default!;
[Dependency] private IOverlayManager _overlays = default!;
[Dependency] private IPlacementManager _placement = default!;
@@ -56,6 +55,7 @@ public sealed partial class MappingState : GameplayStateBase
private EntityMenuUIController _entityMenuController = default!;
private DecalPlacementSystem _decal = default!;
+ private MapSystem _maps = default!;
private SpriteSystem _sprite = default!;
private TransformSystem _transform = default!;
private VerbSystem _verbs = default!;
@@ -207,6 +207,7 @@ private void EnsureSetup()
_entityMenuController = UserInterfaceManager.GetUIController();
_decal = _entityManager.System();
+ _maps = _entityManager.System();
_sprite = _entityManager.System();
_transform = _entityManager.System();
_verbs = _entityManager.System();
@@ -559,7 +560,7 @@ private void OnSelected(MappingSpawnButton button, IPrototype? prototype)
var placement = new PlacementInformation
{
- PlacementOption = placementId > 0 ? EntitySpawnWindow.InitOpts[placementId] : entity.PlacementMode,
+ PlacementOption = placementId > 0 ? _placement.AllModeNames[placementId] : entity.PlacementMode,
EntityType = entity.ID,
IsTile = false
};
@@ -658,7 +659,7 @@ private void OnEntityPlacementSelected(ItemSelectedEventArgs args)
{
var placement = new PlacementInformation
{
- PlacementOption = EntitySpawnWindow.InitOpts[args.Id],
+ PlacementOption = _placement.AllModeNames[args.Id],
EntityType = _placement.CurrentPermission!.EntityType,
TileType = _placement.CurrentPermission.TileType,
Range = 2,
@@ -795,7 +796,7 @@ private bool HandlePick(ICommonSession? session, EntityCoordinates coords, Entit
{
var mapPos = _transform.ToMapCoordinates(coords);
- if (_mapMan.TryFindGridAt(mapPos, out var gridUid, out var grid) &&
+ if (_maps.TryFindGridAt(mapPos, out var gridUid, out var grid) &&
_entityManager.System().TryGetTileRef(gridUid, grid, coords, out var tileRef) &&
_allPrototypesDict.TryGetValue(_entityManager.System().GetContentTileDefinition(tileRef), out button))
{
diff --git a/Content.Client/Maps/GridDraggingSystem.cs b/Content.Client/Maps/GridDraggingSystem.cs
index f78f96c5471..530aebdd739 100644
--- a/Content.Client/Maps/GridDraggingSystem.cs
+++ b/Content.Client/Maps/GridDraggingSystem.cs
@@ -16,7 +16,7 @@ public sealed partial class GridDraggingSystem : SharedGridDraggingSystem
[Dependency] private IEyeManager _eyeManager = default!;
[Dependency] private IGameTiming _gameTiming = default!;
[Dependency] private IInputManager _inputManager = default!;
- [Dependency] private IMapManager _mapManager = default!;
+ [Dependency] private SharedMapSystem _mapSystem = default!;
[Dependency] private InputSystem _inputSystem = default!;
[Dependency] private SharedTransformSystem _transformSystem = default!;
@@ -99,7 +99,7 @@ public override void Update(float frameTime)
if (_dragging == null)
{
- if (!_mapManager.TryFindGridAt(mousePos, out var gridUid, out var grid))
+ if (!_mapSystem.TryFindGridAt(mousePos, out var gridUid, out var grid))
return;
StartDragging(gridUid, Vector2.Transform(mousePos.Position, _transformSystem.GetInvWorldMatrix(gridUid)));
diff --git a/Content.Client/Movement/Systems/FloorOcclusionSystem.cs b/Content.Client/Movement/Systems/FloorOcclusionSystem.cs
index e160a06fb1f..ce0705e3476 100644
--- a/Content.Client/Movement/Systems/FloorOcclusionSystem.cs
+++ b/Content.Client/Movement/Systems/FloorOcclusionSystem.cs
@@ -10,8 +10,6 @@ public sealed partial class FloorOcclusionSystem : SharedFloorOcclusionSystem
{
private static readonly ProtoId HorizontalCut = "HorizontalCut";
- [Dependency] private IPrototypeManager _proto = default!;
-
[Dependency] private EntityQuery _spriteQuery = default!;
public override void Initialize()
@@ -48,7 +46,7 @@ private void SetShader(Entity sprite, bool enabled)
if (!_spriteQuery.Resolve(sprite.Owner, ref sprite.Comp, false))
return;
- var shader = _proto.Index(HorizontalCut).Instance();
+ var shader = ProtoMan.Index(HorizontalCut).Instance();
if (sprite.Comp.PostShader is not null && sprite.Comp.PostShader != shader)
return;
diff --git a/Content.Client/NPC/PathfindingSystem.cs b/Content.Client/NPC/PathfindingSystem.cs
index 3cca80198d9..26bb12ee2d0 100644
--- a/Content.Client/NPC/PathfindingSystem.cs
+++ b/Content.Client/NPC/PathfindingSystem.cs
@@ -19,7 +19,6 @@ public sealed partial class PathfindingSystem : SharedPathfindingSystem
[Dependency] private IEyeManager _eyeManager = default!;
[Dependency] private IGameTiming _timing = default!;
[Dependency] private IInputManager _inputManager = default!;
- [Dependency] private IMapManager _mapManager = default!;
[Dependency] private IOverlayManager _overlayManager = default!;
[Dependency] private IResourceCache _cache = default!;
[Dependency] private NPCSteeringSystem _steering = default!;
@@ -39,7 +38,7 @@ public PathfindingDebugMode Modes
}
else if (!_overlayManager.HasOverlay())
{
- _overlayManager.AddOverlay(new PathfindingOverlay(EntityManager, _eyeManager, _inputManager, _mapManager, _cache, this, _mapSystem, _transformSystem));
+ _overlayManager.AddOverlay(new PathfindingOverlay(EntityManager, _eyeManager, _inputManager, _cache, this, _mapSystem, _transformSystem));
}
if ((value & PathfindingDebugMode.Steering) != 0x0)
@@ -137,7 +136,6 @@ public sealed class PathfindingOverlay : Overlay
private readonly IEntityManager _entManager;
private readonly IEyeManager _eyeManager;
private readonly IInputManager _inputManager;
- private readonly IMapManager _mapManager;
private readonly PathfindingSystem _system;
private readonly MapSystem _mapSystem;
private readonly SharedTransformSystem _transformSystem;
@@ -151,7 +149,6 @@ public PathfindingOverlay(
IEntityManager entManager,
IEyeManager eyeManager,
IInputManager inputManager,
- IMapManager mapManager,
IResourceCache cache,
PathfindingSystem system,
MapSystem mapSystem,
@@ -160,7 +157,6 @@ public PathfindingOverlay(
_entManager = entManager;
_eyeManager = eyeManager;
_inputManager = inputManager;
- _mapManager = mapManager;
_system = system;
_mapSystem = mapSystem;
_transformSystem = transformSystem;
@@ -193,7 +189,7 @@ private void DrawScreen(OverlayDrawArgs args, DrawingHandleScreen screenHandle)
var found = false;
_grids.Clear();
- _mapManager.FindGridsIntersecting(mouseWorldPos.MapId, aabb, ref _grids);
+ _mapSystem.FindGridsIntersecting(mouseWorldPos.MapId, aabb, ref _grids);
foreach (var grid in _grids)
{
@@ -269,7 +265,7 @@ private void DrawScreen(OverlayDrawArgs args, DrawingHandleScreen screenHandle)
if ((_system.Modes & PathfindingDebugMode.Poly) != 0x0 &&
mouseWorldPos.MapId == args.MapId)
{
- if (!_mapManager.TryFindGridAt(mouseWorldPos, out var gridUid, out var grid) || !xformQuery.TryGetComponent(gridUid, out var gridXform))
+ if (!_mapSystem.TryFindGridAt(mouseWorldPos, out var gridUid, out var grid) || !xformQuery.TryGetComponent(gridUid, out var gridXform))
return;
if (!_system.Polys.TryGetValue(_entManager.GetNetEntity(gridUid), out var data))
@@ -346,7 +342,7 @@ private void DrawWorld(OverlayDrawArgs args, DrawingHandleWorld worldHandle)
mouseWorldPos.MapId == args.MapId)
{
_grids.Clear();
- _mapManager.FindGridsIntersecting(mouseWorldPos.MapId, aabb, ref _grids);
+ _mapSystem.FindGridsIntersecting(mouseWorldPos.MapId, aabb, ref _grids);
foreach (var grid in _grids)
{
@@ -408,7 +404,7 @@ private void DrawWorld(OverlayDrawArgs args, DrawingHandleWorld worldHandle)
mouseWorldPos.MapId == args.MapId)
{
_grids.Clear();
- _mapManager.FindGridsIntersecting(args.MapId, aabb, ref _grids);
+ _mapSystem.FindGridsIntersecting(args.MapId, aabb, ref _grids);
foreach (var grid in _grids)
{
@@ -447,7 +443,7 @@ private void DrawWorld(OverlayDrawArgs args, DrawingHandleWorld worldHandle)
mouseWorldPos.MapId == args.MapId)
{
_grids.Clear();
- _mapManager.FindGridsIntersecting(args.MapId, aabb, ref _grids);
+ _mapSystem.FindGridsIntersecting(args.MapId, aabb, ref _grids);
foreach (var grid in _grids)
{
@@ -506,7 +502,7 @@ private void DrawWorld(OverlayDrawArgs args, DrawingHandleWorld worldHandle)
if ((_system.Modes & PathfindingDebugMode.Chunks) != 0x0)
{
_grids.Clear();
- _mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds, ref _grids);
+ _mapSystem.FindGridsIntersecting(args.MapId, args.WorldBounds, ref _grids);
foreach (var grid in _grids)
{
diff --git a/Content.Client/NightVision/NightVisionSystem.cs b/Content.Client/NightVision/NightVisionSystem.cs
new file mode 100644
index 00000000000..8934032c723
--- /dev/null
+++ b/Content.Client/NightVision/NightVisionSystem.cs
@@ -0,0 +1,116 @@
+using Content.Client.Overlays;
+using Content.Shared.GameTicking;
+using Content.Shared.NightVision;
+using Content.Shared.Overlays;
+using Robust.Client.Graphics;
+using Robust.Client.Player;
+using Robust.Shared.Player;
+
+namespace Content.Client.NightVision;
+
+///
+public sealed partial class NightVisionSystem : SharedNightVisionSystem
+{
+ [Dependency] private IOverlayManager _overlayMan = default!;
+ [Dependency] private IPlayerManager _player = default!;
+
+ private NightVisionOverlay _overlay = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _overlay = new NightVisionOverlay();
+ }
+
+ [SubscribeLocalEvent]
+ private void OnPlayerAttached(LocalPlayerAttachedEvent args)
+ {
+ RefreshOverlay(args.Entity);
+ }
+
+ [SubscribeLocalEvent]
+ private void OnPlayerDetached(LocalPlayerDetachedEvent args)
+ {
+ Deactivate(_player.LocalEntity);
+ }
+
+ [SubscribeLocalEvent]
+ private void OnHandleState(Entity ent, ref AfterAutoHandleStateEvent args)
+ {
+ RefreshOverlay(ent);
+ }
+
+ [SubscribeNetworkEvent]
+ private void OnRoundRestart(RoundRestartCleanupEvent args)
+ {
+ var localPlayer = _player.LocalSession?.AttachedEntity;
+ if (localPlayer != null)
+ Deactivate(localPlayer.Value);
+ }
+
+ private void Update(EntityUid entity, List> entities)
+ {
+ if (entity != _player.LocalSession?.AttachedEntity)
+ return;
+
+ // Find the component with the lowest noise.
+ NightVisionComponent? nvision = null;
+ var bestNoise = float.MaxValue;
+ foreach (var ent in entities)
+ {
+ if (!ent.Comp.Enabled)
+ continue;
+
+ if (ent.Comp.RelayOverlay == (ent.Owner == entity))
+ continue;
+
+ if (ent.Comp.Prioritized)
+ {
+ nvision = ent.Comp;
+ break;
+ }
+
+ var noise = ent.Comp.NoiseAmount * ent.Comp.NoiseMultiplier;
+ if (noise < bestNoise)
+ {
+ nvision = ent.Comp;
+ bestNoise = noise;
+ }
+ }
+
+ // There is no active night vision components, so we disable the overlay.
+ if (nvision == null)
+ {
+ Deactivate(entity);
+ return;
+ }
+
+ _overlay.SetParameters(nvision.OverlayColor, nvision.LightingColor, nvision.NoiseAmount, nvision.NoiseMultiplier);
+
+ if (!_overlayMan.HasOverlay())
+ _overlayMan.AddOverlay(_overlay);
+ }
+
+ private void Deactivate(EntityUid? ent)
+ {
+ if (ent != _player.LocalSession?.AttachedEntity)
+ return;
+
+ _overlayMan.RemoveOverlay(_overlay);
+ }
+
+ protected override void RefreshOverlay(EntityUid target)
+ {
+ if (target != _player.LocalSession?.AttachedEntity)
+ return;
+
+ var ev = new RefreshNightVisionEvent();
+ RaiseLocalEvent(target, ref ev);
+
+ if (ev.Entities.Count > 0)
+ Update(target, ev.Entities);
+ else
+ Deactivate(target);
+ }
+}
diff --git a/Content.Client/NodeContainer/NodeGroupSystem.cs b/Content.Client/NodeContainer/NodeGroupSystem.cs
index cbba584a591..200781a274f 100644
--- a/Content.Client/NodeContainer/NodeGroupSystem.cs
+++ b/Content.Client/NodeContainer/NodeGroupSystem.cs
@@ -5,9 +5,6 @@
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.ResourceManagement;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Map;
namespace Content.Client.NodeContainer
{
@@ -16,7 +13,6 @@ public sealed partial class NodeGroupSystem : EntitySystem
{
[Dependency] private IOverlayManager _overlayManager = default!;
[Dependency] private EntityLookupSystem _entityLookup = default!;
- [Dependency] private IMapManager _mapManager = default!;
[Dependency] private IInputManager _inputManager = default!;
[Dependency] private IResourceCache _resourceCache = default!;
@@ -88,7 +84,6 @@ public void SetVisEnabled(bool enabled)
var overlay = new NodeVisualizationOverlay(
this,
_entityLookup,
- _mapManager,
_inputManager,
_resourceCache,
EntityManager);
diff --git a/Content.Client/NodeContainer/NodeVisualizationOverlay.cs b/Content.Client/NodeContainer/NodeVisualizationOverlay.cs
index 9466ca032b1..75567f1f6cb 100644
--- a/Content.Client/NodeContainer/NodeVisualizationOverlay.cs
+++ b/Content.Client/NodeContainer/NodeVisualizationOverlay.cs
@@ -17,7 +17,6 @@ public sealed class NodeVisualizationOverlay : Overlay
{
private readonly NodeGroupSystem _system;
private readonly EntityLookupSystem _lookup;
- private readonly IMapManager _mapManager;
private readonly IInputManager _inputManager;
private readonly IEntityManager _entityManager;
private readonly SharedTransformSystem _transformSystem;
@@ -38,14 +37,12 @@ public sealed class NodeVisualizationOverlay : Overlay
public NodeVisualizationOverlay(
NodeGroupSystem system,
EntityLookupSystem lookup,
- IMapManager mapManager,
IInputManager inputManager,
IResourceCache cache,
IEntityManager entityManager)
{
_system = system;
_lookup = lookup;
- _mapManager = mapManager;
_inputManager = inputManager;
_entityManager = entityManager;
_transformSystem = _entityManager.System();
@@ -119,7 +116,7 @@ private void DrawWorld(in OverlayDrawArgs overlayDrawArgs)
var xformQuery = _entityManager.GetEntityQuery();
_grids.Clear();
- _mapManager.FindGridsIntersecting(map, worldAABB, ref _grids);
+ _mapSystem.FindGridsIntersecting(map, worldAABB, ref _grids);
foreach (var grid in _grids)
{
diff --git a/Content.Client/Nutrition/EntitySystems/CreamPieSystem.cs b/Content.Client/Nutrition/EntitySystems/CreamPieSystem.cs
index 5dd5192537d..2d03abc8439 100644
--- a/Content.Client/Nutrition/EntitySystems/CreamPieSystem.cs
+++ b/Content.Client/Nutrition/EntitySystems/CreamPieSystem.cs
@@ -1,6 +1,8 @@
+using Content.Client.DisplacementMap;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Robust.Client.GameObjects;
+using Robust.Shared.Prototypes;
namespace Content.Client.Nutrition.EntitySystems;
@@ -8,6 +10,7 @@ public sealed partial class CreamPieSystem : SharedCreamPieSystem
{
[Dependency] private SpriteSystem _sprite = default!;
[Dependency] private AppearanceSystem _appearance = default!;
+ [Dependency] private DisplacementMapSystem _displacement = default!;
public override void Initialize()
{
@@ -27,6 +30,7 @@ private void OnComponentInit(Entity ent, ref ComponentInit a
private void OnComponentShutdown(Entity ent, ref ComponentShutdown args)
{
_sprite.RemoveLayer(ent.Owner, CreamPiedVisualLayer.Key);
+ _displacement.EnsureDisplacementIsNotOnSprite((ent, Comp(ent)), CreamPiedVisualLayer.Key);
}
private void OnAppearanceChange(Entity ent, ref AppearanceChangeEvent args)
@@ -54,6 +58,7 @@ private void UpdateAppearance(Entity(ent.Owner, CreamPiedVisuals.Creamed, out var isCreamPied, appearance);
_sprite.LayerSetSprite((ent.Owner, sprite), index, creamPied.Sprite);
_sprite.LayerSetVisible((ent.Owner, sprite), index, isCreamPied);
+
+ if (ProtoMan.Resolve(ent.Comp1.Displacement, out var displacementProto))
+ {
+ _displacement.TryAddDisplacement(displacementProto.Displacement, (ent.Owner, sprite), index, CreamPiedVisualLayer.Key, out _);
+ }
}
}
diff --git a/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml b/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml
index 5a389ca4914..55c47957a06 100644
--- a/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml
+++ b/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml
@@ -4,10 +4,39 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
@@ -19,7 +48,7 @@
-
diff --git a/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml.cs b/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml.cs
index f33da4c5014..4f4c0a8e823 100644
--- a/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml.cs
+++ b/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml.cs
@@ -14,8 +14,17 @@ public AccessibilityTab()
Control.AddOptionCheckBox(CCVars.ChatEnableColorName, EnableColorNameCheckBox);
Control.AddOptionCheckBox(CCVars.AccessibilityColorblindFriendly, ColorblindFriendlyCheckBox);
+
Control.AddOptionCheckBox(CCVars.ReducedMotion, ReducedMotionCheckBox);
Control.AddOptionCheckBox(CCVars.DisableAiStatic, DisableAiStaticCheckBox);
+ Control.AddOptionCheckBox(CCVars.DisableBlurryVision, DisableBlurryVisionCheckBox);
+ Control.AddOptionCheckBox(CCVars.DisableDrunkOverlay, DisableDrunkOverlayCheckBox);
+ Control.AddOptionCheckBox(CCVars.DisableFlashEffect, DisableFlashEffectCheckBox);
+ Control.AddOptionCheckBox(CCVars.DisableHeatDistortion, DisableHeatDistortionCheckBox);
+ Control.AddOptionCheckBox(CCVars.DisableNightVisionNoise, DisableNightVisionNoiseCheckBox);
+ Control.AddOptionCheckBox(CCVars.DisableRainbowOverlay, DisableRainbowOverlayCheckBox);
+ Control.AddOptionCheckBox(CCVars.DisableSinguloWarp, DisableSinguloWarpCheckBox);
+
Control.AddOptionPercentSlider(CCVars.ScreenShakeIntensity, ScreenShakeIntensitySlider);
Control.AddOptionPercentSlider(CCVars.ChatWindowOpacity, ChatWindowOpacitySlider);
Control.AddOptionPercentSlider(CCVars.SpeechBubbleTextOpacity, SpeechBubbleTextOpacitySlider);
diff --git a/Content.Client/Options/UI/Tabs/MiscTab.xaml b/Content.Client/Options/UI/Tabs/MiscTab.xaml
index c1733e209db..42142efc944 100644
--- a/Content.Client/Options/UI/Tabs/MiscTab.xaml
+++ b/Content.Client/Options/UI/Tabs/MiscTab.xaml
@@ -18,6 +18,7 @@
+
diff --git a/Content.Client/Options/UI/Tabs/MiscTab.xaml.cs b/Content.Client/Options/UI/Tabs/MiscTab.xaml.cs
index 6b1a559c259..a8e7256b433 100644
--- a/Content.Client/Options/UI/Tabs/MiscTab.xaml.cs
+++ b/Content.Client/Options/UI/Tabs/MiscTab.xaml.cs
@@ -52,6 +52,7 @@ public MiscTab()
Control.AddOptionCheckBox(CCVars.ChatEnableFancyBubbles, FancySpeechBubblesCheckBox);
Control.AddOptionCheckBox(CCVars.ChatFancyNameBackground, FancyNameBackgroundsCheckBox);
Control.AddOptionCheckBox(CCVars.StaticStorageUI, StaticStorageUI);
+ Control.AddOptionCheckBox(CCVars.InterfaceChatFollowButton, ChatFollowButton);
Control.Initialize();
}
diff --git a/Content.Client/Outline/TargetOutlineSystem.cs b/Content.Client/Outline/TargetOutlineSystem.cs
index 4fc5af91530..350c3e032be 100644
--- a/Content.Client/Outline/TargetOutlineSystem.cs
+++ b/Content.Client/Outline/TargetOutlineSystem.cs
@@ -23,7 +23,6 @@ public sealed partial class TargetOutlineSystem : EntitySystem
[Dependency] private EntityLookupSystem _lookup = default!;
[Dependency] private IInputManager _inputManager = default!;
[Dependency] private IPlayerManager _playerManager = default!;
- [Dependency] private IPrototypeManager _prototypeManager = default!;
[Dependency] private SharedInteractionSystem _interactionSystem = default!;
[Dependency] private EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private SharedTransformSystem _transformSystem = default!;
@@ -83,8 +82,8 @@ public override void Initialize()
{
base.Initialize();
- _shaderTargetValid = _prototypeManager.Index(ShaderTargetValid).InstanceUnique();
- _shaderTargetInvalid = _prototypeManager.Index(ShaderTargetInvalid).InstanceUnique();
+ _shaderTargetValid = ProtoMan.Index(ShaderTargetValid).InstanceUnique();
+ _shaderTargetInvalid = ProtoMan.Index(ShaderTargetInvalid).InstanceUnique();
}
public void Disable()
diff --git a/Content.Client/Overlays/NightVisionOverlay.cs b/Content.Client/Overlays/NightVisionOverlay.cs
new file mode 100644
index 00000000000..613c4500faf
--- /dev/null
+++ b/Content.Client/Overlays/NightVisionOverlay.cs
@@ -0,0 +1,72 @@
+using Content.Shared.CCVar;
+using Robust.Client.Graphics;
+using Robust.Shared.Configuration;
+using Robust.Shared.Enums;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Overlays;
+
+///
+/// Fullscreen overlay that applies the night-vision shader to the rendered screen.
+///
+public sealed partial class NightVisionOverlay : Overlay
+{
+ [Dependency] private IPrototypeManager _prototypeManager = default!;
+ [Dependency] private IConfigurationManager _configManager = default!;
+
+ private static readonly ProtoId Shader = "NightVision";
+ private readonly ShaderInstance _nightVisionShader;
+
+ public Color OverlayColor { get; private set; } = Color.White;
+ public Color LightingColor { get; private set; } = Color.White;
+ public float NoiseAmount { get; private set; }
+ public float NoiseMultiplier { get; private set; }
+
+ public bool DisableNoise = false;
+
+ public override OverlaySpace Space => OverlaySpace.BeforeLighting | OverlaySpace.WorldSpace;
+ public override bool RequestScreenTexture => true;
+
+ public NightVisionOverlay()
+ {
+ IoCManager.InjectDependencies(this);
+ _nightVisionShader = _prototypeManager.Index(Shader).InstanceUnique();
+ _configManager.OnValueChanged(CCVars.DisableNightVisionNoise, OnNightVisionNoiseChanged, invokeImmediately: true);
+ }
+
+ private void OnNightVisionNoiseChanged(bool toggle)
+ {
+ DisableNoise = toggle;
+ }
+
+ public void SetParameters(Color overlayColor, Color lightingColor, float noiseAmount, float noiseMultiplier)
+ {
+ OverlayColor = overlayColor;
+ LightingColor = lightingColor;
+ NoiseAmount = noiseAmount;
+ NoiseMultiplier = noiseMultiplier;
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ if (ScreenTexture == null)
+ return;
+
+ var handle = args.WorldHandle;
+ var isSpace = args.Space == OverlaySpace.WorldSpace;
+
+ if (isSpace)
+ {
+ _nightVisionShader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
+ _nightVisionShader.SetParameter("noise_amount", !DisableNoise ? NoiseAmount : 0);
+ _nightVisionShader.SetParameter("noise_multiplier", !DisableNoise ? NoiseMultiplier : 0);
+ handle.UseShader(_nightVisionShader);
+ }
+
+ var drawingColor = isSpace ? OverlayColor : LightingColor;
+ handle.DrawRect(args.WorldBounds, drawingColor);
+
+ if (isSpace)
+ handle.UseShader(null);
+ }
+}
diff --git a/Content.Client/Overlays/ShowCriminalRecordIconsSystem.cs b/Content.Client/Overlays/ShowCriminalRecordIconsSystem.cs
index d343855ba17..cd38fee82be 100644
--- a/Content.Client/Overlays/ShowCriminalRecordIconsSystem.cs
+++ b/Content.Client/Overlays/ShowCriminalRecordIconsSystem.cs
@@ -1,15 +1,11 @@
using Content.Shared.Overlays;
using Content.Shared.Security.Components;
-using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
-using Robust.Shared.Prototypes;
namespace Content.Client.Overlays;
public sealed partial class ShowCriminalRecordIconsSystem : EquipmentHudSystem
{
- [Dependency] private IPrototypeManager _prototype = default!;
-
public override void Initialize()
{
base.Initialize();
@@ -22,7 +18,7 @@ private void OnGetStatusIconsEvent(EntityUid uid, CriminalRecordComponent compon
if (!IsActive)
return;
- if (_prototype.Resolve(component.StatusIcon, out var iconPrototype))
+ if (ProtoMan.Resolve(component.StatusIcon, out var iconPrototype))
ev.StatusIcons.Add(iconPrototype);
}
}
diff --git a/Content.Client/Overlays/ShowHealthBarsSystem.cs b/Content.Client/Overlays/ShowHealthBarsSystem.cs
index a208a4cfca1..a5badf5842d 100644
--- a/Content.Client/Overlays/ShowHealthBarsSystem.cs
+++ b/Content.Client/Overlays/ShowHealthBarsSystem.cs
@@ -1,7 +1,6 @@
using Content.Shared.Inventory.Events;
using Content.Shared.Overlays;
using Robust.Client.Graphics;
-using Robust.Shared.Prototypes;
namespace Content.Client.Overlays;
@@ -11,7 +10,6 @@ namespace Content.Client.Overlays;
public sealed partial class ShowHealthBarsSystem : EquipmentHudSystem
{
[Dependency] private IOverlayManager _overlayMan = default!;
- [Dependency] private IPrototypeManager _prototype = default!;
private EntityHealthBarOverlay _overlay = default!;
@@ -21,7 +19,7 @@ public override void Initialize()
SubscribeLocalEvent(OnHandleState);
- _overlay = new(EntityManager, _prototype);
+ _overlay = new(EntityManager, ProtoMan);
}
private void OnHandleState(Entity ent, ref AfterAutoHandleStateEvent args)
diff --git a/Content.Client/Overlays/ShowHealthIconsSystem.cs b/Content.Client/Overlays/ShowHealthIconsSystem.cs
index 76f30789581..88f557ac0e3 100644
--- a/Content.Client/Overlays/ShowHealthIconsSystem.cs
+++ b/Content.Client/Overlays/ShowHealthIconsSystem.cs
@@ -4,7 +4,6 @@
using Content.Shared.Overlays;
using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
-using Robust.Shared.Prototypes;
using Content.Shared.Damage.Components;
namespace Content.Client.Overlays;
@@ -14,8 +13,6 @@ namespace Content.Client.Overlays;
///
public sealed partial class ShowHealthIconsSystem : EquipmentHudSystem
{
- [Dependency] private IPrototypeManager _prototypeMan = default!;
-
[ViewVariables]
public HashSet DamageContainers = new();
@@ -81,9 +78,9 @@ private IReadOnlyList DecideHealthIcons(Entity(entity, out var state))
{
// Since there is no MobState for a rotting mob, we have to deal with this case first.
- if (HasComp(entity) && _prototypeMan.Resolve(injurableComp.RottingIcon, out var rottingIcon))
+ if (HasComp(entity) && ProtoMan.Resolve(injurableComp.RottingIcon, out var rottingIcon))
result.Add(rottingIcon);
- else if (injurableComp.HealthIcons.TryGetValue(state.CurrentState, out var value) && _prototypeMan.Resolve(value, out var icon))
+ else if (injurableComp.HealthIcons.TryGetValue(state.CurrentState, out var value) && ProtoMan.Resolve(value, out var icon))
result.Add(icon);
}
}
diff --git a/Content.Client/Overlays/ShowMindShieldIconsSystem.cs b/Content.Client/Overlays/ShowMindShieldIconsSystem.cs
index 31696393abe..6ae2b46f792 100644
--- a/Content.Client/Overlays/ShowMindShieldIconsSystem.cs
+++ b/Content.Client/Overlays/ShowMindShieldIconsSystem.cs
@@ -1,15 +1,11 @@
using Content.Shared.Mindshield.Components;
using Content.Shared.Overlays;
-using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
-using Robust.Shared.Prototypes;
namespace Content.Client.Overlays;
public sealed partial class ShowMindShieldIconsSystem : EquipmentHudSystem
{
- [Dependency] private IPrototypeManager _prototype = default!;
-
public override void Initialize()
{
base.Initialize();
@@ -21,9 +17,9 @@ public override void Initialize()
// ...imagine cheating in a game about silly paper dolls
private void OnGetStatusIconsEventFake(EntityUid uid, FakeMindShieldComponent component, ref GetStatusIconsEvent ev)
{
- if(!IsActive)
+ if (!IsActive)
return;
- if (component.IsEnabled && _prototype.Resolve(component.MindShieldStatusIcon, out var fakeStatusIconPrototype))
+ if (component.IsEnabled && ProtoMan.Resolve(component.MindShieldStatusIcon, out var fakeStatusIconPrototype))
ev.StatusIcons.Add(fakeStatusIconPrototype);
}
@@ -32,7 +28,7 @@ private void OnGetStatusIconsEvent(EntityUid uid, MindShieldComponent component,
if (!IsActive)
return;
- if (_prototype.Resolve(component.MindShieldStatusIcon, out var iconPrototype))
+ if (ProtoMan.Resolve(component.MindShieldStatusIcon, out var iconPrototype))
ev.StatusIcons.Add(iconPrototype);
}
}
diff --git a/Content.Client/Overlays/ShowSyndicateIconsSystem.cs b/Content.Client/Overlays/ShowSyndicateIconsSystem.cs
index c68e8730160..7c5ddd90413 100644
--- a/Content.Client/Overlays/ShowSyndicateIconsSystem.cs
+++ b/Content.Client/Overlays/ShowSyndicateIconsSystem.cs
@@ -2,14 +2,11 @@
using Content.Shared.NukeOps;
using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
-using Robust.Shared.Prototypes;
namespace Content.Client.Overlays;
public sealed partial class ShowSyndicateIconsSystem : EquipmentHudSystem
{
- [Dependency] private IPrototypeManager _prototype = default!;
-
public override void Initialize()
{
base.Initialize();
@@ -22,7 +19,7 @@ private void OnGetStatusIconsEvent(EntityUid uid, NukeOperativeComponent compone
if (!IsActive)
return;
- if (_prototype.TryIndex(component.SyndStatusIcon, out var iconPrototype))
+ if (ProtoMan.TryIndex(component.SyndStatusIcon, out var iconPrototype))
ev.StatusIcons.Add(iconPrototype);
}
}
diff --git a/Content.Client/Overlays/StencilOverlay.Weather.cs b/Content.Client/Overlays/StencilOverlay.Weather.cs
index 2a46dbecd52..2d7a94828ff 100644
--- a/Content.Client/Overlays/StencilOverlay.Weather.cs
+++ b/Content.Client/Overlays/StencilOverlay.Weather.cs
@@ -33,7 +33,7 @@ private void DrawWeather(
_grids.Clear();
// idk if this is safe to cache in a field and clear sloth help
- _mapManager.FindGridsIntersecting(mapId, worldAABB, ref _grids);
+ _map.FindGridsIntersecting(mapId, worldAABB, ref _grids);
foreach (var grid in _grids)
{
diff --git a/Content.Client/Overlays/StencilOverlay.cs b/Content.Client/Overlays/StencilOverlay.cs
index 83f9188ccb1..86c9c68a836 100644
--- a/Content.Client/Overlays/StencilOverlay.cs
+++ b/Content.Client/Overlays/StencilOverlay.cs
@@ -27,7 +27,6 @@ public sealed partial class StencilOverlay : Overlay
[Dependency] private IClyde _clyde = default!;
[Dependency] private IEntityManager _entManager = default!;
[Dependency] private IGameTiming _timing = default!;
- [Dependency] private IMapManager _mapManager = default!;
[Dependency] private IPrototypeManager _protoManager = default!;
private readonly ParallaxSystem _parallax;
private readonly SharedTransformSystem _transform;
diff --git a/Content.Client/PDA/PdaMenu.xaml b/Content.Client/PDA/PdaMenu.xaml
index 8c9b4ae2ee6..c5949aa58da 100644
--- a/Content.Client/PDA/PdaMenu.xaml
+++ b/Content.Client/PDA/PdaMenu.xaml
@@ -48,12 +48,12 @@
-
+ programs)
{
- ProgramList.RemoveAllChildren();
-
if (programs.Count == 0)
{
+ ProgramList.RemoveAllChildren();
+
ProgramList.AddChild(new Label()
{
Text = Loc.GetString("comp-pda-io-no-programs-available"),
@@ -215,50 +215,43 @@ public void UpdateAvailablePrograms(List<(EntityUid, CartridgeComponent)> progra
return;
}
- var row = CreateProgramListRow();
- var itemCount = 1;
- ProgramList.AddChild(row);
-
- foreach (var (uid, component) in programs)
+ if (ProgramList.ChildCount >= 1 && ProgramList.Children[0] is Label label)
{
- //Create a new row every second program item starting from the first
- if (itemCount % 2 != 0)
- {
- row = CreateProgramListRow();
- ProgramList.AddChild(row);
- }
-
- var item = new PdaProgramItem();
+ label.Orphan();
+ }
- if (component.Icon is not null)
- item.Icon.SetFromSpriteSpecifier(component.Icon);
+ while (ProgramList.ChildCount > programs.Count)
+ {
+ ProgramList.Children[ProgramList.ChildCount - 1].Orphan();
+ }
- item.OnPressed += _ => OnProgramItemPressed?.Invoke(uid);
+ for (var i = 0; i < programs.Count; i++)
+ {
+ var cartridge = programs[i];
- switch (component.InstallationStatus)
+ if (i < ProgramList.ChildCount)
{
- case InstallationStatus.Cartridge:
- item.InstallButton.Visible = true;
- item.InstallButton.Text = Loc.GetString("cartridge-bound-user-interface-install-button");
- item.InstallButton.OnPressed += _ => OnInstallButtonPressed?.Invoke(uid);
- break;
- case InstallationStatus.Installed:
- item.InstallButton.Visible = true;
- item.InstallButton.Text = Loc.GetString("cartridge-bound-user-interface-uninstall-button");
- item.InstallButton.OnPressed += _ => OnUninstallButtonPressed?.Invoke(uid);
- break;
+ var currentItem = ProgramList.Children[i];
+ if (currentItem is PdaProgramItem programItem)
+ {
+ programItem.SetCartridge(cartridge);
+ continue;
+ }
+
+ DebugTools.Assert(i == ProgramList.ChildCount-1);
+ currentItem.Orphan();
}
- item.ProgramName.Text = Loc.GetString(component.ProgramName);
- item.SetHeight = 20;
- row.AddChild(item);
-
- itemCount++;
+ var item = new PdaProgramItem(cartridge);
+ item.OnProgramItemPressed += uid => OnProgramItemPressed?.Invoke(uid);
+ item.OnUninstallButtonPressed += uid => OnUninstallButtonPressed?.Invoke(uid);
+ item.OnInstallButtonPressed += uid => OnInstallButtonPressed?.Invoke(uid);
+ ProgramList.AddChild(item);
}
//Add a filler item to the last row when it only contains one item
- if (itemCount % 2 == 0)
- row.AddChild(new Control() { HorizontalExpand = true });
+ if (programs.Count % 2 == 0)
+ ProgramList.AddChild(new Control() { HorizontalExpand = true });
}
///
@@ -319,15 +312,6 @@ public void ChangeView(int view)
_currentView = view;
}
- private static BoxContainer CreateProgramListRow()
- {
- return new BoxContainer()
- {
- Orientation = BoxContainer.LayoutOrientation.Horizontal,
- HorizontalExpand = true
- };
- }
-
private void HideAllViews()
{
var views = ViewContainer.Children;
diff --git a/Content.Client/PDA/PdaProgramItem.xaml b/Content.Client/PDA/PdaProgramItem.xaml
index 9f9fba938f0..c69541e50f0 100644
--- a/Content.Client/PDA/PdaProgramItem.xaml
+++ b/Content.Client/PDA/PdaProgramItem.xaml
@@ -1,17 +1,17 @@
-
+
-
+
-
+
-
-
+
+
-
+
diff --git a/Content.Client/PDA/PdaProgramItem.xaml.cs b/Content.Client/PDA/PdaProgramItem.xaml.cs
index 9709f5706b7..64dd300994a 100644
--- a/Content.Client/PDA/PdaProgramItem.xaml.cs
+++ b/Content.Client/PDA/PdaProgramItem.xaml.cs
@@ -1,4 +1,5 @@
-using Robust.Client.AutoGenerated;
+using Content.Shared.CartridgeLoader;
+using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
@@ -12,6 +13,12 @@ public sealed partial class PdaProgramItem : ContainerButton
public const string NormalBgColor = "#313138";
public const string HoverColor = "#3E6C45";
+ private Entity _cartridge;
+
+ public event Action? OnProgramItemPressed;
+ public event Action? OnUninstallButtonPressed;
+ public event Action? OnInstallButtonPressed;
+
private readonly StyleBoxFlat _styleBox = new()
{
BackgroundColor = Color.FromHex("#25252a"),
@@ -23,10 +30,38 @@ public Color BackgroundColor
set => _styleBox.BackgroundColor = value;
}
- public PdaProgramItem()
+ public PdaProgramItem(Entity program)
{
RobustXamlLoader.Load(this);
+
+ SetCartridge(program);
Panel.PanelOverride = _styleBox;
+
+ OnPressed += OnSelfPressed;
+ InstallButton.OnPressed += OnInstallPressed;
+ }
+
+ private void OnSelfPressed(ButtonEventArgs args)
+ {
+ OnProgramItemPressed?.Invoke(_cartridge);
+ }
+
+ private void OnInstallPressed(ButtonEventArgs args)
+ {
+ switch (_cartridge.Comp.InstallationStatus)
+ {
+ case InstallationStatus.Cartridge:
+ OnInstallButtonPressed?.Invoke(_cartridge.Owner);
+ break;
+
+ case InstallationStatus.Installed:
+ OnUninstallButtonPressed?.Invoke(_cartridge.Owner);
+ break;
+
+ case InstallationStatus.Readonly:
+ default:
+ break;
+ }
}
protected override void Draw(DrawingHandleScreen handle)
@@ -35,6 +70,29 @@ protected override void Draw(DrawingHandleScreen handle)
if (TryGetStyleProperty(StylePropertyBgColor, out var bgColor))
BackgroundColor = bgColor;
+ }
+
+ public void SetCartridge(Entity program)
+ {
+ if (program.Comp.Icon is { } icon)
+ Icon.SetFromSpriteSpecifier(icon);
+
+ _cartridge = program;
+ ProgramName.Text = Loc.GetString(program.Comp.ProgramName);
+ switch (program.Comp.InstallationStatus)
+ {
+ case InstallationStatus.Cartridge:
+ InstallButton.Visible = true;
+ InstallButton.Text = Loc.GetString("cartridge-bound-user-interface-install-button");
+ break;
+ case InstallationStatus.Installed:
+ InstallButton.Visible = true;
+ InstallButton.Text = Loc.GetString("cartridge-bound-user-interface-uninstall-button");
+ break;
+ case InstallationStatus.Readonly:
+ InstallButton.Visible = false;
+ break;
+ }
}
}
diff --git a/Content.Client/Paper/UI/PaperVisualsComponent.cs b/Content.Client/Paper/UI/PaperVisualsComponent.cs
index 8415f5b29dd..b76ea621a14 100644
--- a/Content.Client/Paper/UI/PaperVisualsComponent.cs
+++ b/Content.Client/Paper/UI/PaperVisualsComponent.cs
@@ -107,10 +107,12 @@ public sealed partial class PaperVisualsComponent : Component
public int ContentImageNumLines = 1;
///
- /// Modulate the style's font by this color
+ /// Optional default color for text written on the paper. Useful to
+ /// specify light color text for use on dark backgrounds. Paper text
+ /// can still use markup to override color for subsections.
///
[DataField]
- public Color FontAccentColor = new Color(223, 223, 213);
+ public Color? DefaultTextColor = null;
///
/// This can enforce that your paper has a limited area to write in.
diff --git a/Content.Client/Paper/UI/PaperWindow.xaml.cs b/Content.Client/Paper/UI/PaperWindow.xaml.cs
index 483caedb411..15a2ca8a0df 100644
--- a/Content.Client/Paper/UI/PaperWindow.xaml.cs
+++ b/Content.Client/Paper/UI/PaperWindow.xaml.cs
@@ -23,7 +23,9 @@ public sealed partial class PaperWindow : BaseWindow
private static Color DefaultTextColor = new(25, 25, 25);
- //
+ // Default color for text which hasn't been changed using markup
+ private Color _writtenTextColor = DefaultTextColor;
+
// Size of resize handles around the paper
private const int DRAG_MARGIN_SIZE = 16;
@@ -152,8 +154,7 @@ public void InitVisuals(EntityUid entity, PaperVisualsComponent visuals)
visuals.FooterMargin.Right, visuals.FooterMargin.Bottom);
PaperContent.ModulateSelfOverride = visuals.ContentImageModulate;
- WrittenTextLabel.ModulateSelfOverride = visuals.FontAccentColor;
- FillStatus.ModulateSelfOverride = visuals.FontAccentColor;
+ _writtenTextColor = visuals.DefaultTextColor ?? DefaultTextColor;
var contentImage = visuals.ContentImagePath != null ? _resCache.GetResource(visuals.ContentImagePath) : null;
if (contentImage != null)
@@ -271,7 +272,7 @@ public void Populate(PaperComponent.PaperBoundUserInterfaceState state)
{
msg.AddMarkupPermissive("\r\n");
}
- WrittenTextLabel.SetMessage(msg, UserFormattableTags.BaseAllowedTags, DefaultTextColor);
+ WrittenTextLabel.SetMessage(msg, UserFormattableTags.BaseAllowedTags, _writtenTextColor);
WrittenTextLabel.Visible = !isEditing && state.Text.Length > 0;
BlankPaperIndicator.Visible = !isEditing && state.Text.Length == 0;
diff --git a/Content.Client/Paper/UI/StampCollection.xaml.cs b/Content.Client/Paper/UI/StampCollection.xaml.cs
index b2a3b8c88d6..865dead4c09 100644
--- a/Content.Client/Paper/UI/StampCollection.xaml.cs
+++ b/Content.Client/Paper/UI/StampCollection.xaml.cs
@@ -40,7 +40,8 @@ public void AddStamp(StampWidget s)
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
- var random = new Random(PlacementSeed);
+ var random = new RobustRandom();
+ random.SetSeed(PlacementSeed);
var r = (finalSize * 0.5f).Length();
var dtheta = -MathHelper.DegreesToRadians(90);
var theta0 = random.Next(0, 3) * dtheta;
diff --git a/Content.Client/Parallax/ParallaxGenerator.cs b/Content.Client/Parallax/ParallaxGenerator.cs
index 0337de30d1c..347f1f4ad27 100644
--- a/Content.Client/Parallax/ParallaxGenerator.cs
+++ b/Content.Client/Parallax/ParallaxGenerator.cs
@@ -408,7 +408,8 @@ public override void Apply(Image bitmap)
private void GenPoints(Image buffer)
{
var o = PointSize - 1;
- var random = new Random(Seed);
+ var random = new RobustRandom();
+ random.SetSeed(Seed);
var span = buffer.GetPixelSpan();
for (var i = 0; i < PointCount; i++)
@@ -435,7 +436,8 @@ private void GenPoints(Image buffer)
private void GenPointsMasked(Image buffer)
{
var o = PointSize - 1;
- var random = new Random(Seed);
+ var random = new RobustRandom();
+ random.SetSeed(Seed);
var noise = new FastNoiseLite((int)MaskSeed);
noise.SetFractalType(MaskNoiseType);
noise.SetFractalLacunarity(MaskLacunarity);
diff --git a/Content.Client/Pointing/PointingSystem.cs b/Content.Client/Pointing/PointingSystem.cs
index e833fff60f9..9d85f4cee34 100644
--- a/Content.Client/Pointing/PointingSystem.cs
+++ b/Content.Client/Pointing/PointingSystem.cs
@@ -8,7 +8,7 @@
namespace Content.Client.Pointing;
-public sealed partial class PointingSystem : SharedPointingSystem
+public sealed partial class PointingSystem
{
[Dependency] private SpriteSystem _sprite = default!;
@@ -48,12 +48,21 @@ private void AddPointingVerb(GetVerbsEvent args)
Text = Loc.GetString("pointing-verb-get-data-text"),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/point.svg.192dpi.png")),
ClientExclusive = true,
- Act = () => RaiseNetworkEvent(new PointingAttemptEvent(GetNetEntity(args.Target)))
+ Act = () => TryPointAtEntity(GetNetEntity(args.Target))
};
args.Verbs.Add(verb);
}
+ ///
+ /// Tries to point at a target entity
+ ///
+ /// The target to point at
+ public void TryPointAtEntity(NetEntity target)
+ {
+ RaiseNetworkEvent(new PointingAttemptEvent(target));
+ }
+
private void OnArrowStartup(EntityUid uid, PointingArrowComponent component, ComponentStartup args)
{
if (TryComp(uid, out var sprite))
diff --git a/Content.Client/Popups/PopupSystem.cs b/Content.Client/Popups/PopupSystem.cs
index 32bd93cc849..11d5b02a5cd 100644
--- a/Content.Client/Popups/PopupSystem.cs
+++ b/Content.Client/Popups/PopupSystem.cs
@@ -1,5 +1,4 @@
using System.Linq;
-using Content.Shared.Containers;
using Content.Shared.Examine;
using Content.Shared.GameTicking;
using Content.Shared.Popups;
@@ -12,361 +11,370 @@
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Player;
-using Robust.Shared.Prototypes;
using Robust.Shared.Replays;
-using Robust.Shared.Timing;
-namespace Content.Client.Popups
+namespace Content.Client.Popups;
+
+public sealed partial class PopupSystem : SharedPopupSystem
{
- public sealed partial class PopupSystem : SharedPopupSystem
+ [Dependency] private IConfigurationManager _configManager = default!;
+ [Dependency] private IInputManager _inputManager = default!;
+ [Dependency] private IOverlayManager _overlay = default!;
+ [Dependency] private IPlayerManager _playerManager = default!;
+ [Dependency] private IUserInterfaceManager _uiManager = default!;
+ [Dependency] private IReplayRecordingManager _replayRecording = default!;
+ [Dependency] private ExamineSystemShared _examine = default!;
+ [Dependency] private SharedTransformSystem _transform = default!;
+
+ public IReadOnlyCollection WorldLabels => _aliveWorldLabels.Values;
+ public IReadOnlyCollection CursorLabels => _aliveCursorLabels.Values;
+
+ private readonly Dictionary _aliveWorldLabels = new();
+ private readonly Dictionary _aliveCursorLabels = new();
+
+ ///
+ /// List of popups that have been predicted by the client.
+ /// If a popup is received from the server that matches one of these, it will be ignored to prevent duplicates.
+ ///
+ private readonly List _predictionInstances = new();
+
+ public const float MinimumPopupLifetime = 0.7f;
+ public const float MaximumPopupLifetime = 5f;
+ public const float PopupLifetimePerCharacter = 0.04f;
+
+ public override void Initialize()
{
- [Dependency] private IConfigurationManager _configManager = default!;
- [Dependency] private IInputManager _inputManager = default!;
- [Dependency] private IOverlayManager _overlay = default!;
- [Dependency] private IPlayerManager _playerManager = default!;
- [Dependency] private IPrototypeManager _prototype = default!;
- [Dependency] private IGameTiming _timing = default!;
- [Dependency] private IUserInterfaceManager _uiManager = default!;
- [Dependency] private IReplayRecordingManager _replayRecording = default!;
- [Dependency] private ExamineSystemShared _examine = default!;
- [Dependency] private SharedTransformSystem _transform = default!;
-
- public IReadOnlyCollection WorldLabels => _aliveWorldLabels.Values;
- public IReadOnlyCollection CursorLabels => _aliveCursorLabels.Values;
-
- private readonly Dictionary _aliveWorldLabels = new();
- private readonly Dictionary _aliveCursorLabels = new();
-
- public const float MinimumPopupLifetime = 0.7f;
- public const float MaximumPopupLifetime = 5f;
- public const float PopupLifetimePerCharacter = 0.04f;
-
- public override void Initialize()
- {
- SubscribeNetworkEvent(OnPopupCursorEvent);
- SubscribeNetworkEvent(OnPopupCoordinatesEvent);
- SubscribeNetworkEvent(OnPopupEntityEvent);
- SubscribeNetworkEvent(OnRoundRestart);
- _overlay
- .AddOverlay(new PopupOverlay(
- _configManager,
- EntityManager,
- _playerManager,
- _prototype,
- _uiManager,
- _uiManager.GetUIController(),
- _examine,
- _transform,
- this));
- }
-
- public override void Shutdown()
- {
- base.Shutdown();
- _overlay
- .RemoveOverlay();
- }
-
- private void WrapAndRepeatPopup(PopupLabel existingLabel, string popupMessage)
- {
- existingLabel.TotalTime = 0;
- existingLabel.Repeats += 1;
- existingLabel.Text = Loc.GetString("popup-system-repeated-popup-stacking-wrap",
- ("popup-message", popupMessage),
- ("count", existingLabel.Repeats));
- }
-
- private void PopupMessage(string? message, PopupType type, EntityCoordinates coordinates, EntityUid? entity, bool recordReplay)
- {
- if (message == null)
- return;
+ base.Initialize();
+
+ _overlay.AddOverlay(new PopupOverlay(
+ _configManager,
+ EntityManager,
+ _playerManager,
+ ProtoMan,
+ _uiManager,
+ _uiManager.GetUIController(),
+ _examine,
+ _transform,
+ this));
+ }
- if (recordReplay && _replayRecording.IsRecording)
- {
- if (entity != null)
- _replayRecording.RecordClientMessage(new PopupEntityEvent(message, type, GetNetEntity(entity.Value)));
- else
- _replayRecording.RecordClientMessage(new PopupCoordinatesEvent(message, type, GetNetCoordinates(coordinates)));
- }
+ public override void Shutdown()
+ {
+ base.Shutdown();
- var popupData = new WorldPopupData(message, type, coordinates, entity);
- if (_aliveWorldLabels.TryGetValue(popupData, out var existingLabel))
- {
- WrapAndRepeatPopup(existingLabel, popupData.Message);
- return;
- }
+ _overlay.RemoveOverlay();
+ }
- var label = new WorldPopupLabel(coordinates)
- {
- Text = message,
- Type = type,
- };
+ ///
+ /// If the same popup is repeated, this will make show x2, x3, x4, ... at the end of the message instead of creating a new, overlapping popup.
+ ///
+ private void WrapAndRepeatPopup(PopupLabel existingLabel, string popupMessage)
+ {
+ existingLabel.TotalTime = 0;
+ existingLabel.Repeats += 1;
+ existingLabel.Text = Loc.GetString("popup-system-repeated-popup-stacking-wrap",
+ ("popup-message", popupMessage),
+ ("count", existingLabel.Repeats));
+ }
- _aliveWorldLabels.Add(popupData, label);
- }
+ ///
+ /// Interal implementation for both coordinates and entity popups.
+ ///
+ private void PopupInternal(string? message, PopupType type, EntityCoordinates coordinates, EntityUid? entity, bool recordReplay)
+ {
+ if (message == null)
+ return;
- #region Abstract Method Implementations
- public override void PopupCoordinates(string? message, EntityCoordinates coordinates, PopupType type = PopupType.Small)
+ if (recordReplay && _replayRecording.IsRecording)
{
- PopupMessage(message, type, coordinates, null, true);
+ if (entity != null)
+ _replayRecording.RecordClientMessage(new PopupEntityEvent(message, type, Timing.CurTick, GetNetEntity(entity.Value)));
+ else
+ _replayRecording.RecordClientMessage(new PopupCoordinatesEvent(message, type, Timing.CurTick, GetNetCoordinates(coordinates), 0));
}
- public override void PopupCoordinates(string? message, EntityCoordinates coordinates, ICommonSession recipient, PopupType type = PopupType.Small)
+ var popupData = new WorldPopupData(message, type, coordinates, entity);
+ if (_aliveWorldLabels.TryGetValue(popupData, out var existingLabel))
{
- if (_playerManager.LocalSession == recipient)
- PopupMessage(message, type, coordinates, null, true);
+ WrapAndRepeatPopup(existingLabel, popupData.Message);
+ return;
}
- public override void PopupCoordinates(string? message, EntityCoordinates coordinates, EntityUid recipient, PopupType type = PopupType.Small)
+ var label = new WorldPopupLabel(coordinates)
{
- if (_playerManager.LocalEntity == recipient)
- PopupMessage(message, type, coordinates, null, true);
- }
+ Text = message,
+ Type = type,
+ };
+
+ _aliveWorldLabels.Add(popupData, label);
+ }
- public override void PopupPredictedCoordinates(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small)
+ ///
+ /// Internal implementation for cursor popups.
+ ///
+ private void PopupCursorInternal(string? message, PopupType type, bool recordReplay)
+ {
+ if (message == null)
+ return;
+
+ if (recordReplay && _replayRecording.IsRecording)
+ _replayRecording.RecordClientMessage(new PopupCursorEvent(message, type, Timing.CurTick));
+
+ var popupData = new CursorPopupData(message, type);
+ if (_aliveCursorLabels.TryGetValue(popupData, out var existingLabel))
{
- if (recipient != null && _timing.IsFirstTimePredicted)
- PopupCoordinates(message, coordinates, recipient.Value, type);
+ WrapAndRepeatPopup(existingLabel, popupData.Message);
+ return;
}
- private void PopupCursorInternal(string? message, PopupType type, bool recordReplay)
+ var label = new CursorPopupLabel(_inputManager.MouseScreenPosition)
{
- if (message == null)
- return;
+ Text = message,
+ Type = type,
+ };
- if (recordReplay && _replayRecording.IsRecording)
- _replayRecording.RecordClientMessage(new PopupCursorEvent(message, type));
+ _aliveCursorLabels.Add(popupData, label);
+ }
- var popupData = new CursorPopupData(message, type);
- if (_aliveCursorLabels.TryGetValue(popupData, out var existingLabel))
- {
- WrapAndRepeatPopup(existingLabel, popupData.Message);
- return;
- }
+ #region Abstract Method Implementations
+
+ ///
+ /// Shows a popup at the local user's cursor.
+ ///
+ ///
+ /// This overload only exists on the client. If you want to use this in Shared you should use the overload that takes a recipient, session or filter instead.
+ /// We do not add a virtual method to the shared system because that will cause problems if the shared code is run in a non-predicted way.
+ ///
+ /// The message to display.
+ /// Used to customize how this popup should appear visually.
+ public void PopupCursor(string? message, PopupType type = PopupType.Small)
+ {
+ if (!Timing.IsFirstTimePredicted || message is null)
+ return;
- var label = new CursorPopupLabel(_inputManager.MouseScreenPosition)
- {
- Text = message,
- Type = type,
- };
+ _predictionInstances.Add(new PopupCursorEvent.PredictionInstance(message, type, Timing.CurTick));
+ PopupCursorInternal(message, type, true);
+ }
- _aliveCursorLabels.Add(popupData, label);
- }
+ public override void PopupCursor(string? message, EntityUid? recipient, PopupType type = PopupType.Small)
+ {
+ if (_playerManager.LocalEntity == recipient)
+ PopupCursor(message, type);
+ }
- public override void PopupCursor(string? message, PopupType type = PopupType.Small)
- {
- if (!_timing.IsFirstTimePredicted)
- return;
+ public override void PopupCursor(string? message, ICommonSession recipient, PopupType type = PopupType.Small)
+ {
+ if (_playerManager.LocalSession == recipient)
+ PopupCursor(message, type);
+ }
- PopupCursorInternal(message, type, true);
- }
+ public override void PopupCursor(string? message, Filter filter, bool recordReplay, PopupType type = PopupType.Small)
+ {
+ if (filter.Recipients.Contains(_playerManager.LocalSession))
+ PopupCursor(message, type);
+ }
- public override void PopupCursor(string? message, ICommonSession recipient, PopupType type = PopupType.Small)
- {
- if (_playerManager.LocalSession == recipient)
- PopupCursor(message, type);
- }
+ public override void PopupCoordinates(string? message, EntityCoordinates coordinates, PopupType type = PopupType.Small, int predictionKey = 0)
+ {
+ if (!Timing.IsFirstTimePredicted || message is null)
+ return;
- public override void PopupCursor(string? message, EntityUid recipient, PopupType type = PopupType.Small)
- {
- if (_playerManager.LocalEntity == recipient)
- PopupCursor(message, type);
- }
+ _predictionInstances.Add(new PopupCoordinatesEvent.PredictionInstance(message, type, Timing.CurTick, predictionKey));
+ PopupInternal(message, type, coordinates, null, true);
+ }
- public override void PopupPredictedCursor(string? message, ICommonSession recipient, PopupType type = PopupType.Small)
- {
- PopupCursor(message, recipient, type);
- }
+ public override void PopupCoordinates(string? message, EntityCoordinates coordinates, ICommonSession recipient, PopupType type = PopupType.Small, int predictionKey = 0)
+ {
+ if (_playerManager.LocalSession == recipient)
+ PopupCoordinates(message, coordinates, type, predictionKey);
+ }
- public override void PopupPredictedCursor(string? message, EntityUid recipient, PopupType type = PopupType.Small)
- {
- PopupCursor(message, recipient, type);
- }
+ public override void PopupCoordinates(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small, int predictionKey = 0)
+ {
+ if (_playerManager.LocalEntity == recipient)
+ PopupCoordinates(message, coordinates, type, predictionKey);
+ }
- public override void PopupCoordinates(string? message, EntityCoordinates coordinates, Filter filter, bool replayRecord, PopupType type = PopupType.Small)
- {
- PopupCoordinates(message, coordinates, type);
- }
+ public override void PopupCoordinates(string? message, EntityCoordinates coordinates, Filter filter, bool replayRecord, PopupType type = PopupType.Small, int predictionKey = 0)
+ {
+ if (filter.Recipients.Contains(_playerManager.LocalSession))
+ PopupCoordinates(message, coordinates, type, predictionKey);
+ }
- public override void PopupEntity(string? message, EntityUid uid, EntityUid recipient, PopupType type = PopupType.Small)
- {
- if (_playerManager.LocalEntity == recipient)
- PopupEntity(message, uid, type);
- }
+ public override void PopupEntity(string? message, EntityUid uid, PopupType type = PopupType.Small)
+ {
+ if (!Timing.IsFirstTimePredicted || message is null)
+ return;
- public override void PopupEntity(string? message, EntityUid uid, ICommonSession recipient, PopupType type = PopupType.Small)
- {
- if (_playerManager.LocalSession == recipient)
- PopupEntity(message, uid, type);
- }
+ if (!TryComp(uid, out TransformComponent? transform))
+ return;
- public override void PopupEntity(string? message, EntityUid uid, Filter filter, bool recordReplay, PopupType type = PopupType.Small)
- {
- if (!filter.Recipients.Contains(_playerManager.LocalSession))
- return;
+ _predictionInstances.Add(new PopupEntityEvent.PredictionInstance(message, type, Timing.CurTick, GetNetEntity(uid)));
+ PopupInternal(message, type, transform.Coordinates, uid, true);
+ }
+
+ public override void PopupEntity(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small)
+ {
+ if (_playerManager.LocalEntity == recipient)
+ PopupEntity(message, uid, type);
+ }
+ public override void PopupEntity(string? message, EntityUid uid, ICommonSession recipient, PopupType type = PopupType.Small)
+ {
+ if (_playerManager.LocalSession == recipient)
PopupEntity(message, uid, type);
- }
+ }
- public override void PopupClient(string? message, EntityUid? recipient, PopupType type = PopupType.Small)
- {
- if (recipient == null)
- return;
+ public override void PopupEntity(string? message, EntityUid uid, Filter filter, bool recordReplay, PopupType type = PopupType.Small)
+ {
+ if (filter.Recipients.Contains(_playerManager.LocalSession))
+ PopupEntity(message, uid, type);
+ }
- if (_timing.IsFirstTimePredicted)
- PopupCursor(message, recipient.Value, type);
- }
+ #endregion
- public override void PopupClient(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small)
- {
- if (recipient == null)
- return;
+ #region Network Event Handlers
- if (_timing.IsFirstTimePredicted)
- PopupEntity(message, uid, recipient.Value, type);
- }
+ [SubscribeNetworkEvent]
+ private void OnPopupCursorEvent(PopupCursorEvent ev)
+ {
+ var instance = new PopupCursorEvent.PredictionInstance(ev.Message, ev.Type, ev.Tick);
+ if (_predictionInstances.Remove(instance))
+ return;
- public override void PopupClient(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small)
- {
- if (recipient == null)
- return;
+ PopupCursorInternal(ev.Message, ev.Type, false);
+ }
- if (_timing.IsFirstTimePredicted)
- PopupCoordinates(message, coordinates, recipient.Value, type);
- }
+ [SubscribeNetworkEvent]
+ private void OnPopupCoordinatesEvent(PopupCoordinatesEvent ev)
+ {
+ var instance = new PopupCoordinatesEvent.PredictionInstance(ev.Message, ev.Type, ev.Tick, ev.PredictionKey);
+ if (_predictionInstances.Remove(instance))
+ return;
- public override void PopupEntity(string? message, EntityUid uid, PopupType type = PopupType.Small)
- {
- if (TryComp(uid, out TransformComponent? transform))
- PopupMessage(message, type, transform.Coordinates, uid, true);
- }
+ PopupInternal(ev.Message, ev.Type, GetCoordinates(ev.Coordinates), null, false);
+ }
- public override void PopupPredicted(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small)
- {
- if (recipient != null && _timing.IsFirstTimePredicted)
- PopupEntity(message, uid, recipient.Value, type);
- }
+ [SubscribeNetworkEvent]
+ private void OnPopupEntityEvent(PopupEntityEvent ev)
+ {
+ var instance = new PopupEntityEvent.PredictionInstance(ev.Message, ev.Type, ev.Tick, ev.Uid);
+ if (_predictionInstances.Remove(instance))
+ return;
- public override void PopupPredicted(string? message, EntityUid uid, EntityUid? recipient, Filter filter, bool recordReplay, PopupType type = PopupType.Small)
- {
- if (recipient != null && _timing.IsFirstTimePredicted)
- PopupEntity(message, uid, recipient.Value, type);
- }
+ var entity = GetEntity(ev.Uid);
- public override void PopupPredicted(string? recipientMessage, string? othersMessage, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small)
- {
- if (recipient != null && _timing.IsFirstTimePredicted)
- PopupEntity(recipientMessage, uid, recipient.Value, type);
- }
+ if (TryComp(entity, out TransformComponent? transform))
+ PopupInternal(ev.Message, ev.Type, transform.Coordinates, entity, false);
+ }
- #endregion
+ [SubscribeNetworkEvent]
+ private void OnRoundRestart(RoundRestartCleanupEvent ev)
+ {
+ _aliveCursorLabels.Clear();
+ _aliveWorldLabels.Clear();
+ _predictionInstances.Clear();
+ }
- #region Network Event Handlers
+ #endregion
- private void OnPopupCursorEvent(PopupCursorEvent ev)
- {
- PopupCursorInternal(ev.Message, ev.Type, false);
- }
+ ///
+ /// Calculates the lifetime of a popup based on its text length.
+ ///
+ public static float GetPopupLifetime(PopupLabel label)
+ {
+ return Math.Clamp(PopupLifetimePerCharacter * label.Text.Length,
+ MinimumPopupLifetime,
+ MaximumPopupLifetime);
+ }
- private void OnPopupCoordinatesEvent(PopupCoordinatesEvent ev)
- {
- PopupMessage(ev.Message, ev.Type, GetCoordinates(ev.Coordinates), null, false);
- }
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
- private void OnPopupEntityEvent(PopupEntityEvent ev)
- {
- var entity = GetEntity(ev.Uid);
+ if (!Timing.IsFirstTimePredicted)
+ return; // We only need to clean up once per tick.
- if (TryComp(entity, out TransformComponent? transform))
- PopupMessage(ev.Message, ev.Type, transform.Coordinates, entity, false);
- }
- private void OnRoundRestart(RoundRestartCleanupEvent ev)
+ // We only keep track of prediction instances for a short amount of time to prevent memory leaks.
+ // We can safely assume that if a popup hasn't been received from the server within a 10 seconds, it will never be received.
+ var deleteTickCount = Timing.TickRate * 10;
+ if (_predictionInstances.Count != 0)
{
- _aliveCursorLabels.Clear();
- _aliveWorldLabels.Clear();
+ _predictionInstances.RemoveAll(p => (int)Timing.CurTick.Value - (int)p.Tick.Value > deleteTickCount);
}
+ }
- #endregion
-
- public static float GetPopupLifetime(PopupLabel label)
- {
- return Math.Clamp(PopupLifetimePerCharacter * label.Text.Length,
- MinimumPopupLifetime,
- MaximumPopupLifetime);
- }
+ public override void FrameUpdate(float frameTime)
+ {
+ if (_aliveWorldLabels.Count == 0 && _aliveCursorLabels.Count == 0)
+ return;
- public override void FrameUpdate(float frameTime)
+ if (_aliveWorldLabels.Count > 0)
{
- if (_aliveWorldLabels.Count == 0 && _aliveCursorLabels.Count == 0)
- return;
-
- if (_aliveWorldLabels.Count > 0)
+ var aliveWorldToRemove = new ValueList();
+ foreach (var (data, label) in _aliveWorldLabels)
{
- var aliveWorldToRemove = new ValueList();
- foreach (var (data, label) in _aliveWorldLabels)
+ label.TotalTime += frameTime;
+ if (label.TotalTime > GetPopupLifetime(label) || Deleted(label.InitialPos.EntityId))
{
- label.TotalTime += frameTime;
- if (label.TotalTime > GetPopupLifetime(label) || Deleted(label.InitialPos.EntityId))
- {
- aliveWorldToRemove.Add(data);
- }
- }
- foreach (var data in aliveWorldToRemove)
- {
- _aliveWorldLabels.Remove(data);
+ aliveWorldToRemove.Add(data);
}
}
-
- if (_aliveCursorLabels.Count > 0)
+ foreach (var data in aliveWorldToRemove)
{
- var aliveCursorToRemove = new ValueList();
- foreach (var (data, label) in _aliveCursorLabels)
- {
- label.TotalTime += frameTime;
- if (label.TotalTime > GetPopupLifetime(label))
- {
- aliveCursorToRemove.Add(data);
- }
- }
- foreach (var data in aliveCursorToRemove)
- {
- _aliveCursorLabels.Remove(data);
- }
+ _aliveWorldLabels.Remove(data);
}
}
- public abstract class PopupLabel
+ if (_aliveCursorLabels.Count > 0)
{
- public PopupType Type = PopupType.Small;
- public string Text { get; set; } = string.Empty;
- public float TotalTime { get; set; }
- public int Repeats = 1;
+ var aliveCursorToRemove = new ValueList();
+ foreach (var (data, label) in _aliveCursorLabels)
+ {
+ label.TotalTime += frameTime;
+ if (label.TotalTime > GetPopupLifetime(label))
+ {
+ aliveCursorToRemove.Add(data);
+ }
+ }
+ foreach (var data in aliveCursorToRemove)
+ {
+ _aliveCursorLabels.Remove(data);
+ }
}
+ }
- public sealed class WorldPopupLabel(EntityCoordinates coordinates) : PopupLabel
- {
- ///
- /// The original EntityCoordinates of the label.
- ///
- public EntityCoordinates InitialPos = coordinates;
- }
+ public abstract class PopupLabel
+ {
+ public PopupType Type = PopupType.Small;
+ public string Text { get; set; } = string.Empty;
+ public float TotalTime { get; set; }
+ public int Repeats = 1;
+ }
- public sealed class CursorPopupLabel(ScreenCoordinates screenCoords) : PopupLabel
- {
- public ScreenCoordinates InitialPos = screenCoords;
- }
+ public sealed class WorldPopupLabel(EntityCoordinates coordinates) : PopupLabel
+ {
+ ///
+ /// The original EntityCoordinates of the label.
+ ///
+ public EntityCoordinates InitialPos = coordinates;
+ }
- [UsedImplicitly]
- private record struct WorldPopupData(
- string Message,
- PopupType Type,
- EntityCoordinates Coordinates,
- EntityUid? Entity);
-
- [UsedImplicitly]
- private record struct CursorPopupData(
- string Message,
- PopupType Type);
+ public sealed class CursorPopupLabel(ScreenCoordinates screenCoords) : PopupLabel
+ {
+ public ScreenCoordinates InitialPos = screenCoords;
}
+
+ [UsedImplicitly]
+ private record struct WorldPopupData(
+ string Message,
+ PopupType Type,
+ EntityCoordinates Coordinates,
+ EntityUid? Entity);
+
+ [UsedImplicitly]
+ private record struct CursorPopupData(
+ string Message,
+ PopupType Type);
}
diff --git a/Content.Client/Power/Generation/Teg/TegCirculatorComponent.cs b/Content.Client/Power/Generation/Teg/TegCirculatorComponent.cs
deleted file mode 100644
index f11af1d7307..00000000000
--- a/Content.Client/Power/Generation/Teg/TegCirculatorComponent.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Content.Client.Power.Generation.Teg;
-
-///
-[RegisterComponent]
-public sealed partial class TegCirculatorComponent : Component
-{
-
-}
diff --git a/Content.Client/Power/Generation/Teg/TegSystem.cs b/Content.Client/Power/Generation/Teg/TegSystem.cs
deleted file mode 100644
index 0ae1b5e4964..00000000000
--- a/Content.Client/Power/Generation/Teg/TegSystem.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using Content.Client.Examine;
-using Robust.Shared.Map;
-using Robust.Shared.Prototypes;
-
-namespace Content.Client.Power.Generation.Teg;
-
-///
-/// Handles client-side logic for the thermo-electric generator (TEG).
-///
-///
-///
-/// TEG circulators show which direction the in- and outlet port is by popping up two floating arrows when examined.
-///
-///
-///
-public sealed class TegSystem : EntitySystem
-{
- private static readonly EntProtoId ArrowPrototype = "TegCirculatorArrow";
-
- public override void Initialize()
- {
- SubscribeLocalEvent(CirculatorExamined);
- }
-
- private void CirculatorExamined(EntityUid uid, TegCirculatorComponent component, ClientExaminedEvent args)
- {
- Spawn(ArrowPrototype, new EntityCoordinates(uid, 0, 0));
- }
-}
diff --git a/Content.Client/RCD/AlignRCDConstruction.cs b/Content.Client/RCD/AlignRCDConstruction.cs
index ee7411a6de0..4ffd25c6904 100644
--- a/Content.Client/RCD/AlignRCDConstruction.cs
+++ b/Content.Client/RCD/AlignRCDConstruction.cs
@@ -16,7 +16,6 @@ namespace Content.Client.RCD;
public sealed partial class AlignRCDConstruction : PlacementMode
{
[Dependency] private IEntityManager _entityManager = default!;
- [Dependency] private IMapManager _mapManager = default!;
private readonly SharedMapSystem _mapSystem;
private readonly HandsSystem _handsSystem;
private readonly RCDSystem _rcdSystem;
@@ -46,7 +45,7 @@ public AlignRCDConstruction(PlacementManager pMan) : base(pMan)
public override void AlignPlacementMode(ScreenCoordinates mouseScreen)
{
_unalignedMouseCoords = ScreenToCursorGrid(mouseScreen);
- MouseCoords = _unalignedMouseCoords.AlignWithClosestGridTile(SearchBoxSize, _entityManager, _mapManager);
+ MouseCoords = _unalignedMouseCoords.AlignWithClosestGridTile(SearchBoxSize, _entityManager);
var gridId = _transformSystem.GetGrid(MouseCoords);
diff --git a/Content.Client/RCD/RCDConstructionGhostSystem.cs b/Content.Client/RCD/RCDConstructionGhostSystem.cs
index 2b9a31e77b0..65d2545d53c 100644
--- a/Content.Client/RCD/RCDConstructionGhostSystem.cs
+++ b/Content.Client/RCD/RCDConstructionGhostSystem.cs
@@ -5,7 +5,6 @@
using Robust.Client.Placement;
using Robust.Client.Player;
using Robust.Shared.Enums;
-using Robust.Shared.Prototypes;
namespace Content.Client.RCD;
@@ -18,7 +17,6 @@ public sealed partial class RCDConstructionGhostSystem : EntitySystem
[Dependency] private IPlayerManager _playerManager = default!;
[Dependency] private IPlacementManager _placementManager = default!;
- [Dependency] private IPrototypeManager _protoManager = default!;
[Dependency] private HandsSystem _hands = default!;
private Direction _placementDirection = default;
@@ -55,7 +53,7 @@ public override void Update(float frameTime)
return;
}
- var prototype = _protoManager.Index(rcd.ProtoId);
+ var prototype = ProtoMan.Index(rcd.ProtoId);
// Update the direction the RCD prototype based on the placer direction
if (_placementDirection != _placementManager.Direction)
diff --git a/Content.Client/Revolutionary/RevolutionarySystem.cs b/Content.Client/Revolutionary/RevolutionarySystem.cs
index cf2733fc01b..4edd4d36691 100644
--- a/Content.Client/Revolutionary/RevolutionarySystem.cs
+++ b/Content.Client/Revolutionary/RevolutionarySystem.cs
@@ -1,7 +1,6 @@
using Content.Shared.Revolutionary.Components;
using Content.Shared.Revolutionary;
using Content.Shared.StatusIcon.Components;
-using Robust.Shared.Prototypes;
namespace Content.Client.Revolutionary;
@@ -10,8 +9,6 @@ namespace Content.Client.Revolutionary;
///
public sealed partial class RevolutionarySystem : SharedRevolutionarySystem
{
- [Dependency] private IPrototypeManager _prototype = default!;
-
public override void Initialize()
{
base.Initialize();
@@ -25,13 +22,13 @@ private void GetRevIcon(Entity ent, ref GetStatusIconsEv
if (HasComp(ent))
return;
- if (_prototype.Resolve(ent.Comp.StatusIcon, out var iconPrototype))
+ if (ProtoMan.Resolve(ent.Comp.StatusIcon, out var iconPrototype))
args.StatusIcons.Add(iconPrototype);
}
private void GetHeadRevIcon(Entity ent, ref GetStatusIconsEvent args)
{
- if (_prototype.Resolve(ent.Comp.StatusIcon, out var iconPrototype))
+ if (ProtoMan.Resolve(ent.Comp.StatusIcon, out var iconPrototype))
args.StatusIcons.Add(iconPrototype);
}
}
diff --git a/Content.Client/SSDIndicator/SSDIndicatorSystem.cs b/Content.Client/SSDIndicator/SSDIndicatorSystem.cs
index 18390c3665a..7e7c73f43c8 100644
--- a/Content.Client/SSDIndicator/SSDIndicatorSystem.cs
+++ b/Content.Client/SSDIndicator/SSDIndicatorSystem.cs
@@ -3,10 +3,8 @@
using Content.Shared.Mobs.Systems;
using Content.Shared.NPC;
using Content.Shared.SSDIndicator;
-using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
using Robust.Shared.Configuration;
-using Robust.Shared.Prototypes;
namespace Content.Client.SSDIndicator;
@@ -15,7 +13,6 @@ namespace Content.Client.SSDIndicator;
///
public sealed partial class SSDIndicatorSystem : EntitySystem
{
- [Dependency] private IPrototypeManager _prototype = default!;
[Dependency] private IConfigurationManager _cfg = default!;
[Dependency] private MobStateSystem _mobState = default!;
@@ -34,7 +31,7 @@ private void OnGetStatusIcon(EntityUid uid, SSDIndicatorComponent component, ref
!HasComp(uid) &&
HasComp(uid))
{
- args.StatusIcons.Add(_prototype.Index(component.Icon));
+ args.StatusIcons.Add(ProtoMan.Index(component.Icon));
}
}
}
diff --git a/Content.Client/Sandbox/SandboxSystem.cs b/Content.Client/Sandbox/SandboxSystem.cs
index 3965779b621..18bcc51f2f0 100644
--- a/Content.Client/Sandbox/SandboxSystem.cs
+++ b/Content.Client/Sandbox/SandboxSystem.cs
@@ -13,7 +13,6 @@ public sealed partial class SandboxSystem : SharedSandboxSystem
{
[Dependency] private IClientAdminManager _adminManager = default!;
[Dependency] private IClientConsoleHost _consoleHost = default!;
- [Dependency] private IMapManager _map = default!;
[Dependency] private IPlacementManager _placement = default!;
[Dependency] private ContentEyeSystem _contentEye = default!;
[Dependency] private SharedTransformSystem _transform = default!;
@@ -104,6 +103,8 @@ public bool Copy(ICommonSession? session, EntityCoordinates coords, EntityUid ui
if (_placement.Eraser)
_placement.ToggleEraser();
+ _placement.Direction = _transform.GetWorldRotation(uid).GetCardinalDir();
+
_placement.BeginPlacing(new()
{
EntityType = comp.EntityPrototype.ID,
@@ -116,12 +117,15 @@ public bool Copy(ICommonSession? session, EntityCoordinates coords, EntityUid ui
// Try copy tile.
- if (!_map.TryFindGridAt(_transform.ToMapCoordinates(coords), out var gridUid, out var grid) || !_mapSystem.TryGetTileRef(gridUid, grid, coords, out var tileRef))
+ if (!_mapSystem.TryFindGridAt(_transform.ToMapCoordinates(coords), out var gridUid, out var grid) || !_mapSystem.TryGetTileRef(gridUid, grid, coords, out var tileRef))
return false;
if (_placement.Eraser)
_placement.ToggleEraser();
+ _placement.Direction = (Direction)(tileRef.Tile.RotationMirroring % 4 * 2);
+ _placement.Mirrored = tileRef.Tile.RotationMirroring >= 4;
+
_placement.BeginPlacing(new()
{
EntityType = null,
diff --git a/Content.Client/Shuttles/UI/MapScreen.xaml.cs b/Content.Client/Shuttles/UI/MapScreen.xaml.cs
index c39656f6816..bf59a0b92d0 100644
--- a/Content.Client/Shuttles/UI/MapScreen.xaml.cs
+++ b/Content.Client/Shuttles/UI/MapScreen.xaml.cs
@@ -28,7 +28,6 @@ public sealed partial class MapScreen : BoxContainer
{
[Dependency] private IEntityManager _entManager = default!;
[Dependency] private IGameTiming _timing = default!;
- [Dependency] private IMapManager _mapManager = default!;
[Dependency] private IRobustRandom _random = default!;
private readonly SharedAudioSystem _audio;
private readonly SharedMapSystem _maps;
@@ -311,7 +310,7 @@ private void RebuildMapObjects()
};
_mapHeadings.Add(mapComp.MapId, gridContents);
- foreach (var grid in _mapManager.GetAllGrids(mapComp.MapId))
+ foreach (var grid in _maps.GetAllGrids(mapComp.MapId))
{
_entManager.TryGetComponent(grid.Owner, out IFFComponent? iffComp);
diff --git a/Content.Client/Shuttles/UI/ShuttleDockControl.xaml.cs b/Content.Client/Shuttles/UI/ShuttleDockControl.xaml.cs
index 0a8a5a54951..8937f7e819a 100644
--- a/Content.Client/Shuttles/UI/ShuttleDockControl.xaml.cs
+++ b/Content.Client/Shuttles/UI/ShuttleDockControl.xaml.cs
@@ -18,8 +18,8 @@ namespace Content.Client.Shuttles.UI;
public sealed partial class ShuttleDockControl : BaseShuttleControl
{
[Dependency] private IGameTiming _timing = default!;
- [Dependency] private IMapManager _mapManager = default!;
private readonly DockingSystem _dockSystem;
+ private readonly SharedMapSystem _maps;
private readonly SharedShuttleSystem _shuttles;
private readonly SharedTransformSystem _xformSystem;
@@ -62,6 +62,7 @@ public ShuttleDockControl() : base(2f, 32f, 8f)
{
RobustXamlLoader.Load(this);
_dockSystem = EntManager.System();
+ _maps = EntManager.System();
_shuttles = EntManager.System();
_xformSystem = EntManager.System();
MinSize = new Vector2(SizeFull, SizeFull);
@@ -121,7 +122,7 @@ protected override void Draw(DrawingHandleScreen handle)
// Draw nearby grids
var controlBounds = PixelSizeBox;
_grids.Clear();
- _mapManager.FindGridsIntersecting(gridXform.MapID, viewBoundsWorld, ref _grids);
+ _maps.FindGridsIntersecting(gridXform.MapID, viewBoundsWorld, ref _grids);
// offset the dotted-line position to the bounds.
Vector2? viewedDockPos = _viewedState != null ? MidPointVector : null;
diff --git a/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs b/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs
index 0bcd76a2bf9..fcc2a94dcba 100644
--- a/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs
+++ b/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs
@@ -20,7 +20,7 @@ namespace Content.Client.Shuttles.UI;
[GenerateTypedNameReferences]
public sealed partial class ShuttleNavControl : BaseShuttleControl
{
- [Dependency] private IMapManager _mapManager = default!;
+ private readonly SharedMapSystem _maps;
private readonly SharedShuttleSystem _shuttles;
private readonly SharedTransformSystem _transform;
@@ -52,6 +52,7 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
public ShuttleNavControl() : base(64f, 256f, 256f)
{
RobustXamlLoader.Load(this);
+ _maps = EntManager.System();
_shuttles = EntManager.System();
_transform = EntManager.System();
}
@@ -187,7 +188,7 @@ protected override void Draw(DrawingHandleScreen handle)
var viewAABB = viewBounds.CalcBoundingBox();
_grids.Clear();
- _mapManager.FindGridsIntersecting(xform.MapID, new Box2(mapPos.Position - MaxRadarRangeVector, mapPos.Position + MaxRadarRangeVector), ref _grids, approx: true, includeMap: false);
+ _maps.FindGridsIntersecting(xform.MapID, new Box2(mapPos.Position - MaxRadarRangeVector, mapPos.Position + MaxRadarRangeVector), ref _grids, approx: true, includeMap: false);
// Draw other grids... differently
foreach (var grid in _grids)
@@ -218,14 +219,14 @@ protected override void Draw(DrawingHandleScreen handle)
{
var gridBounds = grid.Comp.LocalAABB;
+ // Display the coordinates of the center of the grid and its distance from the console.
var gridCentre = Vector2.Transform(gridBody.LocalCenter, curGridToView);
+ var gridCenterMapPos = _transform.ToWorldPosition(new EntityCoordinates(gUid, gridBody.LocalCenter));
- var gridDistance = (gridBody.LocalCenter - xform.LocalPosition).Length();
+ var gridDistance = (gridCenterMapPos - mapPos.Position).Length();
var labelText = Loc.GetString("shuttle-console-iff-label", ("name", labelName),
("distance", $"{gridDistance:0.0}"));
-
- var mapCoords = _transform.GetWorldPosition(gUid);
- var coordsText = $"({mapCoords.X:0.0}, {mapCoords.Y:0.0})";
+ var coordsText = $"({gridCenterMapPos.X:0.0}, {gridCenterMapPos.Y:0.0})";
// yes 1.0 scale is intended here.
var labelDimensions = handle.GetDimensions(Font, labelText, 1f);
diff --git a/Content.Client/Singularity/SingularityOverlay.cs b/Content.Client/Singularity/SingularityOverlay.cs
index a406025fec2..cd5ba5e23fd 100644
--- a/Content.Client/Singularity/SingularityOverlay.cs
+++ b/Content.Client/Singularity/SingularityOverlay.cs
@@ -41,7 +41,7 @@ public SingularityOverlay()
_entMan.EventBus.SubscribeEvent(EventSource.Local, this, OnProjectFromScreenToMap);
ZIndex = 101; // Should be drawn after the placement overlay so admins placing items near the singularity can tell where they're going.
- _configManager.OnValueChanged(CCVars.ReducedMotion, (b) => { _reducedMotion = b; }, invokeImmediately: true);
+ _configManager.OnValueChanged(CCVars.DisableSinguloWarp, (b) => { _reducedMotion = b; }, invokeImmediately: true);
}
private readonly Vector2[] _positions = new Vector2[MaxCount];
diff --git a/Content.Client/SprayPainter/SprayPainterSystem.cs b/Content.Client/SprayPainter/SprayPainterSystem.cs
index e4d288d7cc4..13b04141e36 100644
--- a/Content.Client/SprayPainter/SprayPainterSystem.cs
+++ b/Content.Client/SprayPainter/SprayPainterSystem.cs
@@ -60,12 +60,12 @@ private void CachePrototypes()
{
PaintableGroupsByCategory.Clear();
PaintableStylesByGroup.Clear();
- foreach (var category in Proto.EnumeratePrototypes().OrderBy(x => x.ID))
+ foreach (var category in ProtoMan.EnumeratePrototypes().OrderBy(x => x.ID))
{
var groupList = new List();
foreach (var groupId in category.Groups)
{
- if (!Proto.Resolve(groupId, out var group))
+ if (!ProtoMan.Resolve(groupId, out var group))
continue;
groupList.Add(groupId);
@@ -77,7 +77,7 @@ private void CachePrototypes()
}
Decals.Clear();
- foreach (var decalPrototype in Proto.EnumeratePrototypes().OrderBy(x => x.ID))
+ foreach (var decalPrototype in ProtoMan.EnumeratePrototypes().OrderBy(x => x.ID))
{
if (!decalPrototype.Tags.Contains("station")
&& !decalPrototype.Tags.Contains("markings")
diff --git a/Content.Client/Sprite/SpriteFadeSystem.cs b/Content.Client/Sprite/SpriteFadeSystem.cs
index 36807a17afc..eaeed98e4dd 100644
--- a/Content.Client/Sprite/SpriteFadeSystem.cs
+++ b/Content.Client/Sprite/SpriteFadeSystem.cs
@@ -1,3 +1,4 @@
+using System.Linq;
using Content.Client.Gameplay;
using Content.Shared.Sprite;
using Robust.Client.GameObjects;
@@ -78,11 +79,17 @@ private void FadeIn(float change)
{
foreach (var (mapPos, excludeBB) in _points)
{
+
+ var clickable = state.GetClickableEntities(mapPos, excludeFaded: false).ToList();
+
// Also want to handle large entities even if they may not be clickable.
- foreach (var ent in state.GetClickableEntities(mapPos, excludeFaded: false))
+ // We need to know if we're at the end of the list or not.
+ for (var i = 0; i < clickable.Count; i++)
{
+ var ent = clickable[i];
+
if (ent == player ||
- !_fadeQuery.HasComponent(ent) ||
+ !_fadeQuery.TryGetComponent(ent, out var fadeComp) ||
!_spriteQuery.TryGetComponent(ent, out var sprite) ||
sprite.DrawDepth < playerSprite.DrawDepth)
{
@@ -112,6 +119,10 @@ private void FadeIn(float change)
{
continue;
}
+
+ // If this sprite doesn't always fade, and it's at the bottom of the stack, then don't fade!
+ if (!fadeComp.AlwaysFade && i + 1 == clickable.Count)
+ break;
}
if (!_fadingQuery.TryComp(ent, out var fading))
diff --git a/Content.Client/Stealth/StealthSystem.cs b/Content.Client/Stealth/StealthSystem.cs
index f036fcc7084..d31b9984580 100644
--- a/Content.Client/Stealth/StealthSystem.cs
+++ b/Content.Client/Stealth/StealthSystem.cs
@@ -1,5 +1,4 @@
using Content.Client.Interactable.Components;
-using Content.Client.StatusIcon;
using Content.Shared.Stealth;
using Content.Shared.Stealth.Components;
using Robust.Client.GameObjects;
@@ -12,7 +11,6 @@ public sealed partial class StealthSystem : SharedStealthSystem
{
private static readonly ProtoId Shader = "Stealth";
- [Dependency] private IPrototypeManager _protoMan = default!;
[Dependency] private SharedTransformSystem _transformSystem = default!;
[Dependency] private SpriteSystem _sprite = default!;
@@ -22,7 +20,7 @@ public override void Initialize()
{
base.Initialize();
- _shader = _protoMan.Index(Shader).InstanceUnique();
+ _shader = ProtoMan.Index(Shader).InstanceUnique();
SubscribeLocalEvent(OnShutdown);
SubscribeLocalEvent(OnStartup);
diff --git a/Content.Client/Storage/Visualizers/EntityStorageVisualizerSystem.cs b/Content.Client/Storage/Visualizers/EntityStorageVisualizerSystem.cs
index 5d70d077fd9..93d47b17a6f 100644
--- a/Content.Client/Storage/Visualizers/EntityStorageVisualizerSystem.cs
+++ b/Content.Client/Storage/Visualizers/EntityStorageVisualizerSystem.cs
@@ -1,15 +1,11 @@
using Content.Shared.SprayPainter.Prototypes;
using Content.Shared.Storage;
using Robust.Client.GameObjects;
-using Robust.Shared.Prototypes;
namespace Content.Client.Storage.Visualizers;
public sealed partial class EntityStorageVisualizerSystem : VisualizerSystem
{
- [Dependency] private IPrototypeManager _prototypeManager = default!;
- [Dependency] private IComponentFactory _componentFactory = default!;
-
public override void Initialize()
{
base.Initialize();
@@ -42,13 +38,13 @@ protected override void OnAppearanceChange(EntityUid uid,
var forceRedrawBase = false;
if (AppearanceSystem.TryGetData(uid, PaintableVisuals.Prototype, out var prototype, args.Component))
{
- if (_prototypeManager.Resolve(prototype, out var proto))
+ if (ProtoMan.Resolve(prototype, out var proto))
{
- if (proto.TryGetComponent(out SpriteComponent? sprite, _componentFactory))
+ if (proto.TryComp(out SpriteComponent? sprite, Factory))
{
SpriteSystem.SetBaseRsi((uid, args.Sprite), sprite.BaseRSI);
}
- if (proto.TryGetComponent(out EntityStorageVisualsComponent? visuals, _componentFactory))
+ if (proto.TryComp(out EntityStorageVisualsComponent? visuals, Factory))
{
comp.StateBaseOpen = visuals.StateBaseOpen;
comp.StateBaseClosed = visuals.StateBaseClosed;
diff --git a/Content.Client/SubFloor/SubFloorHideSystem.cs b/Content.Client/SubFloor/SubFloorHideSystem.cs
index 64ebc97b27e..b5e056a7a03 100644
--- a/Content.Client/SubFloor/SubFloorHideSystem.cs
+++ b/Content.Client/SubFloor/SubFloorHideSystem.cs
@@ -91,7 +91,8 @@ private void OnAppearanceChanged(EntityUid uid, SubFloorHideComponent component,
{
// Allows sandbox mode to make wires visible over other stuff.
component.OriginalDrawDepth ??= args.Sprite.DrawDepth;
- _sprite.SetDrawDepth((uid, args.Sprite), (int)Shared.DrawDepth.DrawDepth.Overdoors);
+ var drawDepthDifference = Shared.DrawDepth.DrawDepth.ThickPipe - (Shared.DrawDepth.DrawDepth.Overdoors + 1);
+ _sprite.SetDrawDepth((uid, args.Sprite), args.Sprite.DrawDepth - drawDepthDifference);
}
else if (scannerRevealed)
{
@@ -99,8 +100,8 @@ private void OnAppearanceChanged(EntityUid uid, SubFloorHideComponent component,
if (component.OriginalDrawDepth is not null)
return;
component.OriginalDrawDepth = args.Sprite.DrawDepth;
- var drawDepthDifference = Shared.DrawDepth.DrawDepth.ThickPipe - Shared.DrawDepth.DrawDepth.Puddles;
- _sprite.SetDrawDepth((uid, args.Sprite), args.Sprite.DrawDepth - (drawDepthDifference - 1));
+ var drawDepthDifference = Shared.DrawDepth.DrawDepth.ThickPipe - (Shared.DrawDepth.DrawDepth.Puddles + 1);
+ _sprite.SetDrawDepth((uid, args.Sprite), args.Sprite.DrawDepth - drawDepthDifference);
}
else if (component.OriginalDrawDepth.HasValue)
{
diff --git a/Content.Client/SurveillanceCamera/UI/SurveillanceCameraMonitorBoundUi.cs b/Content.Client/SurveillanceCamera/UI/SurveillanceCameraMonitorBoundUi.cs
index 561744cf621..0c22a6631ac 100644
--- a/Content.Client/SurveillanceCamera/UI/SurveillanceCameraMonitorBoundUi.cs
+++ b/Content.Client/SurveillanceCamera/UI/SurveillanceCameraMonitorBoundUi.cs
@@ -36,7 +36,7 @@ protected override void Open()
_window.CameraDisconnect += OnCameraDisconnect;
var xform = EntMan.GetComponent(Owner);
- var gridUid = xform.GridUid ?? xform.MapUid;
+ var gridUid = xform.GridUid;
if (gridUid is not null)
_window?.SetMap(gridUid.Value);
diff --git a/Content.Client/SurveillanceCamera/UI/SurveillanceCameraMonitorWindow.xaml.cs b/Content.Client/SurveillanceCamera/UI/SurveillanceCameraMonitorWindow.xaml.cs
index 9d7ca81f088..6daee03c0c8 100644
--- a/Content.Client/SurveillanceCamera/UI/SurveillanceCameraMonitorWindow.xaml.cs
+++ b/Content.Client/SurveillanceCamera/UI/SurveillanceCameraMonitorWindow.xaml.cs
@@ -23,6 +23,7 @@ public sealed partial class SurveillanceCameraMonitorWindow : DefaultWindow
[Dependency] private IPrototypeManager _prototypeManager = default!;
[Dependency] private IResourceCache _resourceCache = default!;
[Dependency] private IEntityManager _entityManager = default!;
+ [Dependency] private IEntityNetworkManager _entityNetManager = default!;
///
/// Triggered when a camera is selected.
@@ -224,6 +225,6 @@ private void OnCameraMapSelected(NetEntity netEntity)
if (!string.IsNullOrEmpty(marker.Address))
CameraSelected?.Invoke(marker.Address, marker.Subnet);
else
- _entityManager.RaisePredictiveEvent(new RequestCameraMarkerUpdateMessage(netEntity));
+ _entityNetManager.SendSystemNetworkMessage(new RequestCameraMarkerUpdateMessage(netEntity));
}
}
diff --git a/Content.Client/SurveillanceCamera/UI/SurveillanceCameraNavMapControl.cs b/Content.Client/SurveillanceCamera/UI/SurveillanceCameraNavMapControl.cs
index ae8d4d6304f..5d806ba4581 100644
--- a/Content.Client/SurveillanceCamera/UI/SurveillanceCameraNavMapControl.cs
+++ b/Content.Client/SurveillanceCamera/UI/SurveillanceCameraNavMapControl.cs
@@ -23,7 +23,7 @@ public sealed partial class SurveillanceCameraNavMapControl : NavMapControl
private readonly Texture _invalidTexture;
private string _activeCameraAddress = string.Empty;
- private HashSet _availableSubnets = new();
+ private HashSet _availableSubnets = [];
private (Dictionary Cameras, string ActiveAddress, HashSet AvailableSubnets) _lastState;
public bool EnableCameraSelection { get; set; }
diff --git a/Content.Client/Tabletop/TabletopSystem.cs b/Content.Client/Tabletop/TabletopSystem.cs
index 8b884b31e92..73922ca0932 100644
--- a/Content.Client/Tabletop/TabletopSystem.cs
+++ b/Content.Client/Tabletop/TabletopSystem.cs
@@ -27,13 +27,12 @@ public sealed partial class TabletopSystem : SharedTabletopSystem
[Dependency] private IPlayerManager _playerManager = default!;
[Dependency] private IGameTiming _gameTiming = default!;
[Dependency] private AppearanceSystem _appearance = default!;
- [Dependency] private SharedTransformSystem _transformSystem = default!;
+ [Dependency] private SharedTransformSystem _transform = default!;
[Dependency] private SpriteSystem _sprite = default!;
// Time in seconds to wait until sending the location of a dragged entity to the server again
private const float Delay = 1f / 10; // 10 Hz
- private float _timePassed; // Time passed since last update sent to the server.
private EntityUid? _draggedEntity; // Entity being dragged
private ScalingViewport? _viewport; // Viewport currently being used
private BaseWindow? _window; // Current open tabletop window (only allow one at a time)
@@ -60,8 +59,11 @@ private void HandleDraggableRemoved(EntityUid uid, TabletopDraggableComponent co
StopDragging(false);
}
- public override void FrameUpdate(float frameTime)
+ public override void Update(float frameTime)
{
+ base.Update(frameTime);
+ if (!_gameTiming.IsFirstTimePredicted)
+ return;
if (_window == null)
return;
@@ -101,17 +103,10 @@ public override void FrameUpdate(float frameTime)
var clampedCoords = ClampPositionToViewport(coords, _viewport);
if (clampedCoords.Equals(MapCoordinates.Nullspace)) return;
- // Move the entity locally every update
- _transformSystem.SetWorldPosition(_draggedEntity.Value, clampedCoords.Position);
-
- // Increment total time passed
- _timePassed += frameTime;
-
// Only send new position to server when Delay is reached
- if (_timePassed >= Delay && _table != null)
+ if (_table != null)
{
RaisePredictiveEvent(new TabletopMoveEvent(GetNetEntity(_draggedEntity.Value), clampedCoords, GetNetEntity(_table.Value)));
- _timePassed -= Delay;
}
}
@@ -260,7 +255,7 @@ private void StopDragging(bool broadcast = true)
// Set the dragging player on the component to noone
if (broadcast && _draggedEntity != null && HasComp(_draggedEntity.Value))
{
- RaisePredictiveEvent(new TabletopMoveEvent(GetNetEntity(_draggedEntity.Value), Transforms.GetMapCoordinates(_draggedEntity.Value), GetNetEntity(_table!.Value)));
+ RaisePredictiveEvent(new TabletopMoveEvent(GetNetEntity(_draggedEntity.Value), _transform.GetMapCoordinates(_draggedEntity.Value), GetNetEntity(_table!.Value)));
RaisePredictiveEvent(new TabletopDraggingPlayerChangedEvent(GetNetEntity(_draggedEntity.Value), false));
}
diff --git a/Content.Client/Tips/TippyUI.xaml.cs b/Content.Client/Tips/TippyUI.xaml.cs
index 4a2c4bfe2f2..82b49734d38 100644
--- a/Content.Client/Tips/TippyUI.xaml.cs
+++ b/Content.Client/Tips/TippyUI.xaml.cs
@@ -23,7 +23,8 @@ public void InitLabel(PaperVisualsComponent? visuals, IResourceCache resCache)
if (visuals == null)
return;
- Label.ModulateSelfOverride = visuals.FontAccentColor;
+ if (visuals.DefaultTextColor != null)
+ Label.ModulateSelfOverride = visuals.DefaultTextColor;
if (visuals.BackgroundImagePath == null)
return;
diff --git a/Content.Client/Tips/TippyUIController.cs b/Content.Client/Tips/TippyUIController.cs
index cf5cddf6d2d..3e14f111722 100644
--- a/Content.Client/Tips/TippyUIController.cs
+++ b/Content.Client/Tips/TippyUIController.cs
@@ -143,7 +143,7 @@ private void NextState(TippyUI tippy)
paper.BackgroundImagePath = "/Textures/Interface/Paper/paper_background_default.svg.96dpi.png";
paper.BackgroundPatchMargin = new(16f, 16f, 16f, 16f);
paper.BackgroundModulate = new(255, 255, 204);
- paper.FontAccentColor = new(0, 0, 0);
+ paper.DefaultTextColor = new(0, 0, 0);
}
tippy.InitLabel(EntityManager.GetComponentOrNull(_entity), _resCache);
diff --git a/Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs b/Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs
index a547203fca3..c3c121dc2a0 100644
--- a/Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs
+++ b/Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs
@@ -464,7 +464,7 @@ public BwoinkPanel EnsurePanel(NetUserId channelId)
if (_activePanelMap.TryGetValue(channelId, out var existingPanel))
return existingPanel;
- _activePanelMap[channelId] = existingPanel = new BwoinkPanel(text => SendMessageAction?.Invoke(channelId, text, Window?.Bwoink.PlaySound.Pressed ?? true, Window?.Bwoink.AdminOnly.Pressed ?? false));
+ _activePanelMap[channelId] = existingPanel = new BwoinkPanel(text => SendMessageAction?.Invoke(channelId, text, Control?.PlaySound.Pressed ?? true, Control?.AdminOnly.Pressed ?? false));
existingPanel.InputTextChanged += text => InputTextChanged?.Invoke(channelId, text);
existingPanel.Visible = false;
if (!Control!.BwoinkArea.Children.Contains(existingPanel))
diff --git a/Content.Client/UserInterface/Systems/Chat/ChatUIController.Highlighting.cs b/Content.Client/UserInterface/Systems/Chat/ChatUIController.Highlighting.cs
index 74199c154c3..4f727350736 100644
--- a/Content.Client/UserInterface/Systems/Chat/ChatUIController.Highlighting.cs
+++ b/Content.Client/UserInterface/Systems/Chat/ChatUIController.Highlighting.cs
@@ -17,6 +17,8 @@ public sealed partial class ChatUIController : IOnSystemChanged UIManager.GetActiveUIWidgetOrNull()?.GuidebookButton;
- private ProtoId? _lastEntry;
public void OnStateEntered(LobbyState state)
{
@@ -145,7 +144,6 @@ private void OnWindowClosed()
if (_guideWindow != null)
{
_guideWindow.ReturnContainer.Visible = false;
- _lastEntry = _guideWindow.LastEntry;
}
}
@@ -183,7 +181,7 @@ public void OpenGuidebook(
if (guides == null)
{
guides = _prototypeManager.EnumeratePrototypes()
- .ToDictionary(x => new ProtoId(x.ID), x => (GuideEntry) x);
+ .ToDictionary(x => new ProtoId(x.ID), x => (GuideEntry)x);
}
else if (includeChildren)
{
@@ -197,20 +195,23 @@ public void OpenGuidebook(
if (selected == null)
{
- if (_lastEntry is { } lastEntry && guides.ContainsKey(lastEntry))
+ if (_guideWindow.Selected is { } lastEntry && guides.ContainsKey(lastEntry))
{
- selected = _lastEntry;
+ selected = lastEntry;
}
else
{
selected = _configuration.GetCVar(CCVars.DefaultGuide);
}
}
- _guideWindow.UpdateGuides(guides, rootEntries, forceRoot, selected);
+ var changed = _guideWindow.UpdateGuides(guides, rootEntries, forceRoot, selected);
// Expand up to depth-2.
- _guideWindow.Tree.SetAllExpanded(false);
- _guideWindow.Tree.SetAllExpanded(true, 1);
+ if (changed)
+ {
+ _guideWindow.Tree.SetAllExpanded(false);
+ _guideWindow.Tree.SetAllExpanded(true, 1);
+ }
_guideWindow.OpenCenteredRight();
}
diff --git a/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml b/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml
index 3b1257b44cd..0a06624eefd 100644
--- a/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml
+++ b/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml
@@ -19,7 +19,7 @@
-
+
diff --git a/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml.cs b/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml.cs
index 58f94c20ea8..16f0da7d878 100644
--- a/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml.cs
+++ b/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml.cs
@@ -1,6 +1,7 @@
using Content.Client.Items;
using Content.Shared.Hands.Components;
using Content.Shared.IdentityManagement;
+using Content.Shared.Inventory;
using Content.Shared.Inventory.VirtualItem;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
@@ -159,6 +160,13 @@ private void UpdateItemName(Hand? hand)
return;
}
+ if (_entityManager.HasComponent(_entity))
+ {
+ StatusContents.VerticalAlignment = VAlignment.Center;
+ ItemNameLabel.Visible = false;
+ return;
+ }
+
if (_entityManager.TryGetComponent(_entity, out VirtualItemComponent? virtualItem)
&& _entityManager.EntityExists(virtualItem.BlockingEntity))
{
diff --git a/Content.Client/UserInterface/Systems/Inventory/InventoryUIController.cs b/Content.Client/UserInterface/Systems/Inventory/InventoryUIController.cs
index 2ef8574e06d..f5291820d92 100644
--- a/Content.Client/UserInterface/Systems/Inventory/InventoryUIController.cs
+++ b/Content.Client/UserInterface/Systems/Inventory/InventoryUIController.cs
@@ -287,25 +287,30 @@ private void ItemPressed(GUIBoundKeyEventArgs args, SlotControl control)
return;
}
- if (args.Function == ContentKeyFunctions.ExamineEntity)
+ switch (args.Function)
{
- _inventorySystem.UIInventoryExamine(slot, _playerUid.Value);
- }
- else if (args.Function == EngineKeyFunctions.UseSecondary)
- {
- _inventorySystem.UIInventoryOpenContextMenu(slot, _playerUid.Value);
- }
- else if (args.Function == ContentKeyFunctions.ActivateItemInWorld)
- {
- _inventorySystem.UIInventoryActivateItem(slot, _playerUid.Value);
- }
- else if (args.Function == ContentKeyFunctions.AltActivateItemInWorld)
- {
- _inventorySystem.UIInventoryAltActivateItem(slot, _playerUid.Value);
- }
- else
- {
- return;
+ case var _ when args.Function == ContentKeyFunctions.ExamineEntity:
+ _inventorySystem.UIInventoryExamine(slot, _playerUid.Value);
+ break;
+
+ case var _ when args.Function == EngineKeyFunctions.UseSecondary:
+ _inventorySystem.UIInventoryOpenContextMenu(slot, _playerUid.Value);
+ break;
+
+ case var _ when args.Function == ContentKeyFunctions.ActivateItemInWorld:
+ _inventorySystem.UIInventoryActivateItem(slot, _playerUid.Value);
+ break;
+
+ case var _ when args.Function == ContentKeyFunctions.AltActivateItemInWorld:
+ _inventorySystem.UIInventoryAltActivateItem(slot, _playerUid.Value);
+ break;
+
+ case var _ when args.Function == ContentKeyFunctions.Point:
+ _inventorySystem.UIInventoryPointAt(slot, _playerUid.Value);
+ break;
+
+ default:
+ return;
}
args.Handle();
diff --git a/Content.Client/UserInterface/Systems/Storage/Controls/ItemGridPiece.cs b/Content.Client/UserInterface/Systems/Storage/Controls/ItemGridPiece.cs
index d07db112c8a..e3b60f63430 100644
--- a/Content.Client/UserInterface/Systems/Storage/Controls/ItemGridPiece.cs
+++ b/Content.Client/UserInterface/Systems/Storage/Controls/ItemGridPiece.cs
@@ -176,7 +176,10 @@ protected override void Draw(DrawingHandleScreen handle)
var sizeDifference = ((boundingGrid.Size + Vector2i.One) * _centerTexture.Size * 2 - sprite.Size) * UIScale;
- var spriteBox = new Box2Rotated(new Box2(0f, sprite.Height * scale, sprite.Width * scale, 0f), -iconRotation, Vector2.Zero);
+ var spriteBox = new Box2Rotated(
+ Box2.FromTwoPoints(Vector2.Zero, new Vector2(sprite.Width * scale, sprite.Height * scale)),
+ -iconRotation,
+ Vector2.Zero);
var root = spriteBox.CalcBoundingBox().BottomLeft;
var pos = PixelPosition * 2
+ (Parent?.GlobalPixelPosition ?? Vector2.Zero)
diff --git a/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs b/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs
index acd0c7de387..324f6ea3267 100644
--- a/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs
+++ b/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs
@@ -12,6 +12,7 @@
using Content.Shared.CCVar;
using Content.Shared.Input;
using Content.Shared.Interaction;
+using Content.Shared.Pointing;
using Content.Shared.Storage;
using Robust.Client.GameObjects;
using Robust.Client.Input;
@@ -42,6 +43,7 @@ public sealed partial class StorageUIController : UIController, IOnSystemChanged
[Dependency] private CloseRecentWindowUIController _closeRecentWindowUIController = default!;
[UISystemDependency] private readonly StorageSystem _storage = default!;
[UISystemDependency] private readonly UserInterfaceSystem _ui = default!;
+ [UISystemDependency] private readonly Pointing.PointingSystem _pointing = default!;
private readonly DragDropHelper _menuDragHelper;
@@ -282,6 +284,11 @@ private void OnPiecePressed(GUIBoundKeyEventArgs args, StorageWindow window, Ite
EntityManager.RaisePredictiveEvent(new InteractInventorySlotEvent(EntityManager.GetNetEntity(control.Entity), altInteract: true));
args.Handle();
}
+ else if (args.Function == ContentKeyFunctions.Point)
+ {
+ _pointing.TryPointAtEntity(EntityManager.GetNetEntity(control.Entity));
+ args.Handle();
+ }
window.FlagDirty();
}
diff --git a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs
index 94b994a3401..42c60e815ec 100644
--- a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs
+++ b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs
@@ -101,7 +101,7 @@ public override void Update(float frameTime)
EntityCoordinates coordinates;
- if (MapManager.TryFindGridAt(mousePos, out var gridUid, out _))
+ if (Maps.TryFindGridAt(mousePos, out var gridUid, out _))
{
coordinates = TransformSystem.ToCoordinates(gridUid, mousePos);
}
diff --git a/Content.Client/Weapons/Misc/TetherGunSystem.cs b/Content.Client/Weapons/Misc/TetherGunSystem.cs
index 276ed5352c7..88a3a208e0d 100644
--- a/Content.Client/Weapons/Misc/TetherGunSystem.cs
+++ b/Content.Client/Weapons/Misc/TetherGunSystem.cs
@@ -13,7 +13,6 @@ public sealed partial class TetherGunSystem : SharedTetherGunSystem
[Dependency] private IEyeManager _eyeManager = default!;
[Dependency] private IGameTiming _timing = default!;
[Dependency] private IInputManager _input = default!;
- [Dependency] private IMapManager _mapManager = default!;
[Dependency] private IOverlayManager _overlay = default!;
[Dependency] private IPlayerManager _player = default!;
[Dependency] private MapSystem _mapSystem = default!;
@@ -73,7 +72,7 @@ public override void Update(float frameTime)
EntityCoordinates coords;
- if (_mapManager.TryFindGridAt(mouseWorldPos, out var gridUid, out _))
+ if (_mapSystem.TryFindGridAt(mouseWorldPos, out var gridUid, out _))
{
coords = TransformSystem.ToCoordinates(gridUid, mouseWorldPos);
}
diff --git a/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs b/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs
index 3c57cf73d07..e26977c700d 100644
--- a/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs
+++ b/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs
@@ -261,3 +261,73 @@ protected override void DrawItem(DrawingHandleScreen handle, Vector2 renderPos,
handle.DrawRect(UIBox2.FromDimensions(renderPos, new Vector2(SizeH, SizeV)), color);
}
}
+
+public sealed class CustomBulletRenderer : BaseBulletRenderer
+{
+ private readonly Texture _loadedSprite;
+ private readonly Texture _spentSprite;
+
+ private const int Separation = 2;
+
+ public CustomBulletRenderer(Texture loadedSprite, Texture spentSprite, int numberOfRows)
+ {
+ _loadedSprite = loadedSprite;
+ _spentSprite = spentSprite;
+ Rows = numberOfRows;
+
+ Parameters = new LayoutParameters
+ {
+ ItemWidth = _loadedSprite.Width,
+ ItemHeight = _loadedSprite.Height,
+ ItemSeparation = _loadedSprite.Width + Separation,
+ MinCountPerRow = 3,
+ VerticalSeparation = Separation,
+ };
+ }
+
+ protected override void Draw(DrawingHandleScreen handle)
+ {
+ var itemSeparation = _loadedSprite.Width + Separation;
+ var numberItemsPerRow = (int)Math.Ceiling((double)Capacity / Rows);
+
+ // if there is more than one item per row, then we need to recalculate the separation between them.
+ if (numberItemsPerRow > 1)
+ {
+ // the item separation is the distance from the start of a item to the start of the next item,
+ // so the total horizontal size will be the item separation times the number of items - 1 + item width.
+ // the reason for the -1 and the + + item width. is that the last item doesn't have a item after it,
+ // so instead of adding the last item separation we add the item width.
+
+ // converting into a equation:
+ // total horizontal size = item separation * (number of items - 1) + width of the item
+
+ // converting to the variables:
+ // Size.X = itemSeparation * (numberItemsPerRow - 1) + _loadedSprite.Width
+ // Size.X - _loadedSprite.Width = itemSeparation * (numberItemsPerRow - 1)
+ // (Size.X - _loadedSprite.Width) / (numberItemsPerRow - 1) = itemSeparation
+ itemSeparation = (int)(Size.X - _loadedSprite.Width) / (numberItemsPerRow - 1);
+
+ // make sure the itemSeparation is less or equal to the standard item separation
+ itemSeparation = int.Min(_loadedSprite.Width + Separation, itemSeparation);
+
+ // make sure the itemSeparation is greater or equal to 1
+ itemSeparation = int.Max(1, itemSeparation);
+ }
+
+ Parameters = new LayoutParameters()
+ {
+ ItemWidth = _loadedSprite.Width,
+ ItemHeight = _loadedSprite.Height,
+ ItemSeparation = itemSeparation,
+ MinCountPerRow = 3,
+ VerticalSeparation = Separation,
+ };
+
+ base.Draw(handle);
+ }
+
+ protected override void DrawItem(DrawingHandleScreen handle, Vector2 renderPos, bool spent, bool altColor)
+ {
+ handle.DrawTexture(spent ? _spentSprite : _loadedSprite, renderPos);
+ }
+}
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs
index 49b067e3952..f9f46dfddc9 100644
--- a/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs
@@ -158,6 +158,30 @@ public void Update(int count, int max)
}
}
+ public sealed class CustomIconStatusControl : Control
+ {
+ private readonly CustomBulletRenderer _customBulletRenderer;
+
+ public CustomIconStatusControl(Texture loadedSprite, Texture spentTexture, int numberOfRows)
+ {
+ MinHeight = 15;
+ HorizontalExpand = true;
+ VerticalAlignment = VAlignment.Center;
+
+ AddChild(_customBulletRenderer = new CustomBulletRenderer(loadedSprite, spentTexture, numberOfRows)
+ {
+ HorizontalAlignment = HAlignment.Right,
+ VerticalAlignment = VAlignment.Bottom,
+ });
+ }
+
+ public void Update(int count, int capacity)
+ {
+ _customBulletRenderer.Capacity = capacity;
+ _customBulletRenderer.Count = count;
+ }
+ }
+
private sealed class ChamberMagazineStatusControl : Control
{
private readonly BulletRender _bulletRender;
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.CustomAmmoCounter.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.CustomAmmoCounter.cs
new file mode 100644
index 00000000000..0213ce1d1ee
--- /dev/null
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.CustomAmmoCounter.cs
@@ -0,0 +1,30 @@
+using Content.Shared.Weapons.Ranged.Components;
+
+namespace Content.Client.Weapons.Ranged.Systems;
+
+public sealed partial class GunSystem
+{
+ protected override void InitializeCustomAmmoCounter()
+ {
+ base.InitializeCustomAmmoCounter();
+
+ SubscribeLocalEvent(OnAmmoCountUpdate);
+ SubscribeLocalEvent(OnControl);
+ }
+
+ private void OnAmmoCountUpdate(Entity ent, ref UpdateAmmoCounterEvent args)
+ {
+ if (args.Control is not CustomIconStatusControl customIcon)
+ return;
+
+ customIcon.Update(GetAmmoCount(ent), GetAmmoCapacity(ent));
+ }
+
+ private void OnControl(Entity ent, ref AmmoCounterControlEvent args)
+ {
+ var loadedTexture = _sprite.GetFrame(ent.Comp.LoadedAmmoSprite, TimeSpan.Zero);
+ var spentTexture = _sprite.GetFrame(ent.Comp.SpentAmmoSprite, TimeSpan.Zero);
+
+ args.Control = new CustomIconStatusControl(loadedTexture, spentTexture, ent.Comp.NumberOfRows);
+ }
+}
diff --git a/Content.Client/Zombies/ZombieSystem.cs b/Content.Client/Zombies/ZombieSystem.cs
index 60c499e8739..d8053a67ce7 100644
--- a/Content.Client/Zombies/ZombieSystem.cs
+++ b/Content.Client/Zombies/ZombieSystem.cs
@@ -1,17 +1,13 @@
using System.Linq;
using Content.Shared.Body;
-using Content.Shared.Ghost;
-using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
using Content.Shared.Zombies;
using Robust.Client.GameObjects;
-using Robust.Shared.Prototypes;
namespace Content.Client.Zombies;
public sealed partial class ZombieSystem : SharedZombieSystem
{
- [Dependency] private IPrototypeManager _prototype = default!;
[Dependency] private SpriteSystem _sprite = default!;
public override void Initialize()
@@ -25,7 +21,7 @@ public override void Initialize()
private void GetZombieIcon(Entity ent, ref GetStatusIconsEvent args)
{
- var iconPrototype = _prototype.Index(ent.Comp.StatusIcon);
+ var iconPrototype = ProtoMan.Index(ent.Comp.StatusIcon);
args.StatusIcons.Add(iconPrototype);
}
@@ -34,7 +30,7 @@ private void GetInitialInfectedIcon(Entity ent, ref Ge
if (HasComp(ent))
return;
- var iconPrototype = _prototype.Index(ent.Comp.StatusIcon);
+ var iconPrototype = ProtoMan.Index(ent.Comp.StatusIcon);
args.StatusIcons.Add(iconPrototype);
}
diff --git a/Content.IntegrationTests/Fixtures/GameTest.Entities.cs b/Content.IntegrationTests/Fixtures/GameTest.Entities.cs
index 2afdeec8ef5..9f6687d98f7 100644
--- a/Content.IntegrationTests/Fixtures/GameTest.Entities.cs
+++ b/Content.IntegrationTests/Fixtures/GameTest.Entities.cs
@@ -100,12 +100,12 @@ public T CComp(EntityUid target)
}
///
- /// Attempts to retrieve the given component from an entity on the server.
+ /// Attempts to retrieve the given component from an entity on the client.
///
public bool CTryComp(EntityUid? target, [NotNullWhen(true)] out T? component)
where T : IComponent
{
- return SEntMan.TryGetComponent(target, out component);
+ return CEntMan.TryGetComponent(target, out component);
}
///
diff --git a/Content.IntegrationTests/Pair/TestPair.Helpers.cs b/Content.IntegrationTests/Pair/TestPair.Helpers.cs
index 4d02b560c97..25cb78ec356 100644
--- a/Content.IntegrationTests/Pair/TestPair.Helpers.cs
+++ b/Content.IntegrationTests/Pair/TestPair.Helpers.cs
@@ -53,6 +53,8 @@ await Server.WaitAssertion(() =>
mapData.CGridUid = ToClientUid(mapData.Grid);
mapData.CGridCoords = new EntityCoordinates(mapData.CGridUid, 0, 0);
+ TestMap = mapData;
+
return mapData;
}
diff --git a/Content.IntegrationTests/Tests/Access/ExpireIdCardTest.cs b/Content.IntegrationTests/Tests/Access/ExpireIdCardTest.cs
new file mode 100644
index 00000000000..0ec9c34c5c4
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Access/ExpireIdCardTest.cs
@@ -0,0 +1,93 @@
+#nullable enable
+using Content.IntegrationTests.Fixtures;
+using Content.IntegrationTests.Fixtures.Attributes;
+using Content.Shared.Access;
+using Content.Shared.Access.Components;
+using Content.Shared.Access.Systems;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Localization;
+using Robust.Shared.Prototypes;
+using System.Collections.Generic;
+
+namespace Content.IntegrationTests.Tests.Access
+{
+ [TestOf(typeof(ExpireIdCardComponent))]
+ public sealed class ExpireIdCardTest : GameTest
+ {
+ private const string TestExpireIdCard = "TestExpireIdCard";
+ private static readonly ProtoId GenpopEnter = "GenpopEnter";
+ private static readonly ProtoId GenpopLeave = "GenpopLeave";
+
+ [TestPrototypes]
+ private const string Prototypes = $@"
+- type: entity
+ id: {TestExpireIdCard}
+ name: Expire ID Card
+ components:
+ - type: Access
+ tags:
+ - GenpopEnter
+ - type: ExpireIdCard
+ expireMessage: genpop-prisoner-id-expire
+ expiredAccess:
+ - GenpopLeave
+";
+
+ [SidedDependency(Side.Server)] private readonly SharedIdCardSystem _sharedIdCardSystem = null!;
+
+ [Test]
+ public async Task TestExpireIdCardResetsAccessTagsWhenExpiring()
+ {
+ EntityUid ent = default;
+ ExpireIdCardComponent expireComp = default!;
+ AccessComponent accessComp = default!;
+ var expirationTimeInSeconds = 2.0f;
+ var expireTime = TimeSpan.FromSeconds(expirationTimeInSeconds);
+
+ await Pair.Server.WaitPost(() =>
+ {
+ ent = SSpawn(TestExpireIdCard);
+ expireComp = SComp(ent);
+ accessComp = SComp(ent);
+ });
+
+ // Check that default component values are all correct
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(expireComp.Expired, Is.False);
+ Assert.That(expireComp.Permanent, Is.False);
+ Assert.That(expireComp.ExpireTime, Is.EqualTo(TimeSpan.Zero));
+ Assert.That(accessComp.Tags, Is.EqualTo(new HashSet> { GenpopEnter }));
+ Assert.That(expireComp.ExpiredAccess, Is.EqualTo(new HashSet> { GenpopLeave }));
+ Assert.That(expireComp.ExpireMessage, Is.EqualTo(new LocId("genpop-prisoner-id-expire")));
+ }
+
+ // Set the expire time to the future
+ _sharedIdCardSystem.SetExpireTime(ent, expireTime);
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(expireComp.Expired, Is.False);
+ Assert.That(expireComp.Permanent, Is.False);
+ Assert.That(expireComp.ExpireTime, Is.EqualTo(expireTime));
+ Assert.That(accessComp.Tags, Is.EqualTo(new HashSet> { GenpopEnter }));
+ }
+
+ // Ensure that after just before expiry, the card has not yet expired and the access has not been replaced
+ await Pair.RunSeconds(1.0f);
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(expireComp.Expired, Is.False);
+ Assert.That(accessComp.Tags, Is.EqualTo(new HashSet> { GenpopEnter }));
+ }
+
+ // Ensure that after expiry, the card has expired and the access has been replaced
+ await Pair.RunSeconds(1.0f);
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(expireComp.Expired, Is.True);
+ Assert.That(expireComp.Permanent, Is.False);
+ Assert.That(accessComp.Tags, Is.EqualTo(new HashSet> { GenpopLeave }));
+ }
+ }
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Actions/RetractableItemActionTest.cs b/Content.IntegrationTests/Tests/Actions/RetractableItemActionTest.cs
index 26f14d73230..d1befce12c0 100644
--- a/Content.IntegrationTests/Tests/Actions/RetractableItemActionTest.cs
+++ b/Content.IntegrationTests/Tests/Actions/RetractableItemActionTest.cs
@@ -32,7 +32,7 @@ await Server.WaitAssertion(() =>
var armBladeActionProto = ProtoMan.Index(ArmBladeActionProtoId);
// Find the component
- Assert.That(armBladeActionProto.TryGetComponent(out var actionComp, SEntMan.ComponentFactory));
+ Assert.That(armBladeActionProto.TryComp(out var actionComp, SEntMan.ComponentFactory));
// Get the item protoId from the component
var spawnedProtoId = actionComp!.SpawnedPrototype;
diff --git a/Content.IntegrationTests/Tests/Administration/Logs/LogWindowTest.cs b/Content.IntegrationTests/Tests/Administration/Logs/LogWindowTest.cs
index 80236c7d90f..ec37239cef5 100644
--- a/Content.IntegrationTests/Tests/Administration/Logs/LogWindowTest.cs
+++ b/Content.IntegrationTests/Tests/Administration/Logs/LogWindowTest.cs
@@ -13,7 +13,7 @@ namespace Content.IntegrationTests.Tests.Administration.Logs;
public sealed class LogWindowTest : InteractionTest
{
- protected override PoolSettings Settings => new() { Connected = true, Dirty = true, AdminLogsEnabled = true, DummyTicker = false };
+ public override PoolSettings PoolSettings => new() { Connected = true, Dirty = true, AdminLogsEnabled = true, DummyTicker = false };
[Test]
public async Task TestAdminLogsWindow()
@@ -37,12 +37,23 @@ public async Task TestAdminLogsWindow()
var refresh = logWindow.Logs.RefreshButton;
var cont = logWindow.Logs.LogsContainer;
+ async Task SearchForLog(Guid logGuid)
+ {
+ await Client.WaitPost(() => search.Text = logGuid.ToString());
+ await ClickControl(refresh);
+
+ await RunUntilSynced();
+ await RunTicks(10);
+
+ return cont.Children
+ .Where(x => x.Visible && x is AdminLogLabel)
+ .Cast()
+ .ToArray();
+ }
+
// Search for the log we added earlier.
- await Client.WaitPost(() => search.Text = guid.ToString());
- await ClickControl(refresh);
- await RunTicks(5);
- var searchResult = cont.Children.Where(x => x.Visible && x is AdminLogLabel).Cast().ToArray();
- Assert.That(searchResult.Length, Is.EqualTo(1));
+ var searchResult = await SearchForLog(guid);
+ Assert.That(searchResult, Has.Length.EqualTo(1));
Assert.That(searchResult[0].Log.Message, Contains.Substring($" test log 1: {guid}"));
// Add a new log
@@ -50,11 +61,8 @@ public async Task TestAdminLogsWindow()
await Server.WaitPost(() => log.Add(LogType.Unknown, $"{SPlayer} test log 2: {guid}"));
// Update the search and refresh
- await Client.WaitPost(() => search.Text = guid.ToString());
- await ClickControl(refresh);
- await RunTicks(5);
- searchResult = cont.Children.Where(x => x.Visible && x is AdminLogLabel).Cast().ToArray();
- Assert.That(searchResult.Length, Is.EqualTo(1));
+ searchResult = await SearchForLog(guid);
+ Assert.That(searchResult, Has.Length.EqualTo(1));
Assert.That(searchResult[0].Log.Message, Contains.Substring($" test log 2: {guid}"));
}
}
diff --git a/Content.IntegrationTests/Tests/Administration/Notes/NotesControlTest.cs b/Content.IntegrationTests/Tests/Administration/Notes/NotesControlTest.cs
index c8dd637c055..e839c9a0f05 100644
--- a/Content.IntegrationTests/Tests/Administration/Notes/NotesControlTest.cs
+++ b/Content.IntegrationTests/Tests/Administration/Notes/NotesControlTest.cs
@@ -15,7 +15,7 @@ namespace Content.IntegrationTests.Tests.Administration.Notes;
///
public sealed class NotesControlTest : InteractionTest
{
- protected override PoolSettings Settings => new() {Connected = true, Dirty = true, AdminLogsEnabled = true, DummyTicker = false};
+ public override PoolSettings PoolSettings => new() { Connected = true, Dirty = true, AdminLogsEnabled = true, DummyTicker = false };
[Test]
public async Task TestNotesControl()
diff --git a/Content.IntegrationTests/Tests/Afk/AfkSystemTest.cs b/Content.IntegrationTests/Tests/Afk/AfkSystemTest.cs
new file mode 100644
index 00000000000..1d1addf7e20
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Afk/AfkSystemTest.cs
@@ -0,0 +1,225 @@
+#nullable enable
+using System.Reflection;
+using System.Linq;
+using Content.IntegrationTests.Fixtures;
+using Content.IntegrationTests.Fixtures.Attributes;
+using Content.Server.Afk;
+using Content.Server.GameTicking;
+using Content.Shared.Afk;
+using Content.Shared.CCVar;
+using Content.Shared.Input;
+using Robust.Server.Player;
+using Robust.Shared.Configuration;
+using Robust.Shared.Enums;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Input;
+using Robust.Shared.Map;
+using Robust.Shared.Player;
+using Robust.Shared.Timing;
+
+namespace Content.IntegrationTests.Tests.Afk;
+
+[TestOf(typeof(AFKSystem))]
+[TestOf(typeof(AfkConfirmSystem))]
+public sealed class AfkSystemTest : GameTest
+{
+ // Saves having to go through the input API on client and dealing with shenanigans.
+ private static readonly MethodInfo HandleInputCmd =
+ typeof(AFKSystem).GetMethod("HandleInputCmd", BindingFlags.Instance | BindingFlags.NonPublic)!;
+
+ private static readonly MethodInfo OnBoundUiMessageReceived =
+ typeof(AFKSystem).GetMethod("OnBoundUiMessageReceived", BindingFlags.Instance | BindingFlags.NonPublic)!;
+
+ [SidedDependency(Side.Server)] private readonly IAfkManager _afkManager = default!;
+ [SidedDependency(Side.Server)] private readonly AFKSystem _afkSystem = default!;
+ [SidedDependency(Side.Server)] private readonly AfkConfirmSystem _afkConfirm = default!;
+ [SidedDependency(Side.Server)] private readonly IPlayerManager _playerManager = default!;
+ [SidedDependency(Side.Server)] private readonly IConfigurationManager _cfg = default!;
+
+ public override PoolSettings PoolSettings => new()
+ {
+ Connected = true,
+ // Load bearing for connection I think.
+ Dirty = true,
+ DummyTicker = false,
+ };
+
+ [Test]
+ public async Task ConfirmWindowAcknowledgeStopsTimeoutKick()
+ {
+ ICommonSession? session = null;
+
+ await Server.WaitAssertion(() =>
+ {
+ session = GetSession();
+ _cfg.SetCVar(CCVars.AfkConfirmTimeout, 0.01f);
+
+ Assert.That(_afkConfirm.TryStartConfirmation(session), Is.True);
+ Assert.That(_afkConfirm.HasConfirmation(session), Is.True);
+
+ _afkConfirm.Confirm(session);
+ Assert.That(_afkConfirm.HasConfirmation(session), Is.False);
+ });
+
+ await RunTicksSync(5);
+
+ await Server.WaitAssertion(() =>
+ {
+ Assert.That(session!.Status, Is.Not.EqualTo(SessionStatus.Disconnected));
+ });
+ }
+
+ [Test]
+ public async Task ConfirmWindowTimeoutDisconnects()
+ {
+ ICommonSession? session = null;
+
+ await Server.WaitAssertion(() =>
+ {
+ session = GetSession();
+
+ _afkConfirm.AddConfirmationForTest(session, TimeSpan.Zero);
+ Assert.That(_afkConfirm.HasConfirmation(session), Is.True);
+
+ _afkConfirm.Update(0);
+ });
+
+ await RunTicksSync(5);
+
+ await Server.WaitAssertion(() =>
+ {
+ Assert.That(session!.Status, Is.EqualTo(SessionStatus.Disconnected));
+ });
+ }
+
+ [Test]
+ public async Task BoundUiInteractionResetsAfk()
+ {
+ await MakeAfk();
+
+ await Server.WaitAssertion(() =>
+ {
+ var session = GetSession();
+ var actor = session.AttachedEntity!.Value;
+ var ev = new BoundUserInterfaceMessageReceivedEvent(actor, actor, TestUiKey.Key);
+ OnBoundUiMessageReceived.Invoke(_afkSystem, [ev]);
+
+ Assert.That(_afkManager.IsAfk(session), Is.False);
+ });
+ }
+
+ [TestCase("Movement input", nameof(EngineKeyFunctions.MoveUp))]
+ [TestCase("UI", nameof(ContentKeyFunctions.OpenInventoryMenu))]
+ [TestCase("Using an item", nameof(ContentKeyFunctions.UseItemInHand))]
+ public async Task InputResetsAfk(string inputType, string keyFunctionName)
+ {
+ await MakeAfk();
+
+ await Server.WaitAssertion(() =>
+ {
+ var session = GetSession();
+ var keyFunction = GetKeyFunction(keyFunctionName);
+ var message = new FullInputCmdMessage(
+ GameTick.Zero,
+ 0,
+ 0,
+ _playerManager.KeyMap.KeyFunctionID(keyFunction),
+ BoundKeyState.Down,
+ NetCoordinates.Invalid,
+ ScreenCoordinates.Invalid);
+
+ HandleInputCmd.Invoke(_afkSystem, [message, new EntitySessionEventArgs(session)]);
+
+ Assert.That(_afkManager.IsAfk(session), Is.False, inputType);
+ });
+ }
+
+ [Test]
+ public async Task DisabledAfkTimerPreventsAfkFlagging()
+ {
+ await Server.WaitAssertion(() =>
+ {
+ var session = GetSession();
+ _cfg.SetCVar(CCVars.AfkTime, 0f);
+ _cfg.SetCVar(CCVars.AdminAfkTime, 0f);
+ _afkManager.PlayerDidAction(session);
+ });
+
+ await RunTicksSync(5);
+
+ await Server.WaitAssertion(() =>
+ {
+ var session = GetSession();
+ _afkSystem.Update(0);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(_afkManager.IsAfk(session), Is.True);
+ Assert.That(_afkConfirm.HasConfirmation(session), Is.False);
+ });
+ });
+ }
+
+ [Test]
+ public async Task DisabledAfkTimerChangeClearsExistingAfkConfirmation()
+ {
+ await Server.WaitAssertion(() =>
+ {
+ var session = GetSession();
+ _cfg.SetCVar(CCVars.AfkConfirmTimeout, 10f);
+
+ Assert.That(_afkConfirm.TryStartConfirmation(session), Is.True);
+ Assert.That(_afkConfirm.HasConfirmation(session), Is.True);
+
+ _cfg.SetCVar(CCVars.AfkTime, 0f);
+
+ Assert.That(_afkConfirm.HasConfirmation(session), Is.False);
+ });
+ }
+
+ private async Task MakeAfk()
+ {
+ await Server.WaitAssertion(() =>
+ {
+ var session = GetSession();
+ _cfg.SetCVar(CCVars.AfkTime, 0.001f);
+ _cfg.SetCVar(CCVars.AdminAfkTime, 0.001f);
+ _afkManager.PlayerDidAction(session);
+ });
+
+ await RunTicksSync(5);
+
+ await Server.WaitAssertion(() =>
+ {
+ var session = GetSession();
+ Assert.That(_afkManager.IsAfk(session), Is.True);
+ });
+ }
+
+ private ICommonSession GetSession()
+ {
+ var session = _playerManager.Sessions.Single();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(session.Status, Is.EqualTo(SessionStatus.InGame));
+ Assert.That(session.AttachedEntity, Is.Not.Null);
+ });
+
+ return session;
+ }
+
+ private static BoundKeyFunction GetKeyFunction(string fieldName)
+ {
+ var field = typeof(EngineKeyFunctions).GetField(fieldName, BindingFlags.Public | BindingFlags.Static)
+ ?? typeof(ContentKeyFunctions).GetField(fieldName, BindingFlags.Public | BindingFlags.Static);
+
+ Assert.That(field, Is.Not.Null);
+ return (BoundKeyFunction) field!.GetValue(null)!;
+ }
+
+ private enum TestUiKey : byte
+ {
+ Key,
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Atmos/GasArrayTest.cs b/Content.IntegrationTests/Tests/Atmos/GasArrayTest.cs
index eda9061281c..a70aa5d50c6 100644
--- a/Content.IntegrationTests/Tests/Atmos/GasArrayTest.cs
+++ b/Content.IntegrationTests/Tests/Atmos/GasArrayTest.cs
@@ -54,7 +54,7 @@ await server.WaitAssertion(() =>
var gasTank = prototypeManager.Index(GasTankTestDummyId);
Assert.Multiple(() =>
{
- Assert.That(gasTank.TryGetComponent(out var gasTankComponent, compFactory));
+ Assert.That(gasTank.TryComp(out var gasTankComponent, compFactory));
Assert.That(gasTankComponent!.Air.GetMoles(Gas.Oxygen), Is.EqualTo(10));
Assert.That(gasTankComponent!.Air.GetMoles(Gas.Frezon), Is.EqualTo(20));
@@ -67,7 +67,7 @@ await server.WaitAssertion(() =>
var legacyGasTank = prototypeManager.Index(GasTankLegacyTestDummyId);
Assert.Multiple(() =>
{
- Assert.That(legacyGasTank.TryGetComponent(out var gasTankComponent, compFactory));
+ Assert.That(legacyGasTank.TryComp(out var gasTankComponent, compFactory));
Assert.That(gasTankComponent!.Air.GetMoles(3), Is.EqualTo(10));
diff --git a/Content.IntegrationTests/Tests/Audio/Midi/MidiFileCollectionManagerTests.cs b/Content.IntegrationTests/Tests/Audio/Midi/MidiFileCollectionManagerTests.cs
new file mode 100644
index 00000000000..db9c0635927
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Audio/Midi/MidiFileCollectionManagerTests.cs
@@ -0,0 +1,121 @@
+#nullable enable
+
+using System.IO;
+using System.Linq;
+using Content.Client.Audio.Midi;
+using Content.IntegrationTests.Fixtures;
+using Robust.Shared.ContentPack;
+using Robust.Shared.Utility;
+
+namespace Content.IntegrationTests.Tests.Audio.Midi;
+
+[TestFixture]
+public sealed partial class MidiFileCollectionManagerTests : GameTest
+{
+ private static readonly byte[] TestBytes = [1, 2, 3, 4, 5, 6];
+ private static readonly ResPath TestFileName = new ResPath("unit_test.midi");
+ private static ResPath TestUserDataDir => new ResPath("/UserMidis/");
+ private static ResPath TestFullPath => TestUserDataDir / TestFileName;
+
+ private IResourceManager ResManager => Pair.Client.ResolveDependency();
+ private MidiFileCollectionManager MidiLibManager => Pair.Client.ResolveDependency();
+
+ [TearDown]
+ public void CleanUserData()
+ {
+ foreach (var file in ResManager.UserData.DirectoryEntries(TestUserDataDir))
+ {
+ ResManager.UserData.Delete(new ResPath(TestUserDataDir + file));
+ }
+
+ MidiLibManager.ReloadLibrary();
+ }
+
+ [Test]
+ public async Task TestAddMidiFile()
+ {
+ var addedFileName = new ResPath("");
+ Stream stream = new MemoryStream(TestBytes);
+ MidiLibManager.MidiFileAdded += s => { addedFileName = s; };
+
+ await MidiLibManager.AddMidiFile(TestFileName, stream);
+ var outputBytes = ResManager.UserData.ReadAllBytes(TestFullPath);
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(MidiLibManager.GetMidiFiles(), Contains.Item(TestFileName));
+ Assert.That(outputBytes, Is.EqualTo(TestBytes));
+ Assert.That(addedFileName, Is.EqualTo(TestFileName));
+ }
+ }
+
+ [Test]
+ public void TestGetMidiData()
+ {
+ ResManager.UserData.WriteAllBytes(TestFullPath, TestBytes);
+ var midiBytes = MidiLibManager.GetMidiData(TestFileName);
+
+ Assert.That(TestBytes, Is.EqualTo(midiBytes));
+ }
+
+ [Test]
+ public void TestRemoveMidiFile()
+ {
+ var removedFileName = new ResPath("");
+ MidiLibManager.MidiFileRemoved += s => { removedFileName = s; };
+
+ ResManager.UserData.WriteAllBytes(TestFullPath, TestBytes);
+ Assert.That(ResManager.UserData.Exists(TestFullPath), Is.True);
+
+ MidiLibManager.RemoveMidiFile(TestFileName);
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(ResManager.UserData.Exists(TestFullPath), Is.False);
+ Assert.That(MidiLibManager.GetMidiFiles(), Is.Empty);
+ Assert.That(removedFileName, Is.EqualTo(TestFileName));
+ }
+ }
+
+ [Test]
+ public async Task TestRemoveAllMidiFiles()
+ {
+ var resetFired = false;
+
+ MidiLibManager.MidiFilesReset += () => { resetFired = true; };
+ await MidiLibManager.AddMidiFile(new ResPath("1_unit_test.midi"), TestBytes);
+ await MidiLibManager.AddMidiFile(new ResPath("2_unit_test.midi"), TestBytes);
+ await MidiLibManager.AddMidiFile(new ResPath("3_unit_test.midi"), TestBytes);
+
+ Assert.That(MidiLibManager.GetMidiFiles().Count(), Is.EqualTo(3));
+
+ MidiLibManager.RemoveAllMidiFiles();
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(MidiLibManager.GetMidiFiles(), Is.Empty);
+ Assert.That(resetFired, Is.True);
+ }
+ }
+
+ [Test]
+ public void TestRenameMidiFile()
+ {
+ var renamedFileName = new ResPath("unit_test_renamed.midi");
+ var removedFileName = new ResPath("");
+ var addedFileName = new ResPath("");
+
+ MidiLibManager.MidiFileRemoved += s => { removedFileName = s; };
+ MidiLibManager.MidiFileAdded += s => { addedFileName = s; };
+
+ ResManager.UserData.WriteAllBytes(TestFullPath, TestBytes);
+ Assert.That(ResManager.UserData.Exists(TestFullPath), Is.True);
+
+ MidiLibManager.RenameMidiFile(TestFileName, renamedFileName);
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(ResManager.UserData.Exists(TestUserDataDir / renamedFileName), Is.True);
+ Assert.That(ResManager.UserData.Exists(TestFullPath), Is.False);
+ Assert.That(removedFileName, Is.EqualTo(TestFileName));
+ Assert.That(addedFileName, Is.EqualTo(renamedFileName));
+ }
+ }
+}
diff --git a/Content.IntegrationTests/Tests/CargoTest.cs b/Content.IntegrationTests/Tests/CargoTest.cs
index d76842a105d..4a9bf0f0e69 100644
--- a/Content.IntegrationTests/Tests/CargoTest.cs
+++ b/Content.IntegrationTests/Tests/CargoTest.cs
@@ -1,31 +1,40 @@
using System.Collections.Generic;
using System.Linq;
-using System.Numerics;
using Content.IntegrationTests.Fixtures;
+using Content.IntegrationTests.Fixtures.Attributes;
using Content.Server.Cargo.Components;
using Content.Server.Cargo.Systems;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.Mobs.Components;
using Content.Shared.Prototypes;
using Content.Shared.Stacks;
-using Content.Shared.Whitelist;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Map;
-using Robust.Shared.Prototypes;
using Content.Shared.Storage;
using Content.Shared.Tools.Components;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests;
-[TestFixture]
public sealed class CargoTest : GameTest
{
+ ///
+ /// will ignore all s listed here.
+ ///
private static readonly HashSet> Ignored =
[
// This is ignored because it is explicitly intended to be able to sell for more than it costs.
- new("FunCrateGambling")
+ new("FunCrateGambling"),
];
+ [SidedDependency(Side.Server)]
+ private readonly IComponentFactory _sCompFact = null!;
+
+ [SidedDependency(Side.Server)]
+ private readonly PricingSystem _sPricing = null!;
+
+ [SidedDependency(Side.Server)]
+ private readonly CargoSystem _sCargo = null!;
+
[Test]
public async Task NoCargoOrderArbitrage()
{
@@ -51,82 +60,73 @@ await server.WaitAssertion(() =>
var price = pricing.GetPrice(ent);
Assert.That(price, Is.AtMost(proto.Cost), $"Found arbitrage on {proto.ID} cargo product! Cost is {proto.Cost} but sell is {price}!");
- entManager.DeleteEntity(ent);
+ SDeleteNow(ent);
}
});
});
}
+
[Test]
public async Task NoCargoBountyArbitrageTest()
{
- var pair = Pair;
- var server = pair.Server;
+ await Pair.CreateTestMap();
+ var coordinates = Pair.TestMap!.GridCoords;
- var testMap = await pair.CreateTestMap();
-
- var entManager = server.ResolveDependency();
- var mapSystem = server.System();
- var protoManager = server.ResolveDependency();
- var cargo = entManager.System();
-
- var bounties = protoManager.EnumeratePrototypes().ToList();
-
- await server.WaitAssertion(() =>
+ await Server.WaitAssertion(() =>
{
- var mapId = testMap.MapId;
-
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
- foreach (var proto in protoManager.EnumeratePrototypes())
+ foreach (var proto in SProtoMan.EnumeratePrototypes())
{
- var ent = entManager.SpawnEntity(proto.Product, new MapCoordinates(Vector2.Zero, mapId));
+ var ent = SSpawnAtPosition(proto.Product, coordinates);
- foreach (var bounty in bounties)
+ foreach (var bounty in SProtoMan.EnumeratePrototypes())
{
- if (cargo.IsBountyComplete(ent, bounty))
- Assert.That(proto.Cost, Is.GreaterThanOrEqualTo(bounty.Reward), $"Found arbitrage on {bounty.ID} cargo bounty! Product {proto.ID} costs {proto.Cost} but fulfills bounty {bounty.ID} with reward {bounty.Reward}!");
+ if (_sCargo.IsBountyComplete(ent, bounty))
+ Assert.That(
+ proto.Cost,
+ Is.GreaterThanOrEqualTo(bounty.Reward),
+ $"Found arbitrage on {bounty.ID} cargo bounty! Product {proto.ID} costs {proto.Cost} but fulfills bounty {bounty.ID} with reward {bounty.Reward}!"
+ );
}
- entManager.DeleteEntity(ent);
+ SDeleteNow(ent);
}
- });
-
- mapSystem.DeleteMap(mapId);
+ }
});
}
[Test]
public async Task NoStaticPriceAndStackPrice()
{
- var pair = Pair;
- var server = pair.Server;
-
- var protoManager = server.ProtoMan;
- var compFact = server.ResolveDependency();
-
- await server.WaitAssertion(() =>
+ await Server.WaitAssertion(() =>
{
- var protoIds = protoManager.EnumeratePrototypes()
- .Where(p => !p.Abstract)
- .Where(p => !pair.IsTestPrototype(p))
- .Where(p => p.Components.ContainsKey("StaticPrice"))
- .ToList();
-
- foreach (var proto in protoIds)
+ using (Assert.EnterMultipleScope())
{
- // Sanity check
- Assert.That(proto.TryGetComponent(out var staticPriceComp, compFact), Is.True);
+ var protoIds = Pair.GetPrototypesWithComponent();
- if (proto.TryGetComponent(out var stackPriceComp, compFact) && stackPriceComp.Price > 0)
+ foreach (var (proto, staticPriceComp) in protoIds)
{
- Assert.That(staticPriceComp.Price, Is.EqualTo(0),
- $"The prototype {proto} has a StackPriceComponent and StaticPriceComponent whose values are not compatible with each other.");
- }
+ if (
+ proto.TryComp(out var stackPriceComp, _sCompFact)
+ && stackPriceComp.Price > 0
+ )
+ {
+ Assert.That(
+ staticPriceComp.Price,
+ Is.EqualTo(0),
+ $"The prototype {proto} has a {nameof(StackPriceComponent)} and {nameof(StaticPriceComponent)} whose values are not compatible with each other."
+ );
+ }
- if (proto.HasComponent(compFact))
- {
- Assert.That(staticPriceComp.Price, Is.EqualTo(0),
- $"The prototype {proto} has a StackComponent and StaticPriceComponent whose values are not compatible with each other.");
+ if (proto.HasComponent(_sCompFact))
+ {
+ Assert.That(
+ staticPriceComp.Price,
+ Is.EqualTo(0),
+ $"The prototype {proto} has a {nameof(StackComponent)} and {nameof(StaticPriceComponent)} whose values are not compatible with each other."
+ );
+ }
}
}
});
@@ -139,38 +139,18 @@ await server.WaitAssertion(() =>
[Test]
public async Task NoSliceableBountyArbitrageTest()
{
- var pair = Pair;
- var server = pair.Server;
-
- var testMap = await pair.CreateTestMap();
-
- var entManager = server.ResolveDependency();
- var mapSystem = server.System();
- var mapManager = server.ResolveDependency();
- var protoManager = server.ResolveDependency();
- var componentFactory = server.ResolveDependency();
- var whitelist = entManager.System();
- var cargo = entManager.System();
+ await Pair.CreateTestMap();
+ var coordinates = Pair.TestMap!.GridCoords;
- var bounties = protoManager.EnumeratePrototypes().ToList();
+ var bounties = SProtoMan.EnumeratePrototypes().ToList();
- await server.WaitAssertion(() =>
+ await Server.WaitAssertion(() =>
{
- var mapId = testMap.MapId;
- var grid = mapManager.CreateGridEntity(mapId);
- var coord = new EntityCoordinates(grid.Owner, 0, 0);
-
- var sliceableEntityProtos = protoManager.EnumeratePrototypes()
- .Where(p => !p.Abstract)
- .Where(p => !pair.IsTestPrototype(p))
- .Where(p => p.TryGetComponent(out _, componentFactory))
- .Select(p => p.ID)
- .ToList();
-
- foreach (var proto in sliceableEntityProtos)
+ var sliceableEntityProtos = Pair.GetPrototypesWithComponent();
+
+ foreach (var (proto, sliceable) in sliceableEntityProtos)
{
- var ent = entManager.SpawnEntity(proto, coord);
- var sliceable = entManager.GetComponent(ent);
+ var ent = SSpawnAtPosition(proto.ID, coordinates);
// Check each bounty
foreach (var bounty in bounties)
@@ -179,95 +159,95 @@ await server.WaitAssertion(() =>
foreach (var entry in bounty.Entries)
{
// See if the entity counts as part of this bounty entry
- if (!cargo.IsValidBountyEntry(ent, entry))
+ if (!_sCargo.IsValidBountyEntry(ent, entry))
continue;
// Spawn a slice
- var sliceCountByProtoId = EntitySpawnCollection.GetSpawns(sliceable.RefineResult)
- .GroupBy(x => x)
- .ToDictionary(x => x.Key, x => x.Count());
+ var sliceCountByProtoId = EntitySpawnCollection
+ .GetSpawns(sliceable.RefineResult)
+ .GroupBy(x => x)
+ .ToDictionary(x => x.Key, x => x.Count());
foreach (var (sliceProtoId, sliceCount) in sliceCountByProtoId)
{
- var slice = entManager.SpawnEntity(sliceProtoId, coord);
+ var slice = SSpawnAtPosition(sliceProtoId, coordinates);
// See if the slice also counts for this bounty entry
- if (!cargo.IsValidBountyEntry(slice, entry))
+ if (!_sCargo.IsValidBountyEntry(slice, entry))
{
- entManager.DeleteEntity(slice);
+ SDeleteNow(slice);
continue;
}
- entManager.DeleteEntity(slice);
+ SDeleteNow(slice);
// If for some reason it can only make one slice, that's okay, I guess
Assert.That(
sliceCount,
Is.EqualTo(1),
$"{proto} counts as part of cargo bounty {bounty.ID} "
- + $"and slices into {sliceCount} slices which count for the same bounty!"
+ + $"and slices into {sliceCount} slices which count for the same bounty!"
);
}
}
}
- entManager.DeleteEntity(ent);
+ SDeleteNow(ent);
}
- mapSystem.DeleteMap(mapId);
});
}
+ private const string StackEnt = "StackEnt";
+ private const string StackCount = "5";
+ private const string StackUnitPrice = "20";
+
[TestPrototypes]
- private const string StackProto = @"
+ private const string StackProto =
+ @$"
- type: stack
id: StackProto
name: stack-steel
- spawn: StackEnt
+ spawn: {StackEnt}
- type: entity
- id: StackEnt
+ id: {StackEnt}
components:
- type: StackPrice
- price: 20
+ price: {StackUnitPrice}
- type: Stack
stackType: StackProto
- count: 5
+ count: {StackCount}
";
[Test]
public async Task StackPrice()
{
- var pair = Pair;
- var server = pair.Server;
- var entManager = server.ResolveDependency();
-
- await server.WaitAssertion(() =>
+ await Pair.CreateTestMap();
+ var coordinates = Pair.TestMap!.GridCoords;
+ await Server.WaitAssertion(() =>
{
- var priceSystem = entManager.System();
-
- var ent = entManager.SpawnEntity("StackEnt", MapCoordinates.Nullspace);
- var price = priceSystem.GetPrice(ent);
- Assert.That(price, Is.EqualTo(100.0));
+ var ent = SSpawnAtPosition(StackEnt, coordinates);
+ var price = _sPricing.GetPrice(ent);
+ Assert.That(price, Is.EqualTo(double.Parse(StackCount) * double.Parse(StackUnitPrice)));
});
}
[Test]
public async Task MobPrice()
{
- var pair = Pair;
-
- var componentFactory = pair.Server.ResolveDependency();
-
- await pair.Server.WaitAssertion(() =>
+ await Pair.Server.WaitAssertion(() =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
- foreach (var (proto, comp) in pair.GetPrototypesWithComponent())
+ foreach (var (proto, comp) in Pair.GetPrototypesWithComponent())
{
- Assert.That(proto.TryGetComponent(out _, componentFactory), $"Found MobPriceComponent on {proto.ID}, but no MobStateComponent!");
+ Assert.That(
+ proto.TryComp(out _, _sCompFact),
+ $"Found {nameof(MobPriceComponent)} on {proto.ID}, but no {nameof(MobStateComponent)}!"
+ );
}
- });
+ }
});
}
}
diff --git a/Content.IntegrationTests/Tests/Commands/ObjectiveCommandsTest.cs b/Content.IntegrationTests/Tests/Commands/ObjectiveCommandsTest.cs
index eb18d8e1ae0..0422f1bc3db 100644
--- a/Content.IntegrationTests/Tests/Commands/ObjectiveCommandsTest.cs
+++ b/Content.IntegrationTests/Tests/Commands/ObjectiveCommandsTest.cs
@@ -21,7 +21,7 @@ public sealed class ObjectiveCommandsTest : GameTest
components:
- type: Objective
difficulty: 1
- issuer: objective-issuer-syndicate
+ issuer: TheSyndicate
icon:
sprite: error.rsi
state: error
diff --git a/Content.IntegrationTests/Tests/ContainerOcclusionTest.cs b/Content.IntegrationTests/Tests/ContainerOcclusionTest.cs
index cb7e4016018..186e0753af3 100644
--- a/Content.IntegrationTests/Tests/ContainerOcclusionTest.cs
+++ b/Content.IntegrationTests/Tests/ContainerOcclusionTest.cs
@@ -43,7 +43,6 @@ public async Task TestA()
var serverEntManager = server.ResolveDependency();
EntityUid dummy = default;
- var mapManager = server.ResolveDependency();
var map = await pair.CreateTestMap();
await server.WaitPost(() =>
@@ -83,7 +82,6 @@ public async Task TestB()
var serverEntManager = server.ResolveDependency();
EntityUid dummy = default;
- var mapManager = server.ResolveDependency();
var map = await pair.CreateTestMap();
@@ -124,7 +122,6 @@ public async Task TestAb()
var serverEntManager = server.ResolveDependency();
EntityUid dummy = default;
- var mapManager = server.ResolveDependency();
var map = await pair.CreateTestMap();
diff --git a/Content.IntegrationTests/Tests/ContrabandTest.cs b/Content.IntegrationTests/Tests/ContrabandTest.cs
index 9f0fb1d4995..02f96459c4a 100644
--- a/Content.IntegrationTests/Tests/ContrabandTest.cs
+++ b/Content.IntegrationTests/Tests/ContrabandTest.cs
@@ -25,7 +25,7 @@ await client.WaitAssertion(() =>
if (proto.Abstract || pair.IsTestPrototype(proto))
continue;
- if (!proto.TryGetComponent(out var contraband, componentFactory))
+ if (!proto.TryComp(out var contraband, componentFactory))
continue;
if (!protoMan.TryIndex(contraband.Severity, out var severity))
diff --git a/Content.IntegrationTests/Tests/Damageable/DamageAllPrototypes.cs b/Content.IntegrationTests/Tests/Damageable/DamageAllPrototypes.cs
index fceec8e6fa8..4cc7a9f7d79 100644
--- a/Content.IntegrationTests/Tests/Damageable/DamageAllPrototypes.cs
+++ b/Content.IntegrationTests/Tests/Damageable/DamageAllPrototypes.cs
@@ -1,5 +1,3 @@
-using System.Numerics;
-using System.Runtime.CompilerServices;
using Content.IntegrationTests.Fixtures;
using Content.IntegrationTests.Fixtures.Attributes;
using Content.IntegrationTests.Utility;
@@ -8,7 +6,6 @@
using Content.Shared.Damage.Prototypes;
using Content.Shared.Damage.Systems;
using Content.Shared.FixedPoint;
-using Robust.Shared.Map;
namespace Content.IntegrationTests.Tests.Damageable;
@@ -19,42 +16,60 @@ public sealed class DamageAllPrototypesTest : GameTest
{
[SidedDependency(Side.Server)] private readonly DamageableSystem _damageableSystem = default!;
- private static string[] _damageables = GameDataScrounger.EntitiesWithComponent("Damageable");
-
[Test]
[TestOf(typeof(DamageableSystem))]
- [TestCaseSource(nameof(_damageables))]
[Description("Ensures all Entity Prototypes with damageable can be damaged.")]
- public async Task TestDamageableComponents(string damageable)
+ public async Task TestDamageableComponents()
{
var map = await Pair.CreateTestMap();
- var entity = await SpawnAtPosition(damageable, map.GridCoords);
+ try
+ {
+ foreach (var damageable in GameDataScrounger.EntitiesWithComponent("Damageable"))
+ {
+ var entity = await SpawnAtPosition(damageable, map.GridCoords);
- // Intentionally cannot take damage, ignore it.
- if (SEntMan.HasComponent(entity))
- return;
+ try
+ {
+ // Intentionally cannot take damage, ignore it.
+ if (SEntMan.HasComponent(entity))
+ continue;
- var canBeDamaged = false;
+ var canBeDamaged = false;
- foreach (var type in SProtoMan.EnumeratePrototypes())
- {
- if (!_damageableSystem.CanBeDamagedBy(entity, type))
- continue;
+ foreach (var type in SProtoMan.EnumeratePrototypes())
+ {
+ if (!_damageableSystem.CanBeDamagedBy(entity, type))
+ continue;
- canBeDamaged = true;
+ canBeDamaged = true;
- await Server.WaitPost(() =>
- {
- var damage = new DamageSpecifier(type, FixedPoint2.Epsilon);
- var previousDamage = _damageableSystem.GetTotalDamage(entity);
- _damageableSystem.ChangeDamage(entity, damage, ignoreResistances: true);
- Assert.That(_damageableSystem.GetTotalDamage(entity) == FixedPoint2.Epsilon + previousDamage);
- _damageableSystem.ClearAllDamage(entity);
- });
- }
+ await Server.WaitAssertion(() =>
+ {
+ var damage = new DamageSpecifier(type, FixedPoint2.Epsilon);
+ var previousDamage = _damageableSystem.GetTotalDamage(entity);
+ _damageableSystem.ChangeDamage(entity, damage, ignoreResistances: true);
+ Assert.That(
+ _damageableSystem.GetTotalDamage(entity),
+ Is.EqualTo(FixedPoint2.Epsilon + previousDamage),
+ $"{damageable} should take {type.ID} damage.");
+
+ _damageableSystem.ClearAllDamage(entity);
+ });
+ }
- // Ensure that this entity can actually be damaged.
- Assert.That(canBeDamaged);
+ // Ensure that this entity can actually be damaged.
+ Assert.That(canBeDamaged, Is.True, $"{damageable} cannot be damaged by any damage type.");
+ }
+ finally
+ {
+ await Server.WaitPost(() => SEntMan.DeleteEntity(entity));
+ }
+ }
+ }
+ finally
+ {
+ await Server.WaitPost(() => SEntMan.DeleteEntity(map.MapUid));
+ }
}
}
diff --git a/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs b/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs
index f274e1e419e..051a015e0f6 100644
--- a/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs
+++ b/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs
@@ -101,7 +101,6 @@ public async Task TestDamageableComponents()
var server = pair.Server;
var sEntityManager = server.ResolveDependency();
- var sMapManager = server.ResolveDependency();
var sPrototypeManager = server.ResolveDependency();
var sEntitySystemManager = server.ResolveDependency();
diff --git a/Content.IntegrationTests/Tests/DeviceLinking/DeviceLinkingTest.cs b/Content.IntegrationTests/Tests/DeviceLinking/DeviceLinkingTest.cs
index 7a2a880bae5..695f16f65c7 100644
--- a/Content.IntegrationTests/Tests/DeviceLinking/DeviceLinkingTest.cs
+++ b/Content.IntegrationTests/Tests/DeviceLinking/DeviceLinkingTest.cs
@@ -34,7 +34,6 @@ public async Task DeviceLinkSinkAllPortsTest(string protoKey)
var server = pair.Server;
var protoMan = server.ProtoMan;
var compFact = server.ResolveDependency();
- var mapMan = server.ResolveDependency();
var mapSys = server.System();
var deviceLinkSys = server.System