forked from space-syndicate/space-station-14
Compare commits
372 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5aa7e2b91b | ||
|
|
49794858f2 | ||
|
|
66d45285fe | ||
|
|
d2ee564b8f | ||
|
|
333bc80458 | ||
|
|
35aebb7de3 | ||
|
|
80acc36543 | ||
|
|
18d2ddac70 | ||
|
|
ee8c0beff2 | ||
|
|
cf6469fb62 | ||
|
|
c1282f2e5e | ||
|
|
1c771c6709 | ||
|
|
7ee4e66e77 | ||
|
|
b462533f82 | ||
|
|
e88b41e637 | ||
|
|
bd135d7604 | ||
|
|
a9e4370d25 | ||
|
|
7b73811d47 | ||
|
|
ca07d6be49 | ||
|
|
e247ea5850 | ||
|
|
1a5d13fcac | ||
|
|
9d3f2219b9 | ||
|
|
09db1f5b77 | ||
|
|
f501ebec66 | ||
|
|
ab9cf3b5cc | ||
|
|
0d75154385 | ||
|
|
57e9a64d27 | ||
|
|
d2ac15c76f | ||
|
|
57ac7bbe4f | ||
|
|
897a2d40bc | ||
|
|
6df3ed9682 | ||
|
|
b14964398b | ||
|
|
8fb3e138a9 | ||
|
|
7d58e42ade | ||
|
|
cd6c521b37 | ||
|
|
c7e8bbbf87 | ||
|
|
84ca0ebe9c | ||
|
|
ab2a4ebd93 | ||
|
|
fdeb5a736d | ||
|
|
610881db82 | ||
|
|
619672a089 | ||
|
|
28a4a548b6 | ||
|
|
b723d7e49e | ||
|
|
766f429fd9 | ||
|
|
4f997f2069 | ||
|
|
5cda60f2f9 | ||
|
|
499e9f9a0f | ||
|
|
241b0930bc | ||
|
|
bd096a044b | ||
|
|
4920404994 | ||
|
|
aa8a61b6ae | ||
|
|
07076a5a32 | ||
|
|
eff710e312 | ||
|
|
17997984ac | ||
|
|
7532171090 | ||
|
|
5d929533fc | ||
|
|
4219bca74b | ||
|
|
e196d37841 | ||
|
|
d6377862c1 | ||
|
|
1f80b6a95d | ||
|
|
f702dc8f2d | ||
|
|
14b867dbe1 | ||
|
|
48cbd020a8 | ||
|
|
d857acfc07 | ||
|
|
dc47295d24 | ||
|
|
acdeac6172 | ||
|
|
6bc617ca07 | ||
|
|
e5ce73a471 | ||
|
|
b220631278 | ||
|
|
b5fb3d4bdb | ||
|
|
95496c8d2c | ||
|
|
91dd9f7be2 | ||
|
|
d326420204 | ||
|
|
7ebca1d8cc | ||
|
|
9979a08225 | ||
|
|
fb133494cc | ||
|
|
1cc0f83350 | ||
|
|
0af56cefcb | ||
|
|
6cae5d9c4a | ||
|
|
2399b61ca7 | ||
|
|
1fdc70aa3d | ||
|
|
b5f0dd81fc | ||
|
|
60e172e128 | ||
|
|
ac1870a25f | ||
|
|
c7e4f20f02 | ||
|
|
0b27da57f4 | ||
|
|
7540c8f152 | ||
|
|
3cec0aa476 | ||
|
|
c860502e66 | ||
|
|
4d1843f5e4 | ||
|
|
a18fc33724 | ||
|
|
d723054860 | ||
|
|
f8a6a79928 | ||
|
|
4ae961babb | ||
|
|
f0f0871609 | ||
|
|
69d2ddd8bf | ||
|
|
418b2b70b0 | ||
|
|
53607b8ca1 | ||
|
|
738f55c456 | ||
|
|
04bda3ad59 | ||
|
|
69330e5752 | ||
|
|
4d16565c2a | ||
|
|
ea131f7368 | ||
|
|
29c68e467a | ||
|
|
f24e1dba62 | ||
|
|
6cbd19adfa | ||
|
|
f3c40aa46c | ||
|
|
4cd5d115bf | ||
|
|
de9d8334d1 | ||
|
|
de10a3a948 | ||
|
|
84a21039d6 | ||
|
|
d06b18a8f0 | ||
|
|
7f4bc8f7d1 | ||
|
|
45b3609b8a | ||
|
|
360bfd6e1c | ||
|
|
11f308729d | ||
|
|
c9ec5e81f0 | ||
|
|
06a962559a | ||
|
|
94071a6350 | ||
|
|
71040149a1 | ||
|
|
98647f1f0f | ||
|
|
435b7d5cf8 | ||
|
|
4fafb55477 | ||
|
|
a92702e780 | ||
|
|
9338834b1b | ||
|
|
716e5ace87 | ||
|
|
b707110dea | ||
|
|
3a0049e534 | ||
|
|
5025e0d286 | ||
|
|
c0fbaf1228 | ||
|
|
f0ae5896b7 | ||
|
|
46e86149e9 | ||
|
|
5d9371931a | ||
|
|
c3d7652620 | ||
|
|
319617f6ba | ||
|
|
aad5613341 | ||
|
|
4ebdbff86b | ||
|
|
32dafcf2ea | ||
|
|
a7fc17dfc4 | ||
|
|
671eca79c1 | ||
|
|
9256f3f2a1 | ||
|
|
d7fcb03336 | ||
|
|
72b022349f | ||
|
|
96d2339345 | ||
|
|
85b3dcc9cc | ||
|
|
7586c8017a | ||
|
|
78343b2dbb | ||
|
|
51e7a39bad | ||
|
|
16c9cfe899 | ||
|
|
ec024001e7 | ||
|
|
22682fccc3 | ||
|
|
386aca70c7 | ||
|
|
8ec4669bf9 | ||
|
|
b406193372 | ||
|
|
d0e9816261 | ||
|
|
f8ff3a92aa | ||
|
|
acc95fae5e | ||
|
|
5d5c61fefc | ||
|
|
a42eb5695c | ||
|
|
e4ac948dec | ||
|
|
019268b056 | ||
|
|
28e830f8b4 | ||
|
|
617680fb87 | ||
|
|
8ef9ac26e9 | ||
|
|
ad382dcc23 | ||
|
|
80d38c51b3 | ||
|
|
37c4e15d32 | ||
|
|
f9469a5f38 | ||
|
|
1a9a79cbba | ||
|
|
e27ae3d428 | ||
|
|
7aba244b38 | ||
|
|
350c67c73e | ||
|
|
142ce2a59b | ||
|
|
da7bbe5918 | ||
|
|
6187a5a7bd | ||
|
|
74ead53ceb | ||
|
|
a9b953cdfe | ||
|
|
3633cdb537 | ||
|
|
b267bad990 | ||
|
|
95bdc66f10 | ||
|
|
8b9801a5bb | ||
|
|
8be191ab8c | ||
|
|
03b7788774 | ||
|
|
66c1a989fd | ||
|
|
41f91a9207 | ||
|
|
d65aa07a84 | ||
|
|
a8469ca509 | ||
|
|
20d1b2c6cb | ||
|
|
e3419b159e | ||
|
|
590dc948ee | ||
|
|
4ed0d37efc | ||
|
|
de672944e0 | ||
|
|
68d4cfe53b | ||
|
|
279dabd889 | ||
|
|
5207c0694a | ||
|
|
2176f00f19 | ||
|
|
c81e671a74 | ||
|
|
f92ed8418b | ||
|
|
e2baaa1c31 | ||
|
|
cdd990ba56 | ||
|
|
a287d5c3f7 | ||
|
|
ae414ac94b | ||
|
|
2a71253f57 | ||
|
|
19b1f4787f | ||
|
|
4b7aaa3a46 | ||
|
|
54d7f2b736 | ||
|
|
d3137c2d38 | ||
|
|
122feda215 | ||
|
|
71c3fa8fd7 | ||
|
|
e572d75f04 | ||
|
|
4d9c25098b | ||
|
|
62c1302a55 | ||
|
|
d3d35000e1 | ||
|
|
fa7c2be164 | ||
|
|
fe6a2f0708 | ||
|
|
3ad57230f4 | ||
|
|
f007c75794 | ||
|
|
0ab3b1a075 | ||
|
|
a5977efab6 | ||
|
|
7c28daf1ed | ||
|
|
d7219bd499 | ||
|
|
3ab84443f9 | ||
|
|
e86cc06781 | ||
|
|
4d19496dbd | ||
|
|
da4a488197 | ||
|
|
9bd24023a7 | ||
|
|
6cefe4299d | ||
|
|
bbc519c523 | ||
|
|
269bd56844 | ||
|
|
fc995820df | ||
|
|
bdbc148054 | ||
|
|
bdb710270a | ||
|
|
d366c67baf | ||
|
|
856ad11640 | ||
|
|
9754944f11 | ||
|
|
c796eb372f | ||
|
|
732db1921b | ||
|
|
4b9ef4749c | ||
|
|
e5d8800b42 | ||
|
|
e1b790eecd | ||
|
|
4f1a1118b1 | ||
|
|
6de41e8051 | ||
|
|
74d482c5b2 | ||
|
|
267357a4b0 | ||
|
|
4920c9e907 | ||
|
|
445d1b673b | ||
|
|
6e55a7bac4 | ||
|
|
67da176eb9 | ||
|
|
24005e3e93 | ||
|
|
e9932ec0ea | ||
|
|
540f4e4c61 | ||
|
|
1801f47418 | ||
|
|
4eab48fe35 | ||
|
|
2c5b67fc3f | ||
|
|
97a75f49c6 | ||
|
|
8514405ca9 | ||
|
|
360e43b9e7 | ||
|
|
dc9d4accfd | ||
|
|
cdf6461796 | ||
|
|
41234b7eb1 | ||
|
|
0b1e8a4bbc | ||
|
|
a41101e8da | ||
|
|
3cc79c223a | ||
|
|
d61ecd3d50 | ||
|
|
56462d0cb1 | ||
|
|
fb17257562 | ||
|
|
c6c84ef17d | ||
|
|
beb4d5f584 | ||
|
|
df7473a058 | ||
|
|
552938cda2 | ||
|
|
ac3a91eac1 | ||
|
|
be7653c131 | ||
|
|
131108b018 | ||
|
|
951f13fd69 | ||
|
|
077dceeb2d | ||
|
|
0dcb2756c7 | ||
|
|
4cf18a222b | ||
|
|
645c2494ec | ||
|
|
129c56544e | ||
|
|
45fa411485 | ||
|
|
e8dab47f89 | ||
|
|
01e583f500 | ||
|
|
6506c7786f | ||
|
|
662d2ee964 | ||
|
|
cf25961186 | ||
|
|
e1da70ebf7 | ||
|
|
ee2f1da8c2 | ||
|
|
cd8d5a6a9c | ||
|
|
1b3047644a | ||
|
|
589b187499 | ||
|
|
8313a4e310 | ||
|
|
339b28740a | ||
|
|
abeeb910fb | ||
|
|
2aa29de1ee | ||
|
|
8b33f4734f | ||
|
|
caebc10c5d | ||
|
|
4ff7411fb7 | ||
|
|
2182c7be70 | ||
|
|
2f0d347612 | ||
|
|
8fab0ccb58 | ||
|
|
6129fbe98e | ||
|
|
3ecc3cb295 | ||
|
|
2d77e48b4c | ||
|
|
9241325506 | ||
|
|
0444987d50 | ||
|
|
cdc0c35f3f | ||
|
|
e197b7f9ad | ||
|
|
6f38eed9d9 | ||
|
|
9212f261ea | ||
|
|
c6a4d3f7d8 | ||
|
|
7ac84d1acb | ||
|
|
dd22d58f2d | ||
|
|
9511285508 | ||
|
|
92ee561f4b | ||
|
|
ff1cba2949 | ||
|
|
428df6a58a | ||
|
|
dbda861cad | ||
|
|
503052bca7 | ||
|
|
402cc65477 | ||
|
|
229c08c560 | ||
|
|
860f1418cd | ||
|
|
ad6644afd4 | ||
|
|
d601ed5f4a | ||
|
|
9f84b24733 | ||
|
|
38d6b7a119 | ||
|
|
c47f3ca906 | ||
|
|
0ed5619e8b | ||
|
|
3a3707d2a2 | ||
|
|
5363a9f2fa | ||
|
|
cabf9d5124 | ||
|
|
760463a67a | ||
|
|
28fd00b7ea | ||
|
|
8b8f621b8c | ||
|
|
dde01f746f | ||
|
|
347a728ab7 | ||
|
|
b436e2a937 | ||
|
|
853570662e | ||
|
|
f59ef4b986 | ||
|
|
eb41d5010b | ||
|
|
787330f5c6 | ||
|
|
fab0fe14cc | ||
|
|
7750e3ca2e | ||
|
|
0a6ad5dcff | ||
|
|
f3f91e3f6b | ||
|
|
eefecdcf2f | ||
|
|
56bff9aee9 | ||
|
|
2cb8e9b7fe | ||
|
|
85060d96cf | ||
|
|
6932f28191 | ||
|
|
517b37698d | ||
|
|
79f58a0314 | ||
|
|
c179445ec9 | ||
|
|
77036e8cdd | ||
|
|
000c2e9b5d | ||
|
|
e2ef727096 | ||
|
|
dcd083a25b | ||
|
|
a9bb4921a2 | ||
|
|
2c5b023dc1 | ||
|
|
092f0f8b4a | ||
|
|
386115a575 | ||
|
|
e552736422 | ||
|
|
3266c94eac | ||
|
|
c97ffb006e | ||
|
|
444991fbd0 | ||
|
|
d88bc489ae | ||
|
|
1f2d80297c | ||
|
|
fcf8207219 | ||
|
|
2b356f64bd | ||
|
|
24887dc7d5 | ||
|
|
a21983d5aa | ||
|
|
a095c61ba4 | ||
|
|
ccc70aef07 |
@@ -351,7 +351,7 @@ resharper_csharp_qualified_using_at_nested_scope = false
|
||||
resharper_csharp_prefer_qualified_reference = false
|
||||
resharper_csharp_allow_alias = false
|
||||
|
||||
[*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets,props}]
|
||||
[*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets,props,slnx}]
|
||||
indent_size = 2
|
||||
|
||||
[nuget.config]
|
||||
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -14,7 +14,7 @@
|
||||
Небольшие исправления/рефакторинг освобождаются от этого требования. -->
|
||||
|
||||
## Требования
|
||||
<!-- Подтвердите следующее, поставив X в скобках [X]: -->
|
||||
<!-- Подтвердите следующее, поставив X в скобках без пробелов [X]: -->
|
||||
- [ ] Я прочитал(а) и следую [Рекомендациям по оформлению Pull Request и Changelog](https://docs.spacestation14.com/en/general-development/codebase-info/pull-request-guidelines.html).
|
||||
- [ ] Я добавил(а) медиафайлы к этому PR или он не требует демонстрации в игре.
|
||||
<!-- Вы должны понимать, что несоблюдение вышеуказанного может привести к закрытию вашего PR по усмотрению сопровождающего -->
|
||||
|
||||
15
.github/actions/cache-dotnet/action.yml
vendored
15
.github/actions/cache-dotnet/action.yml
vendored
@@ -1,15 +0,0 @@
|
||||
name: Cache .NET dependencies
|
||||
description: Cache NuGet packages using Gitea Actions cache server
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
ACTIONS_CACHE_URL: ${{ vars.ACTIONS_CACHE_URL }}
|
||||
with:
|
||||
path: |
|
||||
~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nuget-
|
||||
56
.github/workflows/benchmarks.yml
vendored
56
.github/workflows/benchmarks.yml
vendored
@@ -1,5 +1,4 @@
|
||||
name: Benchmarks
|
||||
|
||||
name: Benchmarks
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
@@ -11,31 +10,38 @@ jobs:
|
||||
benchmark:
|
||||
name: Run Benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Install Dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Run Benchmarks
|
||||
env:
|
||||
ROBUST_BENCHMARKS_ENABLE_SQL: "1"
|
||||
ROBUST_BENCHMARKS_SQL_ADDRESS: ${{ secrets.BENCHMARKS_SQL_HOST }}
|
||||
ROBUST_BENCHMARKS_SQL_PORT: ${{ secrets.BENCHMARKS_SQL_PORT }}
|
||||
ROBUST_BENCHMARKS_SQL_USER: ${{ secrets.BENCHMARKS_SQL_USER }}
|
||||
ROBUST_BENCHMARKS_SQL_PASSWORD: ${{ secrets.BENCHMARKS_SQL_PASSWORD }}
|
||||
ROBUST_BENCHMARKS_SQL_DATABASE: ${{ secrets.BENCHMARKS_SQL_DATABASE }}
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
- name: Get Engine version
|
||||
run: |
|
||||
cd Content.Benchmarks
|
||||
dotnet run --filter '*' --configuration Release
|
||||
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 }}
|
||||
|
||||
4
.github/workflows/build-docfx.yml
vendored
4
.github/workflows/build-docfx.yml
vendored
@@ -7,8 +7,6 @@ on:
|
||||
jobs:
|
||||
docfx:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- name: Setup submodule
|
||||
@@ -23,7 +21,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
4
.github/workflows/build-map-renderer.yml
vendored
4
.github/workflows/build-map-renderer.yml
vendored
@@ -16,8 +16,6 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
|
||||
steps:
|
||||
- name: Checkout Master
|
||||
@@ -38,7 +36,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
14
.github/workflows/build-test-debug.yml
vendored
14
.github/workflows/build-test-debug.yml
vendored
@@ -16,19 +16,11 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
|
||||
steps:
|
||||
- name: Checkout Master
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Delete Wylab override files (duplicates upstream for customization)
|
||||
run: |
|
||||
rm -f Resources/Prototypes/_Wylab/GameRules/events.yml
|
||||
rm -f Resources/Prototypes/_Wylab/GameRules/pests.yml
|
||||
rm -f Resources/Prototypes/_Wylab/GameRules/subgamemodes.yml
|
||||
|
||||
- name: Setup Submodule
|
||||
run: |
|
||||
git submodule update --init --recursive
|
||||
@@ -44,7 +36,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
@@ -56,8 +48,10 @@ jobs:
|
||||
run: dotnet test --no-build --configuration DebugOpt Content.Tests/Content.Tests.csproj -- NUnit.ConsoleOut=0
|
||||
|
||||
- name: Run Content.IntegrationTests
|
||||
shell: pwsh
|
||||
run: |
|
||||
DOTNET_gcServer=1 dotnet test --no-build --configuration DebugOpt Content.IntegrationTests/Content.IntegrationTests.csproj -- NUnit.ConsoleOut=0 NUnit.MapWarningTo=Failed
|
||||
$env:DOTNET_gcServer=1
|
||||
dotnet test --no-build --configuration DebugOpt Content.IntegrationTests/Content.IntegrationTests.csproj -- NUnit.ConsoleOut=0 NUnit.MapWarningTo=Failed
|
||||
ci-success:
|
||||
name: Build & Test Debug
|
||||
needs:
|
||||
|
||||
14
.github/workflows/labeler-approve.yml
vendored
14
.github/workflows/labeler-approve.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: "Labels: Approve"
|
||||
name: "Labels: Approve"
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
@@ -11,10 +11,8 @@ jobs:
|
||||
if: github.event.review.state == 'approved'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Remove review labels
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
API="${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels"
|
||||
curl -sS -X DELETE -H "Authorization: token $GITHUB_TOKEN" "$API/Status%3A%20Needs%20Review" || true
|
||||
curl -sS -X DELETE -H "Authorization: token $GITHUB_TOKEN" "$API/Status%3A%20Awaiting%20Changes" || true
|
||||
- uses: actions-ecosystem/action-remove-labels@v1
|
||||
with:
|
||||
labels: |
|
||||
Status: Needs Review
|
||||
Status: Awaiting Changes
|
||||
|
||||
16
.github/workflows/labeler-changes.yml
vendored
16
.github/workflows/labeler-changes.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: "Labels: Changes"
|
||||
name: "Labels: Changes"
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
@@ -11,11 +11,9 @@ jobs:
|
||||
if: github.event.review.state == 'changes_requested'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Update labels
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
API="${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels"
|
||||
curl -sS -X POST -H "Authorization: token $GITHUB_TOKEN" -H "Content-Type: application/json" \
|
||||
-d '{"labels":["Status: Awaiting Changes"]}' "$API"
|
||||
curl -sS -X DELETE -H "Authorization: token $GITHUB_TOKEN" "$API/Status%3A%20Needs%20Review" || true
|
||||
- uses: actions-ecosystem/action-add-labels@v1
|
||||
with:
|
||||
labels: "Status: Awaiting Changes"
|
||||
- uses: actions-ecosystem/action-remove-labels@v1
|
||||
with:
|
||||
labels: "Status: Needs Review"
|
||||
|
||||
61
.github/workflows/labeler-conflict.yml
vendored
61
.github/workflows/labeler-conflict.yml
vendored
@@ -9,58 +9,13 @@ on:
|
||||
- ready_for_review
|
||||
|
||||
jobs:
|
||||
check-conflicts:
|
||||
if: github.event.pull_request.draft == false && github.actor != 'IanComradeBot'
|
||||
Label:
|
||||
if: ( github.event.pull_request.draft == false ) && ( github.actor != 'IanComradeBot' )
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check mergeable status and label
|
||||
env:
|
||||
API_TOKEN: ${{ secrets.API_TOKEN }}
|
||||
run: |
|
||||
PR_INDEX=${{ github.event.pull_request.number }}
|
||||
REPO_OWNER=${{ github.repository_owner }}
|
||||
REPO_NAME=${{ github.event.repository.name }}
|
||||
API_URL="${{ github.server_url }}/api/v1"
|
||||
|
||||
# Get PR mergeable status
|
||||
PR_DATA=$(curl -s -H "Authorization: token $API_TOKEN" \
|
||||
"$API_URL/repos/$REPO_OWNER/$REPO_NAME/pulls/$PR_INDEX")
|
||||
MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable')
|
||||
|
||||
echo "PR #$PR_INDEX mergeable status: $MERGEABLE"
|
||||
|
||||
LABEL_NAME="S: Merge Conflict"
|
||||
|
||||
if [ "$MERGEABLE" = "false" ]; then
|
||||
echo "PR has merge conflicts, adding label and comment..."
|
||||
|
||||
# Add label
|
||||
curl -s -X POST -H "Authorization: token $API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$API_URL/repos/$REPO_OWNER/$REPO_NAME/issues/$PR_INDEX/labels" \
|
||||
-d "{\"labels\":[\"$LABEL_NAME\"]}"
|
||||
|
||||
# Add comment
|
||||
curl -s -X POST -H "Authorization: token $API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$API_URL/repos/$REPO_OWNER/$REPO_NAME/issues/$PR_INDEX/comments" \
|
||||
-d '{"body":"This pull request has conflicts, please resolve those before we can evaluate the pull request."}'
|
||||
|
||||
echo "Label and comment added."
|
||||
else
|
||||
echo "PR is mergeable, no conflicts detected."
|
||||
|
||||
# Check if label exists and remove it
|
||||
LABELS=$(curl -s -H "Authorization: token $API_TOKEN" \
|
||||
"$API_URL/repos/$REPO_OWNER/$REPO_NAME/issues/$PR_INDEX/labels")
|
||||
HAS_LABEL=$(echo "$LABELS" | jq -r ".[] | select(.name == \"$LABEL_NAME\") | .id")
|
||||
|
||||
if [ -n "$HAS_LABEL" ]; then
|
||||
echo "Removing stale conflict label..."
|
||||
# URL-encode the label name (handles spaces, colons, etc.)
|
||||
LABEL_NAME_ENCODED=$(echo "$LABEL_NAME" | jq -rR @uri)
|
||||
curl -s -X DELETE -H "Authorization: token $API_TOKEN" \
|
||||
"$API_URL/repos/$REPO_OWNER/$REPO_NAME/issues/$PR_INDEX/labels/$LABEL_NAME_ENCODED"
|
||||
echo "Conflict label removed."
|
||||
fi
|
||||
fi
|
||||
- name: Check for Merge Conflicts
|
||||
uses: eps1lon/actions-label-merge-conflict@v3.0.0
|
||||
with:
|
||||
dirtyLabel: "S: Merge Conflict"
|
||||
repoToken: "${{ secrets.GITHUB_TOKEN }}"
|
||||
commentOnDirty: "This pull request has conflicts, please resolve those before we can evaluate the pull request."
|
||||
|
||||
16
.github/workflows/labeler-needsreview.yml
vendored
16
.github/workflows/labeler-needsreview.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: "Labels: Review"
|
||||
name: "Labels: Review"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
@@ -8,11 +8,9 @@ jobs:
|
||||
add_label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Update labels
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
API="${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels"
|
||||
curl -sS -X POST -H "Authorization: token $GITHUB_TOKEN" -H "Content-Type: application/json" \
|
||||
-d '{"labels":["S: Needs Review"]}' "$API"
|
||||
curl -sS -X DELETE -H "Authorization: token $GITHUB_TOKEN" "$API/S%3A%20Awaiting%20Changes" || true
|
||||
- uses: actions-ecosystem/action-add-labels@v1
|
||||
with:
|
||||
labels: "S: Needs Review"
|
||||
- uses: actions-ecosystem/action-remove-labels@v1
|
||||
with:
|
||||
labels: "S: Awaiting Changes"
|
||||
|
||||
3
.github/workflows/labeler-pr.yml
vendored
3
.github/workflows/labeler-pr.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: "Labels: PR"
|
||||
name: "Labels: PR"
|
||||
|
||||
on:
|
||||
- pull_request_target
|
||||
@@ -11,5 +11,4 @@ jobs:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/labeler@v5
|
||||
|
||||
23
.github/workflows/labeler-review.yml
vendored
Normal file
23
.github/workflows/labeler-review.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: "Labels: Approved"
|
||||
on:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
jobs:
|
||||
add_label:
|
||||
# Change the repository name after you've made sure the team name is correct for your fork!
|
||||
if: ${{ (github.repository == 'space-wizards/space-station-14') && (github.event.review.state == 'APPROVED') }}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: tspascoal/get-user-teams-membership@v3
|
||||
id: checkUserMember
|
||||
with:
|
||||
username: ${{ github.actor }}
|
||||
team: "content-maintainers,junior-maintainers"
|
||||
GITHUB_TOKEN: ${{ secrets.LABELER_PAT }}
|
||||
- if: ${{ steps.checkUserMember.outputs.isTeamMember == 'true' }}
|
||||
uses: actions-ecosystem/action-add-labels@v1
|
||||
with:
|
||||
labels: "S: Approved"
|
||||
12
.github/workflows/labeler-stable.yml
vendored
12
.github/workflows/labeler-stable.yml
vendored
@@ -11,12 +11,6 @@ jobs:
|
||||
add_label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Add branch label
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
curl -sS -X POST \
|
||||
-H "Authorization: token $GITHUB_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"labels":["Branch: Stable"]}' \
|
||||
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels"
|
||||
- uses: actions-ecosystem/action-add-labels@v1
|
||||
with:
|
||||
labels: "Branch: Stable"
|
||||
|
||||
12
.github/workflows/labeler-staging.yml
vendored
12
.github/workflows/labeler-staging.yml
vendored
@@ -11,12 +11,6 @@ jobs:
|
||||
add_label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Add branch label
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
curl -sS -X POST \
|
||||
-H "Authorization: token $GITHUB_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"labels":["Branch: Staging"]}' \
|
||||
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels"
|
||||
- uses: actions-ecosystem/action-add-labels@v1
|
||||
with:
|
||||
labels: "Branch: Staging"
|
||||
|
||||
17
.github/workflows/labeler-untriaged.yml
vendored
17
.github/workflows/labeler-untriaged.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: "Labels: Untriaged"
|
||||
name: "Labels: Untriaged"
|
||||
|
||||
on:
|
||||
issues:
|
||||
@@ -10,14 +10,7 @@ jobs:
|
||||
add_label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Add untriaged label
|
||||
if: github.event.issue.labels[0] == null || github.event.pull_request.labels[0] == null
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
NUMBER="${{ github.event.pull_request.number || github.event.issue.number }}"
|
||||
curl -sS -X POST \
|
||||
-H "Authorization: token $GITHUB_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"labels":["S: Untriaged"]}' \
|
||||
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/$NUMBER/labels"
|
||||
- uses: actions-ecosystem/action-add-labels@v1
|
||||
if: join(github.event.issue.labels) == ''
|
||||
with:
|
||||
labels: "S: Untriaged"
|
||||
|
||||
5
.github/workflows/publish-public.yml
vendored
5
.github/workflows/publish-public.yml
vendored
@@ -11,8 +11,6 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
|
||||
steps:
|
||||
# - name: Install dependencies
|
||||
@@ -44,9 +42,6 @@ jobs:
|
||||
- name: Package client
|
||||
run: dotnet run --project Content.Packaging client --no-wipe-release
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: pip install --break-system-packages requests
|
||||
|
||||
- name: Publish version
|
||||
run: Tools/publish_multi_request.py
|
||||
env:
|
||||
|
||||
7
.github/workflows/publish-testing.yml
vendored
7
.github/workflows/publish-testing.yml
vendored
@@ -12,8 +12,6 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3.6.0
|
||||
@@ -22,7 +20,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Get Engine Tag
|
||||
run: |
|
||||
@@ -41,9 +39,6 @@ jobs:
|
||||
- name: Package client
|
||||
run: dotnet run --project Content.Packaging client --no-wipe-release
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: pip install --break-system-packages requests
|
||||
|
||||
- name: Publish version
|
||||
run: Tools/publish_multi_request.py --fork-id wizards-testing
|
||||
env:
|
||||
|
||||
87
.github/workflows/publish.yml
vendored
87
.github/workflows/publish.yml
vendored
@@ -6,17 +6,11 @@ concurrency:
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
schedule:
|
||||
- cron: '0 1 * * *'
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CDN_MANIFEST_URL: https://cdn.wylab.me/fork/wylab/manifest
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
steps:
|
||||
- name: Fail if we are attempting to run on the master branch
|
||||
if: ${{GITHUB.REF_NAME == 'master' && github.repository == 'space-wizards/space-station-14'}}
|
||||
@@ -29,35 +23,7 @@ jobs:
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Check if build already published
|
||||
id: cdn-check
|
||||
run: |
|
||||
SHA=$(echo "$GITHUB_SHA" | tr '[:upper:]' '[:lower:]')
|
||||
if curl -sSf "$CDN_MANIFEST_URL" | jq -e ".builds[\"$SHA\"]" > /dev/null 2>&1; then
|
||||
echo "Build $SHA already present on CDN; skipping."
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Build $SHA not found on CDN; continuing."
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'global.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nuget-
|
||||
|
||||
- name: Cache RobustToolbox build output
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: RobustToolbox/bin
|
||||
key: ${{ runner.os }}-robust-${{ hashFiles('RobustToolbox/**/*.csproj', 'global.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-robust-
|
||||
|
||||
# Wylab-Secrets-Start
|
||||
# Corvax-Secrets-Start
|
||||
- name: Setup secrets
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.SECRETS_PRIVATE_KEY }}
|
||||
@@ -66,69 +32,46 @@ jobs:
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.SECRETS_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
printf '%s\n' 'Host git.wylab.me' ' Hostname git.wylab.me' ' Port 22' ' User git' ' IdentityFile ~/.ssh/id_rsa' ' StrictHostKeyChecking no' ' UserKnownHostsFile /dev/null' > ~/.ssh/config
|
||||
chmod 600 ~/.ssh/config
|
||||
git clone git@git.wylab.me:wylab/secrets.git Secrets
|
||||
[ -d Secrets/Resources/Prototypes ] && cp -R Secrets/Resources/Prototypes Resources/Prototypes/WylabSecrets
|
||||
[ -d Secrets/Resources/ServerPrototypes ] && cp -R Secrets/Resources/ServerPrototypes Resources/Prototypes/WylabSecretsServer
|
||||
[ -d Secrets/Resources/Locale ] && cp -R Secrets/Resources/Locale Resources/Locale/ru-RU/wylab-secrets
|
||||
[ -d Secrets/Resources/Textures ] && cp -R Secrets/Resources/Textures Resources/Textures/WylabSecrets
|
||||
[ -d Secrets/Resources/Audio ] && cp -R Secrets/Resources/Audio Resources/Audio/WylabSecrets
|
||||
# Wylab-Secrets-End
|
||||
echo "HOST *" > ~/.ssh/config
|
||||
echo " Hostname github.com" >> ~/.ssh/config
|
||||
echo " Port 22" >> ~/.ssh/config
|
||||
echo " StrictHostKeyChecking no" >> ~/.ssh/config
|
||||
git clone git@github.com:corvax-nexus/secrets.git Secrets
|
||||
cp -R Secrets/Resources/Prototypes Resources/Prototypes/CorvaxSecrets
|
||||
cp -R Secrets/Resources/ServerPrototypes Resources/Prototypes/CorvaxSecretsServer
|
||||
cp -R Secrets/Resources/Locale Resources/Locale/ru-RU/corvax-secrets
|
||||
cp -R Secrets/Resources/Textures Resources/Textures/CorvaxSecrets
|
||||
cp -R Secrets/Resources/Audio Resources/Audio/CorvaxSecrets
|
||||
# Corvax-Secrets-End
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Get Engine Tag
|
||||
run: |
|
||||
cd RobustToolbox
|
||||
git fetch --depth=1
|
||||
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
|
||||
|
||||
- name: Build Packaging
|
||||
run: dotnet build Content.Packaging --configuration Release --no-restore /m
|
||||
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
|
||||
|
||||
- name: Package server
|
||||
run: dotnet run --project Content.Packaging server --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64
|
||||
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
|
||||
|
||||
- name: Package client
|
||||
run: dotnet run --project Content.Packaging client --no-wipe-release
|
||||
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
|
||||
|
||||
- name: Publish version
|
||||
run: Tools/publish_multi_request.py
|
||||
env:
|
||||
PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
|
||||
GITHUB_REPOSITORY: wylab/wylab-station-14
|
||||
FORK_ID: wylab
|
||||
ROBUST_CDN_URL: https://cdn.wylab.me/
|
||||
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
|
||||
|
||||
- name: Trigger Docker image rebuild
|
||||
if: ${{ success() && steps.cdn-check.outputs.skip != 'true' }}
|
||||
env:
|
||||
DISPATCH_TOKEN: ${{ secrets.DOCKER_TRIGGER_TOKEN }}
|
||||
TARGET_REPO: wylab/WS14-Docker-Linux-Server
|
||||
PAYLOAD: ${{ github.sha }}
|
||||
run: |
|
||||
if [ -z "${DISPATCH_TOKEN}" ]; then
|
||||
echo "No DOCKER_TRIGGER_TOKEN configured; skipping dispatch."
|
||||
exit 0
|
||||
fi
|
||||
curl -sSL -X POST \
|
||||
-H "Authorization: token ${DISPATCH_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
https://git.wylab.me/api/v1/repos/${TARGET_REPO}/actions/workflows/main.yml/dispatches \
|
||||
-d "{\"ref\":\"main\",\"inputs\":{\"commit\":\"${PAYLOAD}\"}}"
|
||||
GITHUB_REPOSITORY: ${{ vars.GITHUB_REPOSITORY }}
|
||||
FORK_ID: ${{ vars.FORK_ID }}
|
||||
|
||||
# - name: Publish changelog (Discord)
|
||||
# continue-on-error: true
|
||||
|
||||
69
.github/workflows/rsi-diff.yml
vendored
Normal file
69
.github/workflows/rsi-diff.yml
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
name: Diff RSIs
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '**.rsi/**.png'
|
||||
|
||||
jobs:
|
||||
diff:
|
||||
name: Diff
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Get changed files
|
||||
id: files
|
||||
uses: Ana06/get-changed-files@v2.3.0
|
||||
with:
|
||||
format: 'space-delimited'
|
||||
filter: |
|
||||
**.rsi
|
||||
**.png
|
||||
|
||||
- name: Diff changed RSIs
|
||||
id: diff
|
||||
uses: space-wizards/RSIDiffBot@v1.1
|
||||
with:
|
||||
modified: ${{ steps.files.outputs.modified }}
|
||||
removed: ${{ steps.files.outputs.removed }}
|
||||
added: ${{ steps.files.outputs.added }}
|
||||
basename: ${{ github.event.pull_request.base.repo.full_name }}
|
||||
basesha: ${{ github.event.pull_request.base.sha }}
|
||||
headname: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
headsha: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Potentially find comment
|
||||
uses: peter-evans/find-comment@v1
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ github.event.number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: RSI Diff Bot
|
||||
|
||||
- name: Create comment if it doesn't exist
|
||||
if: steps.fc.outputs.comment-id == ''
|
||||
uses: peter-evans/create-or-update-comment@v1
|
||||
with:
|
||||
issue-number: ${{ github.event.number }}
|
||||
body: |
|
||||
${{ steps.diff.outputs.summary-details }}
|
||||
|
||||
- name: Update comment if it exists
|
||||
if: steps.fc.outputs.comment-id != ''
|
||||
uses: peter-evans/create-or-update-comment@v1
|
||||
with:
|
||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
${{ steps.diff.outputs.summary-details }}
|
||||
|
||||
- name: Update comment to read that it has been edited
|
||||
if: steps.fc.outputs.comment-id != ''
|
||||
uses: peter-evans/create-or-update-comment@v1
|
||||
with:
|
||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
edit-mode: append
|
||||
body: |
|
||||
Edit: diff updated after ${{ github.event.pull_request.head.sha }}
|
||||
14
.github/workflows/test-packaging.yml
vendored
14
.github/workflows/test-packaging.yml
vendored
@@ -31,8 +31,6 @@ jobs:
|
||||
name: Test Packaging
|
||||
if: github.actor != 'IanComradeBot' && github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
|
||||
steps:
|
||||
- name: Checkout Master
|
||||
@@ -50,24 +48,24 @@ jobs:
|
||||
cd RobustToolbox/
|
||||
git submodule update --init --recursive
|
||||
|
||||
# Wylab-Secrets-Start
|
||||
# Corvax-Secrets-Start
|
||||
- name: Setup secrets
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.SECRETS_PRIVATE_KEY }}
|
||||
if: ${{ env.SSH_KEY != '' }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
mkdir ~/.ssh
|
||||
echo "${{ secrets.SECRETS_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
echo "HOST git.wylab.me" > ~/.ssh/config
|
||||
echo " StrictHostKeyChecking no" >> ~/.ssh/config
|
||||
echo "HOST *" > ~/.ssh/config
|
||||
echo "StrictHostKeyChecking no" >> ~/.ssh/config
|
||||
git -c submodule.Secrets.update=checkout submodule update --init
|
||||
# Wylab-Secrets-End
|
||||
# Corvax-Secrets-End
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
95
.github/workflows/update-wiki.yml
vendored
95
.github/workflows/update-wiki.yml
vendored
@@ -3,24 +3,19 @@ name: Update Wiki
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ master, jsondump ]
|
||||
paths:
|
||||
- '.github/workflows/update-wiki.yml'
|
||||
- 'Content.Shared/Chemistry/**.cs'
|
||||
- 'Content.Server/Chemistry/**.cs'
|
||||
- 'Content.Server/GuideGenerator/**.cs'
|
||||
- 'Content.Server/Corvax/GuideGenerator/**.cs'
|
||||
- 'Resources/Prototypes/Reagents/**.yml'
|
||||
- 'Resources/Prototypes/Chemistry/**.yml'
|
||||
- 'Resources/Prototypes/Recipes/Reactions/**.yml'
|
||||
- 'Content.Shared/**'
|
||||
- 'Content.Server/**'
|
||||
- 'Content.Client/**'
|
||||
- 'Resources/**'
|
||||
- 'RobustToolbox/'
|
||||
|
||||
jobs:
|
||||
update-wiki:
|
||||
name: Build and Publish JSON blobs to wiki
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
|
||||
steps:
|
||||
- name: Checkout Master
|
||||
@@ -53,42 +48,50 @@ jobs:
|
||||
run: dotnet ./bin/Content.Server/Content.Server.dll --cvar autogen.destination_file=prototypes.json
|
||||
continue-on-error: true
|
||||
|
||||
- name: Upload chem_prototypes to wiki
|
||||
uses: jtmullen/mediawiki-edit-action@v0.1.1
|
||||
with:
|
||||
wiki_text_file: ./bin/Content.Server/data/chem_prototypes.json
|
||||
edit_summary: Update chem_prototypes.json via GitHub Actions
|
||||
page_name: ${{ secrets.WIKI_PAGE_ROOT }}/chem_prototypes.json
|
||||
api_url: https://wiki.wylab.me/api.php
|
||||
username: ${{ secrets.WIKI_BOT_USER }}
|
||||
password: ${{ secrets.WIKI_BOT_PASS }}
|
||||
# Проходит по всем JSON-файлам в директории BASE и загружает каждый файл как страницу в MediaWiki.
|
||||
# Имя страницы формируется из относительного пути к файлу.
|
||||
- name: Upload JSON files to wiki
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
- name: Upload react_prototypes to wiki
|
||||
uses: jtmullen/mediawiki-edit-action@v0.1.1
|
||||
with:
|
||||
wiki_text_file: ./bin/Content.Server/data/react_prototypes.json
|
||||
edit_summary: Update react_prototypes.json via GitHub Actions
|
||||
page_name: ${{ secrets.WIKI_PAGE_ROOT }}/react_prototypes.json
|
||||
api_url: https://wiki.wylab.me/api.php
|
||||
username: ${{ secrets.WIKI_BOT_USER }}
|
||||
password: ${{ secrets.WIKI_BOT_PASS }}
|
||||
BASE="./bin/Content.Server/data"
|
||||
ROOT="${{ secrets.WIKI_PAGE_ROOT }}"
|
||||
API="${{ secrets.WIKI_ROOT_URL }}/api.php"
|
||||
USER="${{ secrets.WIKI_BOT_USER }}"
|
||||
PASS="${{ secrets.WIKI_BOT_PASS }}"
|
||||
|
||||
- name: Upload entity_prototypes to wiki
|
||||
uses: jtmullen/mediawiki-edit-action@v0.1.1
|
||||
with:
|
||||
wiki_text_file: ./bin/Content.Server/data/entity_prototypes.json
|
||||
edit_summary: Update entity_prototypes.json via GitHub Actions
|
||||
page_name: ${{ secrets.WIKI_PAGE_ROOT }}/entity_prototypes.json
|
||||
api_url: https://wiki.wylab.me/api.php
|
||||
username: ${{ secrets.WIKI_BOT_USER }}
|
||||
password: ${{ secrets.WIKI_BOT_PASS }}
|
||||
API="$(printf "%s" "$API" | tr -d '\r\n' | sed 's/[[:space:]]*$//')"
|
||||
USER="$(printf "%s" "$USER" | tr -d '\r\n')"
|
||||
PASS="$(printf "%s" "$PASS" | tr -d '\r\n')"
|
||||
ROOT="$(printf "%s" "$ROOT" | tr -d '\r\n' | sed 's/[[:space:]]*$//')"
|
||||
|
||||
- name: Upload mealrecipes_prototypes to wiki
|
||||
uses: jtmullen/mediawiki-edit-action@v0.1.1
|
||||
with:
|
||||
wiki_text_file: ./bin/Content.Server/data/mealrecipes_prototypes.json
|
||||
edit_summary: Update mealrecipes_prototypes.json via GitHub Actions
|
||||
page_name: ${{ secrets.WIKI_PAGE_ROOT }}/mealrecipes_prototypes.json
|
||||
api_url: https://wiki.wylab.me/api.php
|
||||
username: ${{ secrets.WIKI_BOT_USER }}
|
||||
password: ${{ secrets.WIKI_BOT_PASS }}
|
||||
cookiejar="$(mktemp)"
|
||||
trap 'rm -f "$cookiejar"' EXIT
|
||||
|
||||
login_token=$(curl -sS -c "$cookiejar" --data "action=query&meta=tokens&type=login&format=json" "$API" | jq -r '.query.tokens.logintoken')
|
||||
curl -sS -c "$cookiejar" -b "$cookiejar" \
|
||||
--data-urlencode "action=login" \
|
||||
--data-urlencode "lgname=$USER" \
|
||||
--data-urlencode "lgpassword=$PASS" \
|
||||
--data-urlencode "lgtoken=$login_token" \
|
||||
--data-urlencode "format=json" \
|
||||
"$API" > /dev/null
|
||||
|
||||
find "$BASE" -type f -name '*.json' | while IFS= read -r file; do
|
||||
rel="${file#$BASE/}"
|
||||
rel="$(printf "%s" "$rel" | tr -d '\r\n' | sed 's/:/_/g')"
|
||||
page="$ROOT/$rel"
|
||||
echo "Uploading $rel → $page"
|
||||
|
||||
token=$(curl -sS -b "$cookiejar" --data "action=query&meta=tokens&format=json" "$API" | jq -r '.query.tokens.csrftoken')
|
||||
|
||||
curl -sS -b "$cookiejar" \
|
||||
--data-urlencode "action=edit" \
|
||||
--data-urlencode "title=$page" \
|
||||
--data-urlencode "summary=Update $rel via GitHub Actions" \
|
||||
--data-urlencode "text@${file}" \
|
||||
--data-urlencode "token=$token" \
|
||||
--data-urlencode "format=json" \
|
||||
"$API" | jq -r '.'
|
||||
done
|
||||
|
||||
58
.github/workflows/upstream-sync-merge.yml
vendored
58
.github/workflows/upstream-sync-merge.yml
vendored
@@ -1,58 +0,0 @@
|
||||
name: Upstream Sync Auto-Merge
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build & Test"]
|
||||
types: [completed]
|
||||
branches: [upstream-sync]
|
||||
|
||||
jobs:
|
||||
auto-merge:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
- name: Find and merge upstream-sync PR
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
// Find open PR from upstream-sync branch
|
||||
const { data: prs } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
head: `${context.repo.owner}:upstream-sync`,
|
||||
state: 'open'
|
||||
});
|
||||
|
||||
if (prs.length === 0) {
|
||||
console.log('No open upstream-sync PR found');
|
||||
return;
|
||||
}
|
||||
|
||||
const pr = prs[0];
|
||||
console.log(`Found PR #${pr.number}: ${pr.title}`);
|
||||
|
||||
// Merge the PR using rebase
|
||||
try {
|
||||
await github.rest.pulls.merge({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pr.number,
|
||||
merge_method: 'rebase'
|
||||
});
|
||||
console.log(`Successfully merged PR #${pr.number}`);
|
||||
} catch (error) {
|
||||
console.log(`Failed to merge: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Delete the upstream-sync branch after merge
|
||||
try {
|
||||
await github.rest.git.deleteRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: 'heads/upstream-sync'
|
||||
});
|
||||
console.log('Deleted upstream-sync branch');
|
||||
} catch (error) {
|
||||
console.log(`Failed to delete branch: ${error.message}`);
|
||||
}
|
||||
99
.github/workflows/upstream-sync.yml
vendored
99
.github/workflows/upstream-sync.yml
vendored
@@ -1,99 +0,0 @@
|
||||
name: Upstream Sync
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * *' # Daily at 6am UTC
|
||||
workflow_dispatch: # Manual trigger
|
||||
|
||||
jobs:
|
||||
check-and-sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Add upstream remote
|
||||
run: |
|
||||
git remote add syndicate https://github.com/space-syndicate/space-station-14.git
|
||||
git fetch syndicate master
|
||||
|
||||
- name: Check for upstream updates
|
||||
id: check
|
||||
run: |
|
||||
BEHIND=$(git rev-list HEAD..syndicate/master --count)
|
||||
echo "behind=$BEHIND" >> $GITHUB_OUTPUT
|
||||
if [ "$BEHIND" -gt 0 ]; then
|
||||
echo "Upstream has $BEHIND new commits"
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Already up to date"
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Rebase onto upstream
|
||||
if: steps.check.outputs.has_updates == 'true'
|
||||
id: rebase
|
||||
run: |
|
||||
# Create sync branch
|
||||
git checkout -b upstream-sync
|
||||
|
||||
# Try rebase
|
||||
if git rebase syndicate/master; then
|
||||
echo "rebase_success=true" >> $GITHUB_OUTPUT
|
||||
git push -f origin upstream-sync
|
||||
else
|
||||
git rebase --abort
|
||||
echo "rebase_success=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Create PR for CI verification
|
||||
if: steps.check.outputs.has_updates == 'true' && steps.rebase.outputs.rebase_success == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const behind = '${{ steps.check.outputs.behind }}';
|
||||
|
||||
// Check if PR already exists
|
||||
const { data: prs } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
head: `${context.repo.owner}:upstream-sync`,
|
||||
state: 'open'
|
||||
});
|
||||
|
||||
if (prs.length > 0) {
|
||||
console.log('PR already exists, skipping creation');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create PR
|
||||
await github.rest.pulls.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: `Upstream sync: ${behind} new commits from syndicate/master`,
|
||||
head: 'upstream-sync',
|
||||
base: 'master',
|
||||
body: `Automatic rebase of wylab commits onto updated syndicate/master.\n\n**${behind} new upstream commits**\n\nThis PR will be auto-merged when CI passes.`
|
||||
});
|
||||
|
||||
- name: Create issue on rebase conflict
|
||||
if: steps.check.outputs.has_updates == 'true' && steps.rebase.outputs.rebase_success == 'false'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const behind = '${{ steps.check.outputs.behind }}';
|
||||
await github.rest.issues.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: `Upstream sync failed - ${behind} commits behind`,
|
||||
body: `Automatic rebase onto syndicate/master failed due to conflicts.\n\nManual intervention required:\n\`\`\`bash\ngit fetch syndicate\ngit rebase syndicate/master\n# resolve conflicts\ngit push --force-with-lease origin master\n\`\`\``,
|
||||
labels: ['upstream-sync', 'needs-attention']
|
||||
});
|
||||
|
||||
10
.github/workflows/validate-rgas.yml
vendored
10
.github/workflows/validate-rgas.yml
vendored
@@ -15,19 +15,19 @@ jobs:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- name: Setup Submodule
|
||||
run: git submodule update --init
|
||||
# Wylab-Secrets-Start
|
||||
# Corvax-Secrets-Start
|
||||
- name: Setup secrets
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.SECRETS_PRIVATE_KEY }}
|
||||
if: ${{ env.SSH_KEY != '' }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
mkdir ~/.ssh
|
||||
echo "${{ secrets.SECRETS_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
echo "HOST git.wylab.me" > ~/.ssh/config
|
||||
echo " StrictHostKeyChecking no" >> ~/.ssh/config
|
||||
echo "HOST *" > ~/.ssh/config
|
||||
echo "StrictHostKeyChecking no" >> ~/.ssh/config
|
||||
git -c submodule.Secrets.update=checkout submodule update --init
|
||||
# Wylab-Secrets-End
|
||||
# Corvax-Secrets-End
|
||||
- name: Pull engine updates
|
||||
uses: space-wizards/submodule-dependency@v0.1.5
|
||||
- uses: PaulRitter/yaml-schema-validator@v1
|
||||
|
||||
40
.github/workflows/validate-rsis.yml
vendored
40
.github/workflows/validate-rsis.yml
vendored
@@ -5,57 +5,35 @@ on:
|
||||
branches: [ master, staging, stable ]
|
||||
merge_group:
|
||||
pull_request:
|
||||
types: [ opened, reopened, synchronize, ready_for_review ]
|
||||
branches: [ master, staging, stable ]
|
||||
paths:
|
||||
- '**.rsi/**'
|
||||
|
||||
jobs:
|
||||
validate_rsis:
|
||||
name: Validate RSIs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check for RSI changes
|
||||
id: check_rsi
|
||||
uses: dorny/paths-filter@v3
|
||||
with:
|
||||
filters: |
|
||||
rsi:
|
||||
- '**.rsi/**'
|
||||
|
||||
- name: Skip if no RSI changes
|
||||
if: steps.check_rsi.outputs.rsi != 'true' && github.event_name == 'pull_request'
|
||||
run: echo "No RSI files changed, skipping validation"
|
||||
|
||||
- uses: actions/checkout@v4.2.2
|
||||
if: steps.check_rsi.outputs.rsi == 'true' || github.event_name != 'pull_request'
|
||||
|
||||
- name: Setup Submodule
|
||||
if: steps.check_rsi.outputs.rsi == 'true' || github.event_name != 'pull_request'
|
||||
run: git submodule update --init
|
||||
|
||||
# Wylab-Secrets-Start
|
||||
# Corvax-Secrets-Start
|
||||
- name: Setup secrets
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.SECRETS_PRIVATE_KEY }}
|
||||
if: (steps.check_rsi.outputs.rsi == 'true' || github.event_name != 'pull_request') && env.SSH_KEY != ''
|
||||
if: ${{ env.SSH_KEY != '' }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
mkdir ~/.ssh
|
||||
echo "${{ secrets.SECRETS_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
echo "HOST git.wylab.me" > ~/.ssh/config
|
||||
echo " StrictHostKeyChecking no" >> ~/.ssh/config
|
||||
echo "HOST *" > ~/.ssh/config
|
||||
echo "StrictHostKeyChecking no" >> ~/.ssh/config
|
||||
git -c submodule.Secrets.update=checkout submodule update --init
|
||||
# Wylab-Secrets-End
|
||||
|
||||
# Corvax-Secrets-End
|
||||
- name: Pull engine updates
|
||||
if: steps.check_rsi.outputs.rsi == 'true' || github.event_name != 'pull_request'
|
||||
uses: space-wizards/submodule-dependency@v0.1.5
|
||||
|
||||
- name: Install Python dependencies
|
||||
if: steps.check_rsi.outputs.rsi == 'true' || github.event_name != 'pull_request'
|
||||
run: |
|
||||
python3 -m pip install --user --break-system-packages pillow jsonschema
|
||||
|
||||
pip3 install --ignore-installed --user pillow jsonschema
|
||||
- name: Validate RSIs
|
||||
if: steps.check_rsi.outputs.rsi == 'true' || github.event_name != 'pull_request'
|
||||
run: |
|
||||
python3 RobustToolbox/Schemas/validate_rsis.py Resources/
|
||||
|
||||
10
.github/workflows/validate_mapfiles.yml
vendored
10
.github/workflows/validate_mapfiles.yml
vendored
@@ -15,19 +15,19 @@ jobs:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- name: Setup Submodule
|
||||
run: git submodule update --init
|
||||
# Wylab-Secrets-Start
|
||||
# Corvax-Secrets-Start
|
||||
- name: Setup secrets
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.SECRETS_PRIVATE_KEY }}
|
||||
if: ${{ env.SSH_KEY != '' }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
mkdir ~/.ssh
|
||||
echo "${{ secrets.SECRETS_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
echo "HOST git.wylab.me" > ~/.ssh/config
|
||||
echo " StrictHostKeyChecking no" >> ~/.ssh/config
|
||||
echo "HOST *" > ~/.ssh/config
|
||||
echo "StrictHostKeyChecking no" >> ~/.ssh/config
|
||||
git -c submodule.Secrets.update=checkout submodule update --init
|
||||
# Wylab-Secrets-End
|
||||
# Corvax-Secrets-End
|
||||
- name: Pull engine updates
|
||||
uses: space-wizards/submodule-dependency@v0.1.5
|
||||
- uses: PaulRitter/yaml-schema-validator@v1
|
||||
|
||||
9
.github/workflows/yaml-linter.yml
vendored
9
.github/workflows/yaml-linter.yml
vendored
@@ -12,15 +12,8 @@ jobs:
|
||||
name: YAML Linter
|
||||
if: github.actor != 'IanComradeBot' && github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- name: Delete Wylab override files (duplicates upstream for customization)
|
||||
run: |
|
||||
rm -f Resources/Prototypes/_Wylab/GameRules/events.yml
|
||||
rm -f Resources/Prototypes/_Wylab/GameRules/pests.yml
|
||||
rm -f Resources/Prototypes/_Wylab/GameRules/subgamemodes.yml
|
||||
- name: Setup submodule
|
||||
run: |
|
||||
git submodule update --init --recursive
|
||||
@@ -33,7 +26,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,6 @@
|
||||
# MSbuild binlog files
|
||||
*.binlog
|
||||
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import time
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
SOLUTION_PATH = Path("..") / "SpaceStation14.sln"
|
||||
SOLUTION_PATH = Path("..") / "SpaceStation14.slnx"
|
||||
# If this doesn't match the saved version we overwrite them all.
|
||||
CURRENT_HOOKS_VERSION = "4"
|
||||
QUIET = len(sys.argv) == 2 and sys.argv[1] == "--quiet"
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
|
||||
<PropertyGroup>
|
||||
<!-- Work around https://github.com/dotnet/project-system/issues/4314 -->
|
||||
<TargetFramework>$(TargetFramework)</TargetFramework>
|
||||
<OutputPath>..\bin\Content.Benchmarks\</OutputPath>
|
||||
<IsPackable>false</IsPackable>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<OutputType>Exe</OutputType>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<IsTestingPlatformApplication>false</IsTestingPlatformApplication>
|
||||
<Nullable>disable</Nullable>
|
||||
</PropertyGroup>
|
||||
<Import Project="../MSBuild/Content.props" />
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" />
|
||||
|
||||
<!-- pin transitive deps -->
|
||||
<PackageReference Include="System.Management" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Content.Client\Content.Client.csproj" />
|
||||
@@ -19,10 +22,12 @@
|
||||
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
|
||||
<ProjectReference Include="..\Content.Tests\Content.Tests.csproj" />
|
||||
<ProjectReference Include="..\Content.IntegrationTests\Content.IntegrationTests.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Benchmarks\Robust.Benchmarks.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Client\Robust.Client.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Server\Robust.Server.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Shared\Robust.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\RobustToolbox\Imports\Lidgren.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Client.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Server.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Shared.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Benchmarks.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Testing.props" />
|
||||
</Project>
|
||||
|
||||
83
Content.Benchmarks/HeatCapacityBenchmark.cs
Normal file
83
Content.Benchmarks/HeatCapacityBenchmark.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using System.Threading.Tasks;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Content.IntegrationTests;
|
||||
using Content.IntegrationTests.Pair;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Shared.Atmos;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Benchmarks;
|
||||
|
||||
[Virtual]
|
||||
[GcServer(true)]
|
||||
[MemoryDiagnoser]
|
||||
public class HeatCapacityBenchmark
|
||||
{
|
||||
private TestPair _pair = default!;
|
||||
private IEntityManager _sEntMan = default!;
|
||||
private IEntityManager _cEntMan = default!;
|
||||
private Client.Atmos.EntitySystems.AtmosphereSystem _cAtmos = default!;
|
||||
private AtmosphereSystem _sAtmos = default!;
|
||||
private GasMixture _mix;
|
||||
|
||||
[GlobalSetup]
|
||||
public async Task SetupAsync()
|
||||
{
|
||||
ProgramShared.PathOffset = "../../../../";
|
||||
PoolManager.Startup();
|
||||
_pair = await PoolManager.GetServerClient();
|
||||
await _pair.Connect();
|
||||
_cEntMan = _pair.Client.ResolveDependency<IEntityManager>();
|
||||
_sEntMan = _pair.Server.ResolveDependency<IEntityManager>();
|
||||
_cAtmos = _cEntMan.System<Client.Atmos.EntitySystems.AtmosphereSystem>();
|
||||
_sAtmos = _sEntMan.System<AtmosphereSystem>();
|
||||
|
||||
const float volume = 2500f;
|
||||
const float temperature = 293.15f;
|
||||
|
||||
const float o2 = 12.3f;
|
||||
const float n2 = 45.6f;
|
||||
const float co2 = 0.42f;
|
||||
const float plasma = 0.05f;
|
||||
|
||||
_mix = new GasMixture(volume) { Temperature = temperature };
|
||||
|
||||
_mix.AdjustMoles(Gas.Oxygen, o2);
|
||||
_mix.AdjustMoles(Gas.Nitrogen, n2);
|
||||
_mix.AdjustMoles(Gas.CarbonDioxide, co2);
|
||||
_mix.AdjustMoles(Gas.Plasma, plasma);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public async Task ClientHeatCapacityBenchmark()
|
||||
{
|
||||
await _pair.Client.WaitPost(delegate
|
||||
{
|
||||
for (var i = 0; i < 10000; i++)
|
||||
{
|
||||
_cAtmos.GetHeatCapacity(_mix, applyScaling: true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public async Task ServerHeatCapacityBenchmark()
|
||||
{
|
||||
await _pair.Server.WaitPost(delegate
|
||||
{
|
||||
for (var i = 0; i < 10000; i++)
|
||||
{
|
||||
_sAtmos.GetHeatCapacity(_mix, applyScaling: true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[GlobalCleanup]
|
||||
public async Task CleanupAsync()
|
||||
{
|
||||
await _pair.DisposeAsync();
|
||||
PoolManager.Shutdown();
|
||||
}
|
||||
}
|
||||
@@ -134,7 +134,7 @@ internal sealed class AdminNameOverlay : Overlay
|
||||
? null
|
||||
: _prototypeManager.Index(playerInfo.RoleProto.Value);
|
||||
|
||||
var roleName = Loc.GetString(rolePrototype?.Name ?? RoleTypePrototype.FallbackName);
|
||||
var roleName = rolePrototype?.Name ?? RoleTypePrototype.FallbackName;
|
||||
var roleColor = rolePrototype?.Color ?? RoleTypePrototype.FallbackColor;
|
||||
var roleSymbol = rolePrototype?.Symbol ?? RoleTypePrototype.FallbackSymbol;
|
||||
|
||||
@@ -213,7 +213,7 @@ internal sealed class AdminNameOverlay : Overlay
|
||||
{
|
||||
color = Color.GreenYellow;
|
||||
color.A = alpha;
|
||||
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, Loc.GetString(playerInfo.StartingJob), uiScale, playerInfo.Connected ? color : colorDisconnected);
|
||||
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.StartingJob, uiScale, playerInfo.Connected ? color : colorDisconnected);
|
||||
currentOffset += lineoffset;
|
||||
}
|
||||
|
||||
@@ -241,7 +241,7 @@ internal sealed class AdminNameOverlay : Overlay
|
||||
color = roleColor;
|
||||
symbol = IsFiltered(playerInfo.RoleProto) ? symbol : string.Empty;
|
||||
text = IsFiltered(playerInfo.RoleProto)
|
||||
? roleName.ToUpper()
|
||||
? Loc.GetString(roleName).ToUpper()
|
||||
: string.Empty;
|
||||
break;
|
||||
case AdminOverlayAntagFormat.Subtype:
|
||||
|
||||
35
Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs
Normal file
35
Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Content.Shared.Atmos;
|
||||
|
||||
namespace Content.Client.Atmos.EntitySystems;
|
||||
|
||||
public sealed partial class AtmosphereSystem
|
||||
{
|
||||
/*
|
||||
Partial class for operations involving GasMixtures.
|
||||
|
||||
Any method that is overridden here is usually because the server-sided implementation contains
|
||||
code that would escape sandbox. As such these methods are overridden here with a safe
|
||||
implementation.
|
||||
*/
|
||||
|
||||
[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))
|
||||
{
|
||||
return Atmospherics.SpaceHeatCapacity;
|
||||
}
|
||||
|
||||
// explicit stackalloc call is banned on client tragically.
|
||||
// the JIT does not stackalloc this during runtime,
|
||||
// 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, GasSpecificHeats, 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);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Client.Atmos.EntitySystems;
|
||||
|
||||
public sealed class AtmosphereSystem : SharedAtmosphereSystem
|
||||
public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
using Content.Client.BarSign.Ui;
|
||||
using Content.Shared.BarSign;
|
||||
using Content.Shared.Power;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.BarSign;
|
||||
|
||||
public sealed class BarSignSystem : VisualizerSystem<BarSignComponent>
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<BarSignComponent, AfterAutoHandleStateEvent>(OnAfterAutoHandleState);
|
||||
}
|
||||
|
||||
private void OnAfterAutoHandleState(EntityUid uid, BarSignComponent component, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
if (_ui.TryGetOpenUi<BarSignBoundUserInterface>(uid, BarSignUiKey.Key, out var bui))
|
||||
bui.Update(component.Current);
|
||||
|
||||
UpdateAppearance(uid, component);
|
||||
}
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, BarSignComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
UpdateAppearance(uid, component, args.Component, args.Sprite);
|
||||
}
|
||||
|
||||
private void UpdateAppearance(EntityUid id, BarSignComponent sign, AppearanceComponent? appearance = null, SpriteComponent? sprite = null)
|
||||
{
|
||||
if (!Resolve(id, ref appearance, ref sprite))
|
||||
return;
|
||||
|
||||
AppearanceSystem.TryGetData<bool>(id, PowerDeviceVisuals.Powered, out var powered, appearance);
|
||||
|
||||
if (powered
|
||||
&& sign.Current != null
|
||||
&& _prototypeManager.Resolve(sign.Current, out var proto))
|
||||
{
|
||||
SpriteSystem.LayerSetSprite((id, sprite), 0, proto.Icon);
|
||||
sprite.LayerSetShader(0, "unshaded");
|
||||
}
|
||||
else
|
||||
{
|
||||
SpriteSystem.LayerSetRsiState((id, sprite), 0, "empty");
|
||||
sprite.LayerSetShader(0, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
Content.Client/BarSign/BarSignVisualizerSystem.cs
Normal file
30
Content.Client/BarSign/BarSignVisualizerSystem.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Content.Shared.BarSign;
|
||||
using Content.Shared.Power;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.BarSign;
|
||||
|
||||
public sealed class BarSignVisualizerSystem : VisualizerSystem<BarSignComponent>
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, BarSignComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
AppearanceSystem.TryGetData<bool>(uid, PowerDeviceVisuals.Powered, out var powered, args.Component);
|
||||
AppearanceSystem.TryGetData<string>(uid, BarSignVisuals.BarSignPrototype, out var currentSign, args.Component);
|
||||
|
||||
if (powered
|
||||
&& currentSign != null
|
||||
&& _prototypeManager.Resolve<BarSignPrototype>(currentSign, out var proto))
|
||||
{
|
||||
SpriteSystem.LayerSetSprite((uid, args.Sprite), 0, proto.Icon);
|
||||
args.Sprite?.LayerSetShader(0, "unshaded");
|
||||
}
|
||||
else
|
||||
{
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), 0, "empty");
|
||||
args.Sprite?.LayerSetShader(0, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,32 +19,27 @@ public sealed class BarSignBoundUserInterface(EntityUid owner, Enum uiKey) : Bou
|
||||
var sign = EntMan.GetComponentOrNull<BarSignComponent>(Owner)?.Current is { } current
|
||||
? _prototype.Index(current)
|
||||
: null;
|
||||
var allSigns = Shared.BarSign.BarSignSystem.GetAllBarSigns(_prototype)
|
||||
var allSigns = BarSignSystem.GetAllBarSigns(_prototype)
|
||||
.OrderBy(p => Loc.GetString(p.Name))
|
||||
.ToList();
|
||||
_menu = new(sign, allSigns);
|
||||
|
||||
_menu.OnSignSelected += id =>
|
||||
{
|
||||
SendMessage(new SetBarSignMessage(id));
|
||||
SendPredictedMessage(new SetBarSignMessage(id));
|
||||
};
|
||||
|
||||
_menu.OnClose += Close;
|
||||
_menu.OpenCentered();
|
||||
}
|
||||
|
||||
public void Update(ProtoId<BarSignPrototype>? sign)
|
||||
public override void Update()
|
||||
{
|
||||
if (_prototype.Resolve(sign, out var signPrototype))
|
||||
_menu?.UpdateState(signPrototype);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
if (!EntMan.TryGetComponent<BarSignComponent>(Owner, out var signComp))
|
||||
return;
|
||||
_menu?.Dispose();
|
||||
|
||||
if (_prototype.Resolve(signComp.Current, out var signPrototype))
|
||||
_menu?.UpdateState(signPrototype);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Serilog;
|
||||
|
||||
namespace Content.Client.Cargo.UI;
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
using Content.Client.Chemistry.UI;
|
||||
using Content.Client.Items;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
|
||||
namespace Content.Client.Chemistry.EntitySystems;
|
||||
|
||||
public sealed class HyposprayStatusControlSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!;
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
Subs.ItemStatus<HyposprayComponent>(ent => new HyposprayStatusControl(ent, _solutionContainers));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Content.Client.Chemistry.UI;
|
||||
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 class InjectorStatusControlSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
Subs.ItemStatus<InjectorComponent>(injector => new InjectorStatusControl(injector, _solutionContainers, _prototypeManager));
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using Content.Client.Chemistry.UI;
|
||||
using Content.Client.Items;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
|
||||
namespace Content.Client.Chemistry.EntitySystems;
|
||||
|
||||
public sealed class InjectorSystem : SharedInjectorSystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
Subs.ItemStatus<InjectorComponent>(ent => new InjectorStatusControl(ent, SolutionContainer));
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,10 @@ namespace Content.Client.Chemistry.UI
|
||||
(uint) _window.BottleDosage.Value, _window.LabelLine));
|
||||
_window.BufferSortButton.OnPressed += _ => SendMessage(
|
||||
new ChemMasterSortingTypeCycleMessage());
|
||||
_window.OutputBufferDraw.OnPressed += _ => SendMessage(
|
||||
new ChemMasterOutputDrawSourceMessage(ChemMasterDrawSource.Internal));
|
||||
_window.OutputBeakerDraw.OnPressed += _ => SendMessage(
|
||||
new ChemMasterOutputDrawSourceMessage(ChemMasterDrawSource.External));
|
||||
|
||||
for (uint i = 0; i < _window.PillTypeButtons.Length; i++)
|
||||
{
|
||||
|
||||
@@ -79,10 +79,13 @@
|
||||
|
||||
<!-- Packaging -->
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'chem-master-window-packaging-text'}" />
|
||||
<Label Text="{Loc 'chem-master-output-source'}" StyleClasses="LabelSecondaryColor" Margin="0 0 5 0"/>
|
||||
<Button MinSize="80 0" Name="OutputBufferDraw" Access="Public" Text="{Loc 'chem-master-output-buffer-draw'}" ToggleMode="True" StyleClasses="OpenRight" />
|
||||
<Button MinSize="80 0" Name="OutputBeakerDraw" Access="Public" Text="{Loc 'chem-master-output-beaker-draw'}" ToggleMode="True" StyleClasses="OpenLeft" />
|
||||
<Control HorizontalExpand="True"/>
|
||||
<Label Text="{Loc 'chem-master-window-buffer-label'}" />
|
||||
<Label Name="BufferCurrentVolume" StyleClasses="LabelWeak" />
|
||||
<!-- Output Draw Source -->
|
||||
<Label Name="DrawSource"/>
|
||||
<Label Name="BufferCurrentVolume" StyleClasses="LabelSecondaryColor" />
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Wrap the packaging info-->
|
||||
|
||||
@@ -150,7 +150,17 @@ namespace Content.Client.Chemistry.UI
|
||||
// Ensure the Panel Info is updated, including UI elements for Buffer Volume, Output Container and so on
|
||||
UpdatePanelInfo(castState);
|
||||
|
||||
BufferCurrentVolume.Text = $" {castState.BufferCurrentVolume?.Int() ?? 0}u";
|
||||
switch (castState.DrawSource)
|
||||
{
|
||||
case ChemMasterDrawSource.Internal:
|
||||
SetBufferText(castState.BufferCurrentVolume, "chem-master-output-buffer-draw");
|
||||
break;
|
||||
case ChemMasterDrawSource.External:
|
||||
SetBufferText(castState.InputContainerInfo?.CurrentVolume, "chem-master-output-beaker-draw");
|
||||
break;
|
||||
default:
|
||||
throw new($"Chemmaster {castState.OutputContainerInfo} draw source is not set");
|
||||
}
|
||||
|
||||
InputEjectButton.Disabled = castState.InputContainerInfo is null;
|
||||
OutputEjectButton.Disabled = castState.OutputContainerInfo is null;
|
||||
@@ -168,9 +178,14 @@ namespace Content.Client.Chemistry.UI
|
||||
var holdsReagents = output?.Reagents != null;
|
||||
var pillNumberMax = holdsReagents ? 0 : remainingCapacity;
|
||||
var bottleAmountMax = holdsReagents ? remainingCapacity : 0;
|
||||
var bufferVolume = castState.BufferCurrentVolume?.Int() ?? 0;
|
||||
var outputVolume = castState.DrawSource switch
|
||||
{
|
||||
ChemMasterDrawSource.Internal => castState.BufferCurrentVolume?.Int() ?? 0,
|
||||
ChemMasterDrawSource.External => castState.InputContainerInfo?.CurrentVolume.Int() ?? 0,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
PillDosage.Value = (int)Math.Min(bufferVolume, castState.PillDosageLimit);
|
||||
PillDosage.Value = (int)Math.Min(outputVolume, castState.PillDosageLimit);
|
||||
|
||||
PillTypeButtons[castState.SelectedPillType].Pressed = true;
|
||||
|
||||
@@ -186,25 +201,35 @@ namespace Content.Client.Chemistry.UI
|
||||
// Avoid division by zero
|
||||
if (PillDosage.Value > 0)
|
||||
{
|
||||
PillNumber.Value = Math.Min(bufferVolume / PillDosage.Value, pillNumberMax);
|
||||
PillNumber.Value = Math.Min(outputVolume / PillDosage.Value, pillNumberMax);
|
||||
}
|
||||
else
|
||||
{
|
||||
PillNumber.Value = 0;
|
||||
}
|
||||
|
||||
BottleDosage.Value = Math.Min(bottleAmountMax, bufferVolume);
|
||||
BottleDosage.Value = Math.Min(bottleAmountMax, outputVolume);
|
||||
}
|
||||
/// <summary>
|
||||
/// Generate a product label based on reagents in the buffer.
|
||||
/// Generate a product label based on reagents in the buffer or beaker.
|
||||
/// </summary>
|
||||
/// <param name="state">State data sent by the server.</param>
|
||||
private string GenerateLabel(ChemMasterBoundUserInterfaceState state)
|
||||
{
|
||||
if (state.BufferCurrentVolume == 0)
|
||||
if (
|
||||
state.BufferCurrentVolume == 0 && state.DrawSource == ChemMasterDrawSource.Internal ||
|
||||
state.InputContainerInfo?.CurrentVolume == 0 && state.DrawSource == ChemMasterDrawSource.External ||
|
||||
state.InputContainerInfo?.Reagents == null
|
||||
)
|
||||
return "";
|
||||
|
||||
var reagent = state.BufferReagents.OrderBy(r => r.Quantity).First().Reagent;
|
||||
var reagent = (state.DrawSource switch
|
||||
{
|
||||
ChemMasterDrawSource.Internal => state.BufferReagents,
|
||||
ChemMasterDrawSource.External => state.InputContainerInfo.Reagents ?? [],
|
||||
_ => throw new($"Chemmaster {state.OutputContainerInfo} draw source is not set"),
|
||||
}).MinBy(r => r.Quantity)
|
||||
.Reagent;
|
||||
_prototypeManager.TryIndex(reagent.Prototype, out ReagentPrototype? proto);
|
||||
return proto?.LocalizedName ?? "";
|
||||
}
|
||||
@@ -233,6 +258,8 @@ namespace Content.Client.Chemistry.UI
|
||||
_ => Loc.GetString("chem-master-window-sort-type-none")
|
||||
};
|
||||
|
||||
OutputBufferDraw.Pressed = state.DrawSource == ChemMasterDrawSource.Internal;
|
||||
OutputBeakerDraw.Pressed = state.DrawSource == ChemMasterDrawSource.External;
|
||||
|
||||
if (!state.BufferReagents.Any())
|
||||
{
|
||||
@@ -414,6 +441,12 @@ namespace Content.Client.Chemistry.UI
|
||||
get => LabelLineEdit.Text;
|
||||
set => LabelLineEdit.Text = value;
|
||||
}
|
||||
|
||||
private void SetBufferText(FixedPoint2? volume, string text)
|
||||
{
|
||||
BufferCurrentVolume.Text = $" {volume ?? FixedPoint2.Zero}u";
|
||||
DrawSource.Text = Loc.GetString(text);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ReagentButton : Button
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
using Content.Client.Message;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Chemistry.UI;
|
||||
|
||||
public sealed class HyposprayStatusControl : Control
|
||||
{
|
||||
private readonly Entity<HyposprayComponent> _parent;
|
||||
private readonly RichTextLabel _label;
|
||||
private readonly SharedSolutionContainerSystem _solutionContainers;
|
||||
|
||||
private FixedPoint2 PrevVolume;
|
||||
private FixedPoint2 PrevMaxVolume;
|
||||
private bool PrevOnlyAffectsMobs;
|
||||
|
||||
public HyposprayStatusControl(Entity<HyposprayComponent> parent, SharedSolutionContainerSystem solutionContainers)
|
||||
{
|
||||
_parent = parent;
|
||||
_solutionContainers = solutionContainers;
|
||||
_label = new RichTextLabel { StyleClasses = { StyleClass.ItemStatus } };
|
||||
AddChild(_label);
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.SolutionName, out _, out var solution))
|
||||
return;
|
||||
|
||||
// only updates the UI if any of the details are different than they previously were
|
||||
if (PrevVolume == solution.Volume
|
||||
&& PrevMaxVolume == solution.MaxVolume
|
||||
&& PrevOnlyAffectsMobs == _parent.Comp.OnlyAffectsMobs)
|
||||
return;
|
||||
|
||||
PrevVolume = solution.Volume;
|
||||
PrevMaxVolume = solution.MaxVolume;
|
||||
PrevOnlyAffectsMobs = _parent.Comp.OnlyAffectsMobs;
|
||||
|
||||
var modeStringLocalized = Loc.GetString((_parent.Comp.OnlyAffectsMobs && _parent.Comp.CanContainerDraw) switch
|
||||
{
|
||||
false => "hypospray-all-mode-text",
|
||||
true => "hypospray-mobs-only-mode-text",
|
||||
});
|
||||
|
||||
_label.SetMarkup(Loc.GetString("hypospray-volume-label",
|
||||
("currentVolume", solution.Volume),
|
||||
("totalVolume", solution.MaxVolume),
|
||||
("modeString", modeStringLocalized)));
|
||||
}
|
||||
}
|
||||
@@ -2,26 +2,32 @@ using Content.Client.Message;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Chemistry.UI;
|
||||
|
||||
public sealed class InjectorStatusControl : Control
|
||||
{
|
||||
private readonly IPrototypeManager _prototypeManager;
|
||||
|
||||
private readonly Entity<InjectorComponent> _parent;
|
||||
private readonly SharedSolutionContainerSystem _solutionContainers;
|
||||
private readonly RichTextLabel _label;
|
||||
|
||||
private FixedPoint2 PrevVolume;
|
||||
private FixedPoint2 PrevMaxVolume;
|
||||
private FixedPoint2 PrevTransferAmount;
|
||||
private InjectorToggleMode PrevToggleState;
|
||||
private FixedPoint2 _prevVolume;
|
||||
private FixedPoint2 _prevMaxVolume;
|
||||
private FixedPoint2? _prevTransferAmount;
|
||||
private InjectorBehavior _prevBehavior;
|
||||
|
||||
public InjectorStatusControl(Entity<InjectorComponent> parent, SharedSolutionContainerSystem solutionContainers)
|
||||
public InjectorStatusControl(Entity<InjectorComponent> parent, SharedSolutionContainerSystem solutionContainers, IPrototypeManager prototypeManager)
|
||||
{
|
||||
_prototypeManager = prototypeManager;
|
||||
|
||||
_parent = parent;
|
||||
_solutionContainers = solutionContainers;
|
||||
_label = new RichTextLabel { StyleClasses = { StyleClass.ItemStatus } };
|
||||
@@ -32,33 +38,38 @@ public sealed class InjectorStatusControl : Control
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.SolutionName, out _, out var solution))
|
||||
if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.SolutionName, out _, out var solution)
|
||||
|| !_prototypeManager.Resolve(_parent.Comp.ActiveModeProtoId, out var activeMode))
|
||||
return;
|
||||
|
||||
// only updates the UI if any of the details are different than they previously were
|
||||
if (PrevVolume == solution.Volume
|
||||
&& PrevMaxVolume == solution.MaxVolume
|
||||
&& PrevTransferAmount == _parent.Comp.CurrentTransferAmount
|
||||
&& PrevToggleState == _parent.Comp.ToggleState)
|
||||
if (_prevVolume == solution.Volume
|
||||
&& _prevMaxVolume == solution.MaxVolume
|
||||
&& _prevTransferAmount == _parent.Comp.CurrentTransferAmount
|
||||
&& _prevBehavior == activeMode.Behavior)
|
||||
return;
|
||||
|
||||
PrevVolume = solution.Volume;
|
||||
PrevMaxVolume = solution.MaxVolume;
|
||||
PrevTransferAmount = _parent.Comp.CurrentTransferAmount;
|
||||
PrevToggleState = _parent.Comp.ToggleState;
|
||||
_prevVolume = solution.Volume;
|
||||
_prevMaxVolume = solution.MaxVolume;
|
||||
_prevTransferAmount = _parent.Comp.CurrentTransferAmount;
|
||||
_prevBehavior = activeMode.Behavior;
|
||||
|
||||
// Update current volume and injector state
|
||||
var modeStringLocalized = Loc.GetString(_parent.Comp.ToggleState switch
|
||||
// Seeing transfer volume is only important for injectors that can change it.
|
||||
if (activeMode.TransferAmounts.Count > 1 && _parent.Comp.CurrentTransferAmount.HasValue)
|
||||
{
|
||||
InjectorToggleMode.Draw => "injector-draw-text",
|
||||
InjectorToggleMode.Inject => "injector-inject-text",
|
||||
_ => "injector-invalid-injector-toggle-mode"
|
||||
});
|
||||
|
||||
_label.SetMarkup(Loc.GetString("injector-volume-label",
|
||||
("currentVolume", solution.Volume),
|
||||
("totalVolume", solution.MaxVolume),
|
||||
("modeString", modeStringLocalized),
|
||||
("transferVolume", _parent.Comp.CurrentTransferAmount)));
|
||||
_label.SetMarkup(Loc.GetString("injector-volume-transfer-label",
|
||||
("currentVolume", solution.Volume),
|
||||
("totalVolume", solution.MaxVolume),
|
||||
("modeString", Loc.GetString(activeMode.Name)),
|
||||
("transferVolume", _parent.Comp.CurrentTransferAmount.Value)));
|
||||
}
|
||||
else
|
||||
{
|
||||
_label.SetMarkup(Loc.GetString("injector-volume-label",
|
||||
("currentVolume", solution.Volume),
|
||||
("totalVolume", solution.MaxVolume),
|
||||
("modeString", Loc.GetString(activeMode.Name))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,41 +2,31 @@ using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.FixedPoint;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.Chemistry.UI
|
||||
namespace Content.Client.Chemistry.UI;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class TransferAmountBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class TransferAmountBoundUserInterface : BoundUserInterface
|
||||
[ViewVariables]
|
||||
private TransferAmountWindow? _window;
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
private IEntityManager _entManager;
|
||||
private EntityUid _owner;
|
||||
[ViewVariables]
|
||||
private TransferAmountWindow? _window;
|
||||
base.Open();
|
||||
_window = this.CreateWindow<TransferAmountWindow>();
|
||||
|
||||
public TransferAmountBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
if (EntMan.TryGetComponent<SolutionTransferComponent>(Owner, out var comp))
|
||||
_window.SetBounds(comp.MinimumTransferAmount.Int(), comp.MaximumTransferAmount.Int());
|
||||
|
||||
_window.ApplyButton.OnPressed += _ =>
|
||||
{
|
||||
_owner = owner;
|
||||
_entManager = IoCManager.Resolve<IEntityManager>();
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
_window = this.CreateWindow<TransferAmountWindow>();
|
||||
|
||||
if (_entManager.TryGetComponent<SolutionTransferComponent>(_owner, out var comp))
|
||||
_window.SetBounds(comp.MinimumTransferAmount.Int(), comp.MaximumTransferAmount.Int());
|
||||
|
||||
_window.ApplyButton.OnPressed += _ =>
|
||||
if (int.TryParse(_window.AmountLineEdit.Text, out var i))
|
||||
{
|
||||
if (int.TryParse(_window.AmountLineEdit.Text, out var i))
|
||||
{
|
||||
SendMessage(new TransferAmountSetValueMessage(FixedPoint2.New(i)));
|
||||
_window.Close();
|
||||
}
|
||||
};
|
||||
}
|
||||
SendPredictedMessage(new TransferAmountSetValueMessage(FixedPoint2.New(i)));
|
||||
_window.Close();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,34 +3,33 @@ using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Chemistry.UI
|
||||
namespace Content.Client.Chemistry.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class TransferAmountWindow : DefaultWindow
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class TransferAmountWindow : DefaultWindow
|
||||
private int _max = Int32.MaxValue;
|
||||
private int _min = 1;
|
||||
|
||||
public TransferAmountWindow()
|
||||
{
|
||||
private int _max = Int32.MaxValue;
|
||||
private int _min = 1;
|
||||
RobustXamlLoader.Load(this);
|
||||
AmountLineEdit.OnTextChanged += OnValueChanged;
|
||||
}
|
||||
|
||||
public TransferAmountWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
AmountLineEdit.OnTextChanged += OnValueChanged;
|
||||
}
|
||||
public void SetBounds(int min, int max)
|
||||
{
|
||||
_min = min;
|
||||
_max = max;
|
||||
MinimumAmount.Text = Loc.GetString("comp-solution-transfer-set-amount-min", ("amount", _min));
|
||||
MaximumAmount.Text = Loc.GetString("comp-solution-transfer-set-amount-max", ("amount", _max));
|
||||
}
|
||||
|
||||
public void SetBounds(int min, int max)
|
||||
{
|
||||
_min = min;
|
||||
_max = max;
|
||||
MinimumAmount.Text = Loc.GetString("comp-solution-transfer-set-amount-min", ("amount", _min));
|
||||
MaximumAmount.Text = Loc.GetString("comp-solution-transfer-set-amount-max", ("amount", _max));
|
||||
}
|
||||
|
||||
private void OnValueChanged(LineEdit.LineEditEventArgs args)
|
||||
{
|
||||
if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount > _max || amount < _min)
|
||||
ApplyButton.Disabled = true;
|
||||
else
|
||||
ApplyButton.Disabled = false;
|
||||
}
|
||||
private void OnValueChanged(LineEdit.LineEditEventArgs args)
|
||||
{
|
||||
if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount > _max || amount < _min)
|
||||
ApplyButton.Disabled = true;
|
||||
else
|
||||
ApplyButton.Disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,8 @@ using Content.Shared.Weapons.Ranged.Components;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Serialization;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.CombatMode;
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
using Content.Shared.Communications;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Communications;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class CommunicationsConsoleComponent : SharedCommunicationsConsoleComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// The prototype ID to use in the UI to show what entities a broadcast will display on
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntProtoId ScreenDisplayId = "Screen";
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Communications;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Communications.UI
|
||||
{
|
||||
@@ -22,31 +23,37 @@ namespace Content.Client.Communications.UI
|
||||
base.Open();
|
||||
|
||||
_menu = this.CreateWindow<CommunicationsConsoleMenu>();
|
||||
_menu.OnRadioAnnounce += RadioAnnounceButtonPressed;
|
||||
_menu.OnScreenBroadcast += ScreenBroadcastButtonPressed;
|
||||
_menu.OnAlertLevelChanged += AlertLevelSelected;
|
||||
_menu.OnShuttleCalled += CallShuttle;
|
||||
_menu.OnShuttleRecalled += RecallShuttle;
|
||||
|
||||
if (EntMan.TryGetComponent<CommunicationsConsoleComponent>(Owner, out var console))
|
||||
{
|
||||
_menu.SetBroadcastDisplayEntity(console.ScreenDisplayId);
|
||||
}
|
||||
_menu.OnAnnounce += AnnounceButtonPressed;
|
||||
_menu.OnBroadcast += BroadcastButtonPressed;
|
||||
_menu.OnAlertLevel += AlertLevelSelected;
|
||||
_menu.OnEmergencyLevel += EmergencyShuttleButtonPressed;
|
||||
}
|
||||
|
||||
public void AlertLevelSelected(string level)
|
||||
{
|
||||
SendMessage(new CommunicationsConsoleSelectAlertLevelMessage(level));
|
||||
if (_menu!.AlertLevelSelectable)
|
||||
{
|
||||
_menu.CurrentLevel = level;
|
||||
SendMessage(new CommunicationsConsoleSelectAlertLevelMessage(level));
|
||||
}
|
||||
}
|
||||
|
||||
public void RadioAnnounceButtonPressed(string message)
|
||||
public void EmergencyShuttleButtonPressed()
|
||||
{
|
||||
if (_menu!.CountdownStarted)
|
||||
RecallShuttle();
|
||||
else
|
||||
CallShuttle();
|
||||
}
|
||||
|
||||
public void AnnounceButtonPressed(string message)
|
||||
{
|
||||
var maxLength = _cfg.GetCVar(CCVars.ChatMaxAnnouncementLength);
|
||||
var msg = SharedChatSystem.SanitizeAnnouncement(message, maxLength);
|
||||
SendMessage(new CommunicationsConsoleAnnounceMessage(msg));
|
||||
}
|
||||
|
||||
public void ScreenBroadcastButtonPressed(string message)
|
||||
public void BroadcastButtonPressed(string message)
|
||||
{
|
||||
SendMessage(new CommunicationsConsoleBroadcastMessage(message));
|
||||
}
|
||||
@@ -70,7 +77,20 @@ namespace Content.Client.Communications.UI
|
||||
|
||||
if (_menu != null)
|
||||
{
|
||||
_menu.UpdateState(commsState);
|
||||
_menu.CanAnnounce = commsState.CanAnnounce;
|
||||
_menu.CanBroadcast = commsState.CanBroadcast;
|
||||
_menu.CanCall = commsState.CanCall;
|
||||
_menu.CountdownStarted = commsState.CountdownStarted;
|
||||
_menu.AlertLevelSelectable = commsState.AlertLevels != null && !float.IsNaN(commsState.CurrentAlertDelay) && commsState.CurrentAlertDelay <= 0;
|
||||
_menu.CurrentLevel = commsState.CurrentAlert;
|
||||
_menu.CountdownEnd = commsState.ExpectedCountdownEnd;
|
||||
|
||||
_menu.UpdateCountdown();
|
||||
_menu.UpdateAlertLevels(commsState.AlertLevels, _menu.CurrentLevel);
|
||||
_menu.AlertLevelButton.Disabled = !_menu.AlertLevelSelectable;
|
||||
_menu.EmergencyShuttleButton.Disabled = !_menu.CanCall;
|
||||
_menu.AnnounceButton.Disabled = !_menu.CanAnnounce;
|
||||
_menu.BroadcastButton.Disabled = !_menu.CanBroadcast;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,62 @@
|
||||
<comms:CommunicationsConsoleMenu xmlns="https://spacestation14.io"
|
||||
xmlns:comms="clr-namespace:Content.Client.Communications.UI"
|
||||
xmlns:widgets="clr-namespace:Content.Client.Communications.UI.Widgets"
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
MouseFilter="Stop" MinSize="400 660" SetWidth="450">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
|
||||
<controls:LayeredImageContainer StyleClasses="BrightAngleRectOutline" VerticalExpand="True">
|
||||
<BoxContainer Orientation="Vertical" Margin="12 8 12 12" VerticalExpand="True">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'comms-console-menu-title'}"
|
||||
StyleClasses="FancyWindowTitle" HorizontalExpand="True"/>
|
||||
<TextureButton Name="CloseButton" StyleClasses="windowCloseButton"
|
||||
Modulate="#646464" VerticalAlignment="Center" Margin="0 0 0 6"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True">
|
||||
<PanelContainer StyleClasses="PanelDark" VerticalExpand="True">
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True">
|
||||
<widgets:MessagingControls Name="MessagingControls" VerticalExpand="True" />
|
||||
Title="{Loc 'comms-console-menu-title'}"
|
||||
MinSize="400 300">
|
||||
|
||||
<controls:HSpacer Spacing="20" />
|
||||
<!-- Main Container -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="False"
|
||||
VerticalExpand="True"
|
||||
Margin="6 6 6 5">
|
||||
|
||||
<TextEdit Name="MessageInput"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch"
|
||||
MinHeight="100"/>
|
||||
|
||||
<!-- ButtonsPart -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
VerticalAlignment="Bottom"
|
||||
SeparationOverride="4">
|
||||
|
||||
<!-- AnnouncePart -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="0 2">
|
||||
|
||||
<Button Name="AnnounceButton"
|
||||
Access="Public"
|
||||
Text="{Loc 'comms-console-menu-announcement-button'}"
|
||||
ToolTip="{Loc 'comms-console-menu-announcement-button-tooltip'}"
|
||||
StyleClasses="OpenLeft"
|
||||
Margin="0 0 1 0"
|
||||
Disabled="True"/>
|
||||
|
||||
<Button Name="BroadcastButton"
|
||||
Access="Public"
|
||||
Text="{Loc 'comms-console-menu-broadcast-button'}"
|
||||
ToolTip="{Loc 'comms-console-menu-broadcast-button-tooltip'}"
|
||||
StyleClasses="OpenBoth"/>
|
||||
|
||||
<OptionButton Name="AlertLevelButton"
|
||||
Access="Public"
|
||||
ToolTip="{Loc 'comms-console-menu-alert-level-button-tooltip'}"
|
||||
StyleClasses="OpenRight"/>
|
||||
|
||||
<widgets:AlertLevelControls Name="AlertLevelControls" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:LayeredImageContainer>
|
||||
|
||||
<widgets:ShuttleControls Name="ShuttleControls" />
|
||||
<!-- EmergencyPart -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
SeparationOverride="6">
|
||||
|
||||
<RichTextLabel Name="CountdownLabel"/>
|
||||
|
||||
<Button Name="EmergencyShuttleButton"
|
||||
Access="Public"
|
||||
Text="Placeholder Text"
|
||||
ToolTip="{Loc 'comms-console-menu-emergency-shuttle-button-tooltip'}"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</comms:CommunicationsConsoleMenu>
|
||||
</controls:FancyWindow>
|
||||
|
||||
@@ -1,83 +1,137 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.Communications;
|
||||
using System.Globalization;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Communications.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class CommunicationsConsoleMenu : BaseWindow
|
||||
namespace Content.Client.Communications.UI
|
||||
{
|
||||
private const int DragMoveSize = 40;
|
||||
private const int DragResizeSize = 7;
|
||||
|
||||
public event Action? OnShuttleCalled;
|
||||
public event Action? OnShuttleRecalled;
|
||||
public event Action<string>? OnAlertLevelChanged;
|
||||
public event Action<string>? OnRadioAnnounce;
|
||||
public event Action<string>? OnScreenBroadcast;
|
||||
|
||||
public CommunicationsConsoleMenu()
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class CommunicationsConsoleMenu : FancyWindow
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
|
||||
CloseButton.OnPressed += _ => Close();
|
||||
public bool CanAnnounce;
|
||||
public bool CanBroadcast;
|
||||
public bool CanCall;
|
||||
public bool AlertLevelSelectable;
|
||||
public bool CountdownStarted;
|
||||
public string CurrentLevel = string.Empty;
|
||||
public TimeSpan? CountdownEnd;
|
||||
|
||||
MessagingControls.OnRadioAnnounce += message => OnRadioAnnounce?.Invoke(message);
|
||||
MessagingControls.OnScreenBroadcast += message => OnScreenBroadcast?.Invoke(message);
|
||||
public event Action? OnEmergencyLevel;
|
||||
public event Action<string>? OnAlertLevel;
|
||||
public event Action<string>? OnAnnounce;
|
||||
public event Action<string>? OnBroadcast;
|
||||
|
||||
AlertLevelControls.OnAlertLevelChanged += newLevel => OnAlertLevelChanged?.Invoke(newLevel);
|
||||
|
||||
ShuttleControls.OnShuttleCalled += () => OnShuttleCalled?.Invoke();
|
||||
ShuttleControls.OnShuttleRecalled += () => OnShuttleRecalled?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use the specified prototype ID as an example display in the
|
||||
/// UI subsection where users type broadcast messages
|
||||
/// </summary>
|
||||
public void SetBroadcastDisplayEntity(EntProtoId broadcastEntityId)
|
||||
{
|
||||
MessagingControls.SetBroadcastDisplayEntity(broadcastEntityId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure UI to be consistent with the input state
|
||||
/// </summary>
|
||||
public void UpdateState(CommunicationsConsoleInterfaceState commsState)
|
||||
{
|
||||
MessagingControls.CanRadioAnnounce = commsState.CanAnnounce;
|
||||
MessagingControls.CanScreenBroadcast = commsState.CanBroadcast;
|
||||
|
||||
var alertLevelSelectable = commsState.AlertLevels != null && commsState.CurrentAlertDelay <= 0;
|
||||
AlertLevelControls.UpdateAlertLevels(commsState.AlertLevels, commsState.CurrentAlert, commsState.CurrentAlertColor, alertLevelSelectable);
|
||||
|
||||
ShuttleControls.UpdateState(commsState.CanCall, commsState.CountdownStarted, commsState.ExpectedCountdownEnd);
|
||||
}
|
||||
|
||||
protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
|
||||
{
|
||||
if (relativeMousePos.Y < DragMoveSize)
|
||||
public CommunicationsConsoleMenu()
|
||||
{
|
||||
return DragMode.Move;
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
MessageInput.Placeholder = new Rope.Leaf(_loc.GetString("comms-console-menu-announcement-placeholder"));
|
||||
|
||||
var maxAnnounceLength = _cfg.GetCVar(CCVars.ChatMaxAnnouncementLength);
|
||||
MessageInput.OnTextChanged += (args) =>
|
||||
{
|
||||
if (args.Control.TextLength > maxAnnounceLength)
|
||||
{
|
||||
AnnounceButton.Disabled = true;
|
||||
AnnounceButton.ToolTip = Loc.GetString("comms-console-message-too-long");
|
||||
}
|
||||
else
|
||||
{
|
||||
AnnounceButton.Disabled = !CanAnnounce;
|
||||
AnnounceButton.ToolTip = null;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
AnnounceButton.OnPressed += _ => OnAnnounce?.Invoke(Rope.Collapse(MessageInput.TextRope));
|
||||
AnnounceButton.Disabled = !CanAnnounce;
|
||||
|
||||
BroadcastButton.OnPressed += _ => OnBroadcast?.Invoke(Rope.Collapse(MessageInput.TextRope));
|
||||
BroadcastButton.Disabled = !CanBroadcast;
|
||||
|
||||
AlertLevelButton.OnItemSelected += args =>
|
||||
{
|
||||
var metadata = AlertLevelButton.GetItemMetadata(args.Id);
|
||||
if (metadata != null && metadata is string cast)
|
||||
{
|
||||
OnAlertLevel?.Invoke(cast);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
AlertLevelButton.Disabled = !AlertLevelSelectable;
|
||||
|
||||
EmergencyShuttleButton.OnPressed += _ => OnEmergencyLevel?.Invoke();
|
||||
EmergencyShuttleButton.Disabled = !CanCall;
|
||||
}
|
||||
else
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
var mode = DragMode.None;
|
||||
base.FrameUpdate(args);
|
||||
UpdateCountdown();
|
||||
}
|
||||
|
||||
if (relativeMousePos.Y > Size.Y - DragResizeSize)
|
||||
// The current alert could make levels unselectable, so we need to ensure that the UI reacts properly.
|
||||
// If the current alert is unselectable, the only item in the alerts list will be
|
||||
// the current alert. Otherwise, it will be the list of alerts, with the current alert
|
||||
// selected.
|
||||
public void UpdateAlertLevels(List<string>? alerts, string currentAlert)
|
||||
{
|
||||
AlertLevelButton.Clear();
|
||||
|
||||
if (alerts == null)
|
||||
{
|
||||
mode |= DragMode.Bottom;
|
||||
var name = currentAlert;
|
||||
if (_loc.TryGetString($"alert-level-{currentAlert}", out var locName))
|
||||
{
|
||||
name = locName;
|
||||
}
|
||||
AlertLevelButton.AddItem(name);
|
||||
AlertLevelButton.SetItemMetadata(AlertLevelButton.ItemCount - 1, currentAlert);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var alert in alerts)
|
||||
{
|
||||
var name = alert;
|
||||
if (_loc.TryGetString($"alert-level-{alert}", out var locName))
|
||||
{
|
||||
name = locName;
|
||||
}
|
||||
AlertLevelButton.AddItem(name);
|
||||
AlertLevelButton.SetItemMetadata(AlertLevelButton.ItemCount - 1, alert);
|
||||
if (alert == currentAlert)
|
||||
{
|
||||
AlertLevelButton.Select(AlertLevelButton.ItemCount - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateCountdown()
|
||||
{
|
||||
if (!CountdownStarted)
|
||||
{
|
||||
CountdownLabel.SetMessage(string.Empty);
|
||||
EmergencyShuttleButton.Text = Loc.GetString("comms-console-menu-call-shuttle");
|
||||
return;
|
||||
}
|
||||
|
||||
if (relativeMousePos.X > Size.X - DragResizeSize)
|
||||
{
|
||||
mode |= DragMode.Right;
|
||||
}
|
||||
return mode;
|
||||
var diff = MathHelper.Max((CountdownEnd - _timing.CurTime) ?? TimeSpan.Zero, TimeSpan.Zero);
|
||||
|
||||
EmergencyShuttleButton.Text = Loc.GetString("comms-console-menu-recall-shuttle");
|
||||
var infoText = Loc.GetString($"comms-console-menu-time-remaining",
|
||||
("time", diff.ToString(@"hh\:mm\:ss", CultureInfo.CurrentCulture)));
|
||||
CountdownLabel.SetMessage(infoText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Client.Stylesheets.Stylesheets;
|
||||
using Content.Client.Stylesheets.SheetletConfigs;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using static Content.Client.Stylesheets.StylesheetHelpers;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
[CommonSheetlet]
|
||||
public sealed class CommunicationsConsoleSheetlet<T> : Sheetlet<T> where T : PalettedStylesheet, IButtonConfig, IIconConfig
|
||||
{
|
||||
public override StyleRule[] GetRules(T sheet, object config)
|
||||
{
|
||||
var lcdFontLarge = ResCache.GetFont("/Fonts/7SegmentDisplayDigits.ttf", 20);
|
||||
|
||||
return [
|
||||
E<Label>().Class("LabelLCDBig")
|
||||
.Prop("font-color", sheet.NegativePalette.Text)
|
||||
.Prop("font", lcdFontLarge),
|
||||
|
||||
/// Large red texture button
|
||||
E<TextureButton>().Identifier("TemptingRedButton")
|
||||
.Prop(TextureButton.StylePropertyTexture, sheet.GetTextureOr(sheet.RoundedButtonPath, NanotrasenStylesheet.TextureRoot))
|
||||
.Prop(Control.StylePropertyModulateSelf, sheet.NegativePalette.Element),
|
||||
|
||||
E<TextureButton>().Identifier("TemptingRedButton")
|
||||
.Pseudo(ContainerButton.StylePseudoClassNormal)
|
||||
.Prop(Control.StylePropertyModulateSelf, sheet.NegativePalette.Element),
|
||||
E<TextureButton>().Identifier("TemptingRedButton")
|
||||
.Pseudo(ContainerButton.StylePseudoClassDisabled)
|
||||
.Prop(TextureButton.StylePropertyTexture, ResCache.GetTexture("/Textures/Interface/Nano/rounded_locked_button.svg.96dpi.png"))
|
||||
.Prop(Control.StylePropertyModulateSelf, sheet.NegativePalette.DisabledElement),
|
||||
E<TextureButton>().Identifier("TemptingRedButton").Pseudo(ContainerButton.StylePseudoClassHover)
|
||||
.Prop(Control.StylePropertyModulateSelf, sheet.NegativePalette.HoveredElement),
|
||||
|
||||
E<TextureRect>().Identifier("ScrewHead")
|
||||
.Prop(TextureRect.StylePropertyTexture, ResCache.GetTexture("/Textures/Interface/Diegetic/screw.svg.96dpi.png")),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
Orientation="Vertical">
|
||||
<!-- Info about the current alert level -->
|
||||
<BoxContainer HorizontalAlignment="Center">
|
||||
<Label StyleClasses="StatusFieldTitle" Margin="0 0 9 0"
|
||||
Text="{Loc 'comms-console-alert-current-level-header'}" />
|
||||
<Label Name="CurrentAlertLevelLabel" Margin="9 0 0 0"/>
|
||||
</BoxContainer>
|
||||
|
||||
<Label Name="CurrentAlertLevelFlavorLabel" Align="Center" />
|
||||
|
||||
<!-- Controls for changing the alert level -->
|
||||
<BoxContainer Margin="9 12 9 6">
|
||||
<OptionButton Name="AlertLevelSelector" StyleClasses="OpenRight"
|
||||
ToolTip="{Loc 'comms-console-menu-alert-level-button-tooltip'}"
|
||||
HorizontalExpand="True" SizeFlagsStretchRatio="4" />
|
||||
<Button Name="ConfirmAlertLevelButton" StyleClasses="OpenLeft"
|
||||
HorizontalExpand="True" SizeFlagsStretchRatio="1"
|
||||
Text="{Loc 'comms-console-confirm-alert-level-button'}"
|
||||
Disabled="True" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
@@ -1,137 +0,0 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Communications.UI.Widgets;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AlertLevelControls : BoxContainer
|
||||
{
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
private bool _alertLevelSelectable;
|
||||
private string _currentAlertLevel = string.Empty;
|
||||
|
||||
public event Action<string>? OnAlertLevelChanged;
|
||||
|
||||
public AlertLevelControls()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
AlertLevelSelector.OnItemSelected += args =>
|
||||
{
|
||||
AlertLevelSelector.Select(args.Id);
|
||||
EnableDisableConfirmLevelChangeButton();
|
||||
};
|
||||
|
||||
ConfirmAlertLevelButton.OnPressed += _ =>
|
||||
{
|
||||
var metadata = AlertLevelSelector.GetItemMetadata(AlertLevelSelector.SelectedId);
|
||||
if (metadata is string cast)
|
||||
{
|
||||
OnAlertLevelChanged?.Invoke(cast);
|
||||
}
|
||||
};
|
||||
|
||||
AlertLevelSelector.Disabled = !_alertLevelSelectable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the UI components to display the current alert level and the
|
||||
/// selectable alert levels
|
||||
/// </summary>
|
||||
public void UpdateAlertLevels(List<string>? alerts,
|
||||
string currentAlert,
|
||||
Color currentAlertColor,
|
||||
bool alertLevelSelectable)
|
||||
{
|
||||
_alertLevelSelectable = alertLevelSelectable;
|
||||
_currentAlertLevel = currentAlert;
|
||||
CurrentAlertLevelLabel.Text = GetLocalizedAlertName(currentAlert);
|
||||
CurrentAlertLevelLabel.ModulateSelfOverride = currentAlertColor;
|
||||
|
||||
AlertLevelSelector.Disabled = alerts == null || !_alertLevelSelectable;
|
||||
|
||||
// If user had changed the selection, but hadn't pressed the confirm
|
||||
// button at the point we received an update message, we want to
|
||||
// remember what they had selected, so we can attempt to re-select it
|
||||
// if it's still an option in the new set of alert level possibilities.
|
||||
string? previousSelection = null;
|
||||
if (AlertLevelSelector.ItemCount > 1)
|
||||
{
|
||||
var metadata = AlertLevelSelector.GetItemMetadata(AlertLevelSelector.SelectedId);
|
||||
if (metadata is string cast)
|
||||
{
|
||||
previousSelection = cast;
|
||||
}
|
||||
}
|
||||
|
||||
// The server will only send alert levels which are selectable, but
|
||||
// unfortunately, for a short time after changing the alert level, no
|
||||
// level is selectable, so we won't have any levels to put into the
|
||||
// alert level list. Here, we make a dummy item to handle that; this
|
||||
// item also informs the user what this combo box is for.
|
||||
AlertLevelSelector.Clear();
|
||||
AlertLevelSelector.AddItem(Loc.GetString("comms-console-change-alert-level-button"));
|
||||
AlertLevelSelector.Select(0);
|
||||
|
||||
if (alerts != null)
|
||||
{
|
||||
foreach (var alert in alerts)
|
||||
{
|
||||
AlertLevelSelector.AddItem(GetLocalizedAlertName(alert));
|
||||
AlertLevelSelector.SetItemMetadata(AlertLevelSelector.ItemCount - 1, alert);
|
||||
|
||||
if (alert == previousSelection)
|
||||
{
|
||||
AlertLevelSelector.Select(AlertLevelSelector.ItemCount - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_loc.TryGetString($"comms-console-level-{currentAlert}-flavour-label", out var flavour))
|
||||
{
|
||||
CurrentAlertLevelFlavorLabel.Text = flavour;
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentAlertLevelFlavorLabel.Text = string.Empty;
|
||||
}
|
||||
|
||||
EnableDisableConfirmLevelChangeButton();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure the "confirm alert level changed" button to be consistent with
|
||||
/// the server state and user interactions
|
||||
/// </summary>
|
||||
private void EnableDisableConfirmLevelChangeButton()
|
||||
{
|
||||
// Disable the button when:
|
||||
// 1. Server says we can't change level
|
||||
// 2. User selected special info option (id 0)
|
||||
// 3. User selected the same level the station is currently on
|
||||
var selectedId = AlertLevelSelector.SelectedId;
|
||||
ConfirmAlertLevelButton.Disabled = !_alertLevelSelectable || selectedId == 0;
|
||||
if (selectedId != 0)
|
||||
{
|
||||
var metadata = AlertLevelSelector.GetItemMetadata(selectedId);
|
||||
if (metadata is string selected)
|
||||
{
|
||||
ConfirmAlertLevelButton.Disabled |= selected == _currentAlertLevel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Utility function to convert an alert level identifier into a localized string
|
||||
/// </summary>
|
||||
private string GetLocalizedAlertName(string alertName)
|
||||
{
|
||||
if (_loc.TryGetString($"alert-level-{alertName}", out var locName))
|
||||
{
|
||||
return locName;
|
||||
}
|
||||
return alertName;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
<TabContainer xmlns="https://spacestation14.io" >
|
||||
<!-- Controls for sending radio announcements -->
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True"
|
||||
TabContainer.TabTitle="{Loc 'comms-console-announce-tab-title'}">
|
||||
<Label StyleClasses="StatusFieldTitle" Align="Center"
|
||||
Text="{Loc 'comms-console-station-announcements-header'}" />
|
||||
<PanelContainer StyleClasses="highlight"
|
||||
Margin="6" VerticalExpand="True">
|
||||
<PanelContainer StyleClasses="BackgroundDark" Margin="2"
|
||||
HorizontalExpand="True" VerticalExpand="True">
|
||||
<TextEdit Name="RadioMessageInput"
|
||||
Margin="2 0 0 0" MinHeight="100"
|
||||
HorizontalExpand="True" VerticalExpand="True" />
|
||||
</PanelContainer>
|
||||
</PanelContainer>
|
||||
<Button Name="AnnounceButton" HorizontalExpand="True" Margin="6"
|
||||
Text="{Loc 'comms-console-menu-announcement-button'}"
|
||||
ToolTip="{Loc 'comms-console-menu-announcement-button-tooltip'}" />
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Controls for putting messages on station screens (aka broadcasting) -->
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True"
|
||||
TabContainer.TabTitle="{Loc 'comms-console-broadcast-tab-title'}">
|
||||
<Label StyleClasses="StatusFieldTitle" Align="Center" VAlign="Top"
|
||||
VerticalExpand="True" VerticalAlignment="Top"
|
||||
Text="{Loc 'comms-console-station-broadcast-header'}" />
|
||||
<SpriteView Name="BroadcastEntityDisplay" Scale="8 8" />
|
||||
<LineEdit Name="ScreenMessageInput" Margin="6 0 6 0"
|
||||
PlaceHolder="{Loc 'comms-console-menu-broadcast-placeholder'}" />
|
||||
<Button Name="BroadcastButton" HorizontalExpand="True" Margin="6"
|
||||
Text="{Loc 'comms-console-menu-broadcast-button'}"
|
||||
ToolTip="{Loc 'comms-console-menu-broadcast-button-tooltip'}" />
|
||||
</BoxContainer>
|
||||
</TabContainer>
|
||||
@@ -1,117 +0,0 @@
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.TextScreen;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Communications.UI.Widgets;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class MessagingControls : TabContainer
|
||||
{
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||
|
||||
// Entity temporarily created to display a screen preview
|
||||
private EntityUid _broadcastDisplayEntity = EntityUid.Invalid;
|
||||
|
||||
public event Action<string>? OnRadioAnnounce;
|
||||
public event Action<string>? OnScreenBroadcast;
|
||||
|
||||
private bool _canRadioAnnounce;
|
||||
public bool CanRadioAnnounce
|
||||
{
|
||||
set
|
||||
{
|
||||
_canRadioAnnounce = value;
|
||||
SyncButtonState();
|
||||
}
|
||||
}
|
||||
|
||||
private bool _canScreenBroadcast;
|
||||
public bool CanScreenBroadcast
|
||||
{
|
||||
set
|
||||
{
|
||||
_canScreenBroadcast = value;
|
||||
SyncButtonState();
|
||||
}
|
||||
}
|
||||
|
||||
public MessagingControls()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
RadioMessageInput.Placeholder = new Rope.Leaf(_loc.GetString("comms-console-menu-announcement-placeholder"));
|
||||
|
||||
RadioMessageInput.OnTextChanged += (_) => SyncButtonState();
|
||||
|
||||
AnnounceButton.OnPressed += _ =>
|
||||
{
|
||||
OnRadioAnnounce?.Invoke(Rope.Collapse(RadioMessageInput.TextRope));
|
||||
};
|
||||
|
||||
var appearanceSystem = _entMan.System<SharedAppearanceSystem>();
|
||||
ScreenMessageInput.OnTextChanged += args =>
|
||||
{
|
||||
if (_broadcastDisplayEntity.IsValid())
|
||||
{
|
||||
appearanceSystem.SetData(_broadcastDisplayEntity, TextScreenVisuals.ScreenText, args.Text);
|
||||
}
|
||||
};
|
||||
|
||||
BroadcastButton.OnPressed += _ =>
|
||||
{
|
||||
OnScreenBroadcast?.Invoke(ScreenMessageInput.Text);
|
||||
};
|
||||
|
||||
SyncButtonState();
|
||||
}
|
||||
|
||||
public void SetBroadcastDisplayEntity(EntProtoId broadcastEntityId)
|
||||
{
|
||||
_broadcastDisplayEntity = _entMan.Spawn(broadcastEntityId);
|
||||
if (_broadcastDisplayEntity.IsValid())
|
||||
{
|
||||
BroadcastEntityDisplay.SetEntity(_broadcastDisplayEntity);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
if (_broadcastDisplayEntity.IsValid())
|
||||
{
|
||||
_entMan.DeleteEntity(_broadcastDisplayEntity);
|
||||
}
|
||||
}
|
||||
|
||||
private void SyncButtonState()
|
||||
{
|
||||
if (_canRadioAnnounce)
|
||||
{
|
||||
var maxAnnounceLength = _cfg.GetCVar(CCVars.ChatMaxAnnouncementLength);
|
||||
if (RadioMessageInput.TextLength > maxAnnounceLength)
|
||||
{
|
||||
AnnounceButton.Disabled = true;
|
||||
AnnounceButton.ToolTip = _loc.GetString("comms-console-message-too-long");
|
||||
}
|
||||
else
|
||||
{
|
||||
AnnounceButton.Disabled = false;
|
||||
AnnounceButton.ToolTip = _loc.GetString("comms-console-menu-announcement-button-tooltip");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AnnounceButton.Disabled = true;
|
||||
AnnounceButton.ToolTip = _loc.GetString("comms-console-message-cannot-send");
|
||||
}
|
||||
|
||||
BroadcastButton.Disabled = !_canScreenBroadcast;
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
<Control xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
||||
<controls:StripeBack
|
||||
HasTopEdge="False" HasBottomEdge="False" StyleClasses="status-warning" >
|
||||
|
||||
<TextureRect StyleIdentifier="ScrewHead" TextureScale="0.25 0.25"
|
||||
HorizontalAlignment="Left" VerticalAlignment="Top" Margin="4"/>
|
||||
<TextureRect StyleIdentifier="ScrewHead" TextureScale="0.25 0.25"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Top" Margin="4"/>
|
||||
<TextureRect StyleIdentifier="ScrewHead" TextureScale="0.25 0.25"
|
||||
HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="4"/>
|
||||
<TextureRect StyleIdentifier="ScrewHead" TextureScale="0.25 0.25"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="4"/>
|
||||
|
||||
<!-- Controls for calling, recalling and displaying info for the emergency shuttle -->
|
||||
<PanelContainer StyleClasses="PanelDark" Margin="20">
|
||||
<BoxContainer Orientation="Vertical" Margin="8">
|
||||
<Label StyleClasses="FancyWindowTitle" Align="Center"
|
||||
Text="{Loc 'comms-console-shuttle-controls-header'}" />
|
||||
|
||||
<!-- This contains some blank `Controls` to act as dummies. This is because the
|
||||
GridContainer will always left-align the children, while we want them centered.
|
||||
The dummy Controls will attempt to take an even amount of space each, which
|
||||
will re-center our real controls -->
|
||||
<GridContainer Columns="7" HorizontalExpand="True" Margin="0 8 0 0">
|
||||
<!-- Row 1: the buttons themselves -->
|
||||
<Control HorizontalExpand="True" />
|
||||
<controls:LayeredImageContainer>
|
||||
<Control.StyleClasses>
|
||||
<sys:String>PanelMount</sys:String>
|
||||
<sys:String>PanelDark</sys:String>
|
||||
</Control.StyleClasses>
|
||||
<TextureButton Name="EmergencyShuttleCallButton"
|
||||
StyleIdentifier="TemptingRedButton" MinSize="60 60"
|
||||
ToolTip="{Loc 'comms-console-menu-call-shuttle'}" />
|
||||
</controls:LayeredImageContainer>
|
||||
<Control HorizontalExpand="True" />
|
||||
<controls:LayeredImageContainer
|
||||
VerticalAlignment="Center">
|
||||
<Control.StyleClasses>
|
||||
<sys:String>PanelMount</sys:String>
|
||||
<sys:String>PanelDark</sys:String>
|
||||
</Control.StyleClasses>
|
||||
<Label Name="CountdownLabel" StyleClasses="LabelLCDBig"
|
||||
ReservesSpace="True" Margin="6"/>
|
||||
</controls:LayeredImageContainer>
|
||||
<Control HorizontalExpand="True" />
|
||||
<controls:LayeredImageContainer>
|
||||
<Control.StyleClasses>
|
||||
<sys:String>PanelMount</sys:String>
|
||||
<sys:String>PanelDark</sys:String>
|
||||
</Control.StyleClasses>
|
||||
<TextureButton Name="EmergencyShuttleRecallButton"
|
||||
StyleIdentifier="TemptingRedButton" MinSize="60 60"
|
||||
ToolTip="{Loc 'comms-console-menu-recall-shuttle'}" />
|
||||
</controls:LayeredImageContainer>
|
||||
<Control HorizontalExpand="True" />
|
||||
|
||||
<!-- Row 2: The labels for the buttons -->
|
||||
<Control HorizontalExpand="True" />
|
||||
<Label Text="{Loc 'comms-console-call-button-label'}" Align="Center" />
|
||||
<Control HorizontalExpand="True" />
|
||||
<Label Text="{Loc 'comms-console-shuttle-status-label'}" Align="Center" />
|
||||
<Control HorizontalExpand="True" />
|
||||
<Label Text="{Loc 'comms-console-recall-button-label'}" Align="Center" />
|
||||
<Control HorizontalExpand="True" />
|
||||
|
||||
</GridContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</controls:StripeBack>
|
||||
</Control>
|
||||
@@ -1,99 +0,0 @@
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Communications.UI.Widgets;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ShuttleControls : Control
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private bool _canCall;
|
||||
private bool _countdownStarted;
|
||||
private TimeSpan? _countdownEnd;
|
||||
|
||||
public event Action? OnShuttleCalled;
|
||||
public event Action? OnShuttleRecalled;
|
||||
|
||||
public ShuttleControls()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
EmergencyShuttleCallButton.OnPressed += _ => OnShuttleCalled?.Invoke();
|
||||
EmergencyShuttleRecallButton.OnPressed += _ => OnShuttleRecalled?.Invoke();
|
||||
SyncButtonState();
|
||||
}
|
||||
|
||||
public void UpdateState(bool canCallShuttle, bool countdownStarted, TimeSpan? expectedCountdownEnd)
|
||||
{
|
||||
_canCall = canCallShuttle;
|
||||
_countdownStarted = countdownStarted;
|
||||
_countdownEnd = expectedCountdownEnd;
|
||||
SyncButtonState();
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
UpdateCountdown();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Syncs and animates the display of the time-to-shuttle-arrival label
|
||||
/// </summary>
|
||||
private void UpdateCountdown()
|
||||
{
|
||||
// Set the label on the LCD countdown
|
||||
var countdown = _countdownEnd == null ? 0.0f : (float)Math.Max(_countdownEnd.Value.Subtract(_timing.CurTime).TotalSeconds, 0);
|
||||
|
||||
var remainingWholeSeconds = _countdownStarted ? (int)Math.Ceiling(countdown) : 0;
|
||||
var message = (remainingWholeSeconds / 60).ToString("D2") + ":" + (remainingWholeSeconds % 60).ToString("D2");
|
||||
CountdownLabel.Text = message;
|
||||
|
||||
CountdownLabel.Visible = _countdownStarted;
|
||||
if(countdown == 0)
|
||||
{
|
||||
CountdownLabel.ModulateSelfOverride = Color.White;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Blink the LCD
|
||||
var alpha = 1.0f;
|
||||
if (!_cfg.GetCVar(CCVars.ReducedMotion))
|
||||
{
|
||||
var subSecondsRemaining = countdown - (float)Math.Floor(countdown);
|
||||
var lightEnableBlend = SmoothStep(0.1f, 0.3f, subSecondsRemaining);
|
||||
var lightDisableBlend = SmoothStep(0.9f, 0.95f, subSecondsRemaining);
|
||||
alpha = lightEnableBlend - lightDisableBlend;
|
||||
}
|
||||
CountdownLabel.ModulateSelfOverride = new Color(1.0f, 1.0f, 1.0f, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
private float SmoothStep(float stepBegin, float stepEnd, float x)
|
||||
{
|
||||
if (x < stepBegin)
|
||||
return 0;
|
||||
|
||||
if (x >= stepEnd)
|
||||
return 1;
|
||||
|
||||
var t = (x - stepBegin) / (stepEnd - stepBegin);
|
||||
return (3 * t * t) - (2 * t * t * t);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure the shuttle call/recall buttons to have a consistent state
|
||||
/// </summary>
|
||||
private void SyncButtonState()
|
||||
{
|
||||
EmergencyShuttleCallButton.Disabled = !(_canCall && !_countdownStarted);
|
||||
EmergencyShuttleRecallButton.Disabled = !(_canCall && _countdownStarted);
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,10 @@ namespace Content.Client.Construction.UI
|
||||
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
|
||||
private readonly SpriteSystem _spriteSystem;
|
||||
private readonly ISawmill _sawmill;
|
||||
|
||||
private readonly IConstructionMenuView _constructionView;
|
||||
private readonly EntityWhitelistSystem _whitelistSystem;
|
||||
@@ -90,6 +93,7 @@ namespace Content.Client.Construction.UI
|
||||
_constructionView = new ConstructionMenu();
|
||||
_whitelistSystem = _entManager.System<EntityWhitelistSystem>();
|
||||
_spriteSystem = _entManager.System<SpriteSystem>();
|
||||
_sawmill = _logManager.GetSawmill("construction.ui");
|
||||
|
||||
// This is required so that if we load after the system is initialized, we can bind to it immediately
|
||||
if (_systemManager.TryGetEntitySystem<ConstructionSystem>(out var constructionSystem))
|
||||
@@ -284,7 +288,7 @@ namespace Content.Client.Construction.UI
|
||||
|
||||
if (!_constructionSystem!.TryGetRecipePrototype(recipe.ID, out var targetProtoId))
|
||||
{
|
||||
Logger.Error("Cannot find the target prototype in the recipe cache with the id \"{0}\" of {1}.",
|
||||
_sawmill.Error("Cannot find the target prototype in the recipe cache with the id \"{0}\" of {1}.",
|
||||
recipe.ID,
|
||||
nameof(ConstructionPrototype));
|
||||
continue;
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
using System.Linq;
|
||||
using Content.Client.Materials;
|
||||
using Content.Client.Materials.UI;
|
||||
using Content.Client.Message;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Construction.Components;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.Materials;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
@@ -61,57 +59,48 @@ public sealed partial class FlatpackCreatorMenu : FancyWindow
|
||||
!_itemSlots.TryGetSlot(_owner, flatpacker.SlotId, out var itemSlot))
|
||||
return;
|
||||
|
||||
var flatpackerEnt = (_owner, flatpacker);
|
||||
|
||||
if (flatpacker.Packing)
|
||||
{
|
||||
PackButton.Disabled = true;
|
||||
}
|
||||
else if (_currentBoard != null)
|
||||
{
|
||||
Dictionary<string, int> cost;
|
||||
if (_entityManager.TryGetComponent<MachineBoardComponent>(_currentBoard, out var machineBoardComp))
|
||||
cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), (_currentBoard.Value, machineBoardComp));
|
||||
else
|
||||
cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), null);
|
||||
|
||||
PackButton.Disabled = !_materialStorage.CanChangeMaterialAmount(_owner, cost);
|
||||
PackButton.Disabled = !_flatpack.TryGetFlatpackCreationCost(flatpackerEnt, _currentBoard.Value, out var curCost)
|
||||
|| !_materialStorage.CanChangeMaterialAmount(_owner, curCost);
|
||||
}
|
||||
|
||||
if (_currentBoard == itemSlot.Item)
|
||||
return;
|
||||
|
||||
_currentBoard = itemSlot.Item;
|
||||
CostHeaderLabel.Visible = _currentBoard != null;
|
||||
CostHeaderLabel.Visible = false;
|
||||
InsertLabel.Visible = _currentBoard == null;
|
||||
|
||||
if (_currentBoard is not null)
|
||||
if (_currentBoard is null)
|
||||
{
|
||||
string? prototype = null;
|
||||
Dictionary<string, int>? cost = null;
|
||||
MachineSprite.SetPrototype(NoBoardEffectId);
|
||||
CostLabel.SetMessage(Loc.GetString("flatpacker-ui-no-board-label"));
|
||||
MachineNameLabel.SetMessage(string.Empty);
|
||||
PackButton.Disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_entityManager.TryGetComponent<MachineBoardComponent>(_currentBoard, out var newMachineBoardComp))
|
||||
{
|
||||
prototype = newMachineBoardComp.Prototype;
|
||||
cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), (_currentBoard.Value, newMachineBoardComp));
|
||||
}
|
||||
else if (_entityManager.TryGetComponent<ComputerBoardComponent>(_currentBoard, out var computerBoard))
|
||||
{
|
||||
prototype = computerBoard.Prototype;
|
||||
cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), null);
|
||||
}
|
||||
|
||||
if (prototype is not null && cost is not null)
|
||||
{
|
||||
var proto = _prototypeManager.Index<EntityPrototype>(prototype);
|
||||
MachineSprite.SetPrototype(prototype);
|
||||
MachineNameLabel.SetMessage(proto.Name);
|
||||
CostLabel.SetMarkup(GetCostString(cost));
|
||||
}
|
||||
if (_flatpack.TryGetFlatpackResultPrototype(_currentBoard.Value, out var prototype) &&
|
||||
_flatpack.TryGetFlatpackCreationCost(flatpackerEnt, _currentBoard.Value, out var cost))
|
||||
{
|
||||
var proto = _prototypeManager.Index<EntityPrototype>(prototype);
|
||||
MachineSprite.SetPrototype(prototype);
|
||||
MachineNameLabel.SetMessage(proto.Name);
|
||||
CostLabel.SetMarkup(GetCostString(cost));
|
||||
CostHeaderLabel.Visible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
MachineSprite.SetPrototype(NoBoardEffectId);
|
||||
CostLabel.SetMessage(Loc.GetString("flatpacker-ui-no-board-label"));
|
||||
MachineNameLabel.SetMessage(" ");
|
||||
CostLabel.SetMarkup(Loc.GetString("flatpacker-ui-board-invalid-label"));
|
||||
MachineNameLabel.SetMessage(string.Empty);
|
||||
PackButton.Disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<!-- Work around https://github.com/dotnet/project-system/issues/4314 -->
|
||||
<TargetFramework>$(TargetFramework)</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<OutputPath>..\bin\Content.Client\</OutputPath>
|
||||
<OutputType Condition="'$(FullRelease)' != 'True'">Exe</OutputType>
|
||||
<WarningsAsErrors>RA0032;nullable</WarningsAsErrors>
|
||||
<Nullable>enable</Nullable>
|
||||
<Configurations>Debug;Release;Tools;DebugOpt</Configurations>
|
||||
<Platforms>AnyCPU</Platforms>
|
||||
</PropertyGroup>
|
||||
<Import Project="../MSBuild/Content.props" />
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Nett" />
|
||||
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" />
|
||||
<PackageReference Include="Pidgin" />
|
||||
<PackageReference Include="Robust.Shared.AuthLib" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\RobustToolbox\Imports\Lidgren.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Client.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Shared.props" />
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\RobustToolbox\Lidgren.Network\Lidgren.Network.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Shared\Robust.Shared.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Client\Robust.Client.csproj" />
|
||||
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
|
||||
<ProjectReference Include="..\Corvax\Content.Corvax.Interfaces.Shared\Content.Corvax.Interfaces.Shared.csproj" />
|
||||
<ProjectReference Include="..\Corvax\Content.Corvax.Interfaces.Client\Content.Corvax.Interfaces.Client.csproj" />
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<RichTextLabel xmlns="https://spacestation14.io"></RichTextLabel>
|
||||
56
Content.Client/Corvax/Guidebook/Controls/FTLTextpart.xaml.cs
Normal file
56
Content.Client/Corvax/Guidebook/Controls/FTLTextpart.xaml.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Client.Guidebook;
|
||||
using Content.Client.Guidebook.Richtext;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Corvax.Guidebook.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Control for embedding text with fluent support into guidebook/document
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class FTLTextpart : RichTextLabel, IDocumentTag
|
||||
{
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
[Dependency] private readonly DocumentParsingManager _documentParsingManager = default!;
|
||||
|
||||
private readonly ISawmill _sawmill;
|
||||
|
||||
public FTLTextpart()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
_sawmill = _logManager.GetSawmill("guidebook.loc");
|
||||
MouseFilter = MouseFilterMode.Stop;
|
||||
}
|
||||
|
||||
public bool TryParseTag(Dictionary<string, string> args, [NotNullWhen(true)] out Control? control)
|
||||
{
|
||||
if (!args.TryGetValue("Key", out var key))
|
||||
{
|
||||
_sawmill.Error("Fluent tag cannot be found");
|
||||
control = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_loc.TryGetString(key, out var fluentString))
|
||||
{
|
||||
var doc = new Document();
|
||||
if (_documentParsingManager.TryAddMarkup(doc, fluentString))
|
||||
{
|
||||
control = doc;
|
||||
return true;
|
||||
}
|
||||
|
||||
control = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
_sawmill.Error($"Fluent key {key} cannot be found");
|
||||
control = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
44
Content.Client/Corvax/Markup/TooltipTag.cs
Normal file
44
Content.Client/Corvax/Markup/TooltipTag.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.RichText;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Corvax.Markup;
|
||||
|
||||
public sealed class TooltipTag : IMarkupTagHandler
|
||||
{
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
|
||||
public string Name => "tooltip";
|
||||
|
||||
public bool TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
|
||||
{
|
||||
if (!node.Value.TryGetString(out var tooltipKey) || string.IsNullOrWhiteSpace(tooltipKey))
|
||||
{
|
||||
control = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_loc.TryGetString(tooltipKey, out var tooltipText))
|
||||
tooltipText = tooltipKey;
|
||||
|
||||
var visibleText = tooltipText;
|
||||
if (node.Attributes.TryGetValue("text", out var textParam) && textParam.TryGetString(out var explicitText)
|
||||
&& !string.IsNullOrEmpty(explicitText))
|
||||
{
|
||||
visibleText = explicitText;
|
||||
}
|
||||
|
||||
var label = new Label
|
||||
{
|
||||
Text = visibleText,
|
||||
MouseFilter = Control.MouseFilterMode.Stop,
|
||||
ToolTip = tooltipText,
|
||||
FontColorOverride = Color.LightYellow
|
||||
};
|
||||
|
||||
control = label;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -87,9 +87,14 @@ public sealed class TTSSystem : EntitySystem
|
||||
|
||||
if (ev.SourceUid != null)
|
||||
{
|
||||
if (!TryGetEntity(ev.SourceUid.Value, out _))
|
||||
return;
|
||||
var sourceUid = GetEntity(ev.SourceUid.Value);
|
||||
|
||||
if (!Exists(sourceUid) || Deleted(sourceUid))
|
||||
{
|
||||
_contentRoot.RemoveFile(filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
_audio.PlayEntity(audioResource.AudioStream, sourceUid, soundSpecifier, audioParams);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -73,7 +73,19 @@ public sealed class PuddleSystem : SharedPuddleSystem
|
||||
// Maybe someday we'll have clientside prediction for entity spawning, but not today.
|
||||
// Until then, these methods do nothing on the client.
|
||||
/// <inheritdoc/>
|
||||
public override bool TrySplashSpillAt(EntityUid uid, EntityCoordinates coordinates, Solution solution, out EntityUid puddleUid, bool sound = true, EntityUid? user = null)
|
||||
public override bool TrySplashSpillAt(Entity<SpillableComponent?> entity, EntityCoordinates coordinates, out EntityUid puddleUid, out Solution solution, bool sound = true, EntityUid? user = null)
|
||||
{
|
||||
puddleUid = EntityUid.Invalid;
|
||||
solution = new Solution();
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool TrySplashSpillAt(EntityUid entity,
|
||||
EntityCoordinates coordinates,
|
||||
Solution spilled,
|
||||
out EntityUid puddleUid,
|
||||
bool sound = true,
|
||||
EntityUid? user = null)
|
||||
{
|
||||
puddleUid = EntityUid.Invalid;
|
||||
return false;
|
||||
|
||||
@@ -42,7 +42,7 @@ public sealed class GuidebookRichPrototypeLink : Control, IPrototypeLinkControl
|
||||
public void SetMessage(FormattedMessage message)
|
||||
{
|
||||
_message = message;
|
||||
_richTextLabel.SetMessage(_message);
|
||||
_richTextLabel.SetMessage(_message, tagsAllowed: null);
|
||||
}
|
||||
|
||||
public IPrototype? LinkedPrototype { get; set; }
|
||||
|
||||
@@ -39,6 +39,13 @@ public sealed partial class DocumentParsingManager
|
||||
var whitespaceAndCommentParser = SkipWhitespaces.Then(Try(String("<!--").Then(Parser<char>.Any.SkipUntil(Try(String("-->"))))).SkipMany());
|
||||
|
||||
_controlParser = OneOf(_tagParser, TryHeaderControl, ListControlParser, TextControlParser)
|
||||
.Select(control =>
|
||||
{
|
||||
if (!_lastControlWasList)
|
||||
_numberedListCounter = 0;
|
||||
_lastControlWasList = false;
|
||||
return control;
|
||||
}) // Corvax-Guidebook
|
||||
.Before(whitespaceAndCommentParser);
|
||||
|
||||
foreach (var typ in _reflectionManager.GetAllChildren<IDocumentTag>())
|
||||
@@ -70,6 +77,8 @@ public sealed partial class DocumentParsingManager
|
||||
{
|
||||
try
|
||||
{
|
||||
_numberedListCounter = 0; // Corvax-Guidebook
|
||||
_lastControlWasList = false; // Corvax-Guidebook
|
||||
foreach (var child in ControlParser.ParseOrThrow(text))
|
||||
{
|
||||
control.AddChild(child);
|
||||
|
||||
@@ -48,13 +48,16 @@ public sealed partial class DocumentParsingManager
|
||||
private static readonly Parser<char, Unit> TryStartList =
|
||||
Try(SkipNewline.Then(SkipWhitespaces).Then(Char('-'))).Then(SkipWhitespaces);
|
||||
|
||||
private static readonly Parser<char, Unit> TryStartNumber =
|
||||
Try(SkipNewline.Then(SkipWhitespaces).Then(Token(char.IsDigit).AtLeastOnceString().Before(Char('.')))).Then(SkipWhitespaces); // Corvax-Guidebook
|
||||
|
||||
private static readonly Parser<char, Unit> TryStartTag = Try(Char('<')).Then(SkipWhitespaces);
|
||||
|
||||
private static readonly Parser<char, Unit> TryStartParagraph =
|
||||
Try(SkipNewline.Then(SkipNewline)).Then(SkipWhitespaces);
|
||||
|
||||
private static readonly Parser<char, Unit> TryLookTextEnd =
|
||||
Lookahead(OneOf(TryStartTag, TryStartList, TryStartParagraph, Try(Whitespace.SkipUntil(End))));
|
||||
Lookahead(OneOf(TryStartTag, TryStartList, TryStartNumber, TryStartParagraph, Try(Whitespace.SkipUntil(End)))); // Corvax-Guidebook: TryStartNumber
|
||||
|
||||
private static readonly Parser<char, string> TextParser =
|
||||
TextChar.AtLeastOnceUntil(TryLookTextEnd).Select(string.Concat);
|
||||
@@ -82,7 +85,7 @@ public sealed partial class DocumentParsingManager
|
||||
}
|
||||
|
||||
msg.Pop();
|
||||
rt.SetMessage(msg);
|
||||
rt.SetMessage(msg, tagsAllowed: null);
|
||||
return rt;
|
||||
},
|
||||
TextParser)
|
||||
@@ -121,17 +124,46 @@ public sealed partial class DocumentParsingManager
|
||||
|
||||
private static readonly Parser<char, Control> TryHeaderControl = OneOf(TertiaryHeaderControlParser, SubHeaderControlParser, HeaderControlParser);
|
||||
|
||||
private static readonly Parser<char, Control> ListControlParser = Try(Char('-'))
|
||||
// Corvax-Guidebook-Start
|
||||
private static readonly Parser<char, Control> BulletListControlParser = Try(Char('-'))
|
||||
.Then(SkipWhitespaces)
|
||||
.Then(Map(
|
||||
control => new BoxContainer
|
||||
control =>
|
||||
{
|
||||
Children = { new Label { Text = ListBullet, VerticalAlignment = VAlignment.Top }, control },
|
||||
Orientation = LayoutOrientation.Horizontal
|
||||
_lastControlWasList = true;
|
||||
return new BoxContainer
|
||||
{
|
||||
Children = { new Label { Text = ListBullet, VerticalAlignment = VAlignment.Top }, control },
|
||||
Orientation = LayoutOrientation.Horizontal
|
||||
};
|
||||
},
|
||||
TextControlParser)
|
||||
.Cast<Control>())
|
||||
.Labelled("list");
|
||||
.Labelled("bulletlist");
|
||||
|
||||
private static int _numberedListCounter;
|
||||
private static bool _lastControlWasList;
|
||||
|
||||
// Parser for numbered lists like "1. text". The displayed number is auto-incremented across consecutive items.
|
||||
private static readonly Parser<char, Control> NumberedListControlParser = Try(Token(char.IsDigit).AtLeastOnceString().Before(Char('.')))
|
||||
.Then(SkipWhitespaces)
|
||||
.Then(Map(control =>
|
||||
{
|
||||
_numberedListCounter++;
|
||||
_lastControlWasList = true;
|
||||
var label = new Label { Text = $" {_numberedListCounter}. ", VerticalAlignment = VAlignment.Top };
|
||||
return new BoxContainer
|
||||
{
|
||||
Children = { label, control },
|
||||
Orientation = LayoutOrientation.Horizontal
|
||||
};
|
||||
},
|
||||
TextControlParser)
|
||||
.Cast<Control>())
|
||||
.Labelled("numberedlist");
|
||||
|
||||
private static readonly Parser<char, Control> ListControlParser = OneOf(NumberedListControlParser, BulletListControlParser);
|
||||
// Corvax-Guidebook-End
|
||||
|
||||
#region Text Parsing
|
||||
|
||||
|
||||
53
Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml
Normal file
53
Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml
Normal file
@@ -0,0 +1,53 @@
|
||||
<BoxContainer
|
||||
xmlns="https://spacestation14.io"
|
||||
VerticalExpand="True"
|
||||
Orientation="Vertical">
|
||||
<Label
|
||||
Name="NoPatientDataText"
|
||||
Text="{Loc health-analyzer-window-no-patient-data-text}" />
|
||||
|
||||
<BoxContainer
|
||||
Name="PatientDataContainer"
|
||||
Margin="0 0 0 5"
|
||||
Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal" Margin="0 0 0 5">
|
||||
<SpriteView OverrideDirection="South" Scale="2 2" Name="SpriteView" Access="Public" SetSize="64 64" />
|
||||
<TextureRect Name="NoDataTex" Access="Public" SetSize="64 64" Visible="false" Stretch="KeepAspectCentered" TexturePath="/Textures/Interface/Misc/health_analyzer_out_of_range.png"/>
|
||||
<BoxContainer Margin="5 0 0 0" Orientation="Vertical" VerticalAlignment="Top">
|
||||
<RichTextLabel Name="NameLabel" SetWidth="150" />
|
||||
<Label Name="SpeciesLabel" VerticalAlignment="Top" StyleClasses="LabelSubText" />
|
||||
</BoxContainer>
|
||||
<Label Margin="0 0 5 0" HorizontalExpand="True" HorizontalAlignment="Right" VerticalExpand="True"
|
||||
VerticalAlignment="Top" Name="ScanModeLabel"
|
||||
Text="{Loc 'health-analyzer-window-entity-unknown-text'}" />
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
|
||||
<GridContainer Margin="0 5 0 0" Columns="2">
|
||||
<Label Text="{Loc 'health-analyzer-window-entity-status-text'}" />
|
||||
<Label Name="StatusLabel" />
|
||||
<Label Text="{Loc 'health-analyzer-window-entity-temperature-text'}" />
|
||||
<Label Name="TemperatureLabel" />
|
||||
<Label Text="{Loc 'health-analyzer-window-entity-blood-level-text'}" />
|
||||
<Label Name="BloodLabel" />
|
||||
<Label Text="{Loc 'health-analyzer-window-entity-damage-total-text'}" />
|
||||
<Label Name="DamageLabel" />
|
||||
</GridContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer Name="AlertsDivider" Visible="False" StyleClasses="LowDivider" />
|
||||
|
||||
<BoxContainer Name="AlertsContainer" Visible="False" Margin="0 5" Orientation="Vertical" HorizontalAlignment="Center">
|
||||
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
|
||||
<BoxContainer
|
||||
Name="GroupsContainer"
|
||||
Margin="0 5 0 5"
|
||||
Orientation="Vertical">
|
||||
</BoxContainer>
|
||||
|
||||
</BoxContainer>
|
||||
241
Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml.cs
Normal file
241
Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml.cs
Normal file
@@ -0,0 +1,241 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.MedicalScanner;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
namespace Content.Client.HealthAnalyzer.UI;
|
||||
|
||||
// Health analyzer UI is split from its window because it's used by both the
|
||||
// health analyzer item and the cryo pod UI.
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class HealthAnalyzerControl : BoxContainer
|
||||
{
|
||||
private readonly IEntityManager _entityManager;
|
||||
private readonly SpriteSystem _spriteSystem;
|
||||
private readonly IPrototypeManager _prototypes;
|
||||
private readonly IResourceCache _cache;
|
||||
|
||||
public HealthAnalyzerControl()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
var dependencies = IoCManager.Instance!;
|
||||
_entityManager = dependencies.Resolve<IEntityManager>();
|
||||
_spriteSystem = _entityManager.System<SpriteSystem>();
|
||||
_prototypes = dependencies.Resolve<IPrototypeManager>();
|
||||
_cache = dependencies.Resolve<IResourceCache>();
|
||||
}
|
||||
|
||||
public void Populate(HealthAnalyzerUiState state)
|
||||
{
|
||||
var target = _entityManager.GetEntity(state.TargetEntity);
|
||||
|
||||
if (target == null
|
||||
|| !_entityManager.TryGetComponent<DamageableComponent>(target, out var damageable))
|
||||
{
|
||||
NoPatientDataText.Visible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
NoPatientDataText.Visible = false;
|
||||
|
||||
// Scan Mode
|
||||
|
||||
ScanModeLabel.Text = state.ScanMode.HasValue
|
||||
? state.ScanMode.Value
|
||||
? Loc.GetString("health-analyzer-window-scan-mode-active")
|
||||
: Loc.GetString("health-analyzer-window-scan-mode-inactive")
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-text");
|
||||
|
||||
ScanModeLabel.FontColorOverride = state.ScanMode.HasValue && state.ScanMode.Value ? Color.Green : Color.Red;
|
||||
|
||||
// Patient Information
|
||||
|
||||
SpriteView.SetEntity(target.Value);
|
||||
SpriteView.Visible = state.ScanMode.HasValue && state.ScanMode.Value;
|
||||
NoDataTex.Visible = !SpriteView.Visible;
|
||||
|
||||
var name = new FormattedMessage();
|
||||
name.PushColor(Color.White);
|
||||
name.AddText(_entityManager.HasComponent<MetaDataComponent>(target.Value)
|
||||
? Identity.Name(target.Value, _entityManager)
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-text"));
|
||||
NameLabel.SetMessage(name);
|
||||
|
||||
SpeciesLabel.Text =
|
||||
_entityManager.TryGetComponent<HumanoidAppearanceComponent>(target.Value,
|
||||
out var humanoidAppearanceComponent)
|
||||
? Loc.GetString(_prototypes.Index<SpeciesPrototype>(humanoidAppearanceComponent.Species).Name)
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-species-text");
|
||||
|
||||
// Basic Diagnostic
|
||||
|
||||
TemperatureLabel.Text = !float.IsNaN(state.Temperature)
|
||||
? $"{state.Temperature - Atmospherics.T0C:F1} °C ({state.Temperature:F1} K)"
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-value-text");
|
||||
|
||||
BloodLabel.Text = !float.IsNaN(state.BloodLevel)
|
||||
? $"{state.BloodLevel * 100:F1} %"
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-value-text");
|
||||
|
||||
StatusLabel.Text =
|
||||
_entityManager.TryGetComponent<MobStateComponent>(target.Value, out var mobStateComponent)
|
||||
? GetStatus(mobStateComponent.CurrentState)
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-text");
|
||||
|
||||
// Total Damage
|
||||
|
||||
DamageLabel.Text = damageable.TotalDamage.ToString();
|
||||
|
||||
// Alerts
|
||||
|
||||
var showAlerts = state.Unrevivable == true || state.Bleeding == true;
|
||||
|
||||
AlertsDivider.Visible = showAlerts;
|
||||
AlertsContainer.Visible = showAlerts;
|
||||
|
||||
if (showAlerts)
|
||||
AlertsContainer.RemoveAllChildren();
|
||||
|
||||
if (state.Unrevivable == true)
|
||||
AlertsContainer.AddChild(new RichTextLabel
|
||||
{
|
||||
Text = Loc.GetString("health-analyzer-window-entity-unrevivable-text"),
|
||||
Margin = new Thickness(0, 4),
|
||||
MaxWidth = 300
|
||||
});
|
||||
|
||||
if (state.Bleeding == true)
|
||||
AlertsContainer.AddChild(new RichTextLabel
|
||||
{
|
||||
Text = Loc.GetString("health-analyzer-window-entity-bleeding-text"),
|
||||
Margin = new Thickness(0, 4),
|
||||
MaxWidth = 300
|
||||
});
|
||||
|
||||
// Damage Groups
|
||||
|
||||
var damageSortedGroups =
|
||||
damageable.DamagePerGroup.OrderByDescending(damage => damage.Value)
|
||||
.ToDictionary(x => x.Key, x => x.Value);
|
||||
|
||||
IReadOnlyDictionary<string, FixedPoint2> damagePerType = damageable.Damage.DamageDict;
|
||||
|
||||
DrawDiagnosticGroups(damageSortedGroups, damagePerType);
|
||||
}
|
||||
|
||||
private static string GetStatus(MobState mobState)
|
||||
{
|
||||
return mobState switch
|
||||
{
|
||||
MobState.Alive => Loc.GetString("health-analyzer-window-entity-alive-text"),
|
||||
MobState.Critical => Loc.GetString("health-analyzer-window-entity-critical-text"),
|
||||
MobState.Dead => Loc.GetString("health-analyzer-window-entity-dead-text"),
|
||||
_ => Loc.GetString("health-analyzer-window-entity-unknown-text"),
|
||||
};
|
||||
}
|
||||
|
||||
private void DrawDiagnosticGroups(
|
||||
Dictionary<string, FixedPoint2> groups,
|
||||
IReadOnlyDictionary<string, FixedPoint2> damageDict)
|
||||
{
|
||||
GroupsContainer.RemoveAllChildren();
|
||||
|
||||
foreach (var (damageGroupId, damageAmount) in groups)
|
||||
{
|
||||
if (damageAmount == 0)
|
||||
continue;
|
||||
|
||||
var groupTitleText = $"{Loc.GetString(
|
||||
"health-analyzer-window-damage-group-text",
|
||||
("damageGroup", _prototypes.Index<DamageGroupPrototype>(damageGroupId).LocalizedName),
|
||||
("amount", damageAmount)
|
||||
)}";
|
||||
|
||||
var groupContainer = new BoxContainer
|
||||
{
|
||||
Align = AlignMode.Begin,
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
};
|
||||
|
||||
groupContainer.AddChild(CreateDiagnosticGroupTitle(groupTitleText, damageGroupId));
|
||||
|
||||
GroupsContainer.AddChild(groupContainer);
|
||||
|
||||
// Show the damage for each type in that group.
|
||||
var group = _prototypes.Index<DamageGroupPrototype>(damageGroupId);
|
||||
|
||||
foreach (var type in group.DamageTypes)
|
||||
{
|
||||
if (!damageDict.TryGetValue(type, out var typeAmount) || typeAmount <= 0)
|
||||
continue;
|
||||
|
||||
var damageString = Loc.GetString(
|
||||
"health-analyzer-window-damage-type-text",
|
||||
("damageType", _prototypes.Index<DamageTypePrototype>(type).LocalizedName),
|
||||
("amount", typeAmount)
|
||||
);
|
||||
|
||||
groupContainer.AddChild(CreateDiagnosticItemLabel(damageString.Insert(0, " · ")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Texture GetTexture(string texture)
|
||||
{
|
||||
var rsiPath = new ResPath("/Textures/Objects/Devices/health_analyzer.rsi");
|
||||
var rsiSprite = new SpriteSpecifier.Rsi(rsiPath, texture);
|
||||
|
||||
var rsi = _cache.GetResource<RSIResource>(rsiSprite.RsiPath).RSI;
|
||||
if (!rsi.TryGetState(rsiSprite.RsiState, out var state))
|
||||
{
|
||||
rsiSprite = new SpriteSpecifier.Rsi(rsiPath, "unknown");
|
||||
}
|
||||
|
||||
return _spriteSystem.Frame0(rsiSprite);
|
||||
}
|
||||
|
||||
private static Label CreateDiagnosticItemLabel(string text)
|
||||
{
|
||||
return new Label
|
||||
{
|
||||
Text = text,
|
||||
};
|
||||
}
|
||||
|
||||
private BoxContainer CreateDiagnosticGroupTitle(string text, string id)
|
||||
{
|
||||
var rootContainer = new BoxContainer
|
||||
{
|
||||
Margin = new Thickness(0, 6, 0, 0),
|
||||
VerticalAlignment = VAlignment.Bottom,
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
};
|
||||
|
||||
rootContainer.AddChild(new TextureRect
|
||||
{
|
||||
SetSize = new Vector2(30, 30),
|
||||
Texture = GetTexture(id.ToLower())
|
||||
});
|
||||
|
||||
rootContainer.AddChild(CreateDiagnosticItemLabel(text));
|
||||
|
||||
return rootContainer;
|
||||
}
|
||||
}
|
||||
@@ -1,64 +1,15 @@
|
||||
<controls:FancyWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:ui="clr-namespace:Content.Client.HealthAnalyzer.UI"
|
||||
MaxHeight="525"
|
||||
MinWidth="300">
|
||||
<ScrollContainer
|
||||
Margin="5 5 5 5"
|
||||
ReturnMeasure="True"
|
||||
VerticalExpand="True">
|
||||
<BoxContainer
|
||||
Name="RootContainer"
|
||||
VerticalExpand="True"
|
||||
Orientation="Vertical">
|
||||
<Label
|
||||
Name="NoPatientDataText"
|
||||
Text="{Loc health-analyzer-window-no-patient-data-text}" />
|
||||
|
||||
<BoxContainer
|
||||
Name="PatientDataContainer"
|
||||
Margin="0 0 0 5"
|
||||
Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal" Margin="0 0 0 5">
|
||||
<SpriteView OverrideDirection="South" Scale="2 2" Name="SpriteView" Access="Public" SetSize="64 64" />
|
||||
<TextureRect Name="NoDataTex" Access="Public" SetSize="64 64" Visible="false" Stretch="KeepAspectCentered" TexturePath="/Textures/Interface/Misc/health_analyzer_out_of_range.png"/>
|
||||
<BoxContainer Margin="5 0 0 0" Orientation="Vertical" VerticalAlignment="Top">
|
||||
<RichTextLabel Name="NameLabel" SetWidth="150" />
|
||||
<Label Name="SpeciesLabel" VerticalAlignment="Top" StyleClasses="LabelSubText" />
|
||||
</BoxContainer>
|
||||
<Label Margin="0 0 5 0" HorizontalExpand="True" HorizontalAlignment="Right" VerticalExpand="True"
|
||||
VerticalAlignment="Top" Name="ScanModeLabel"
|
||||
Text="{Loc 'health-analyzer-window-entity-unknown-text'}" />
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
|
||||
<GridContainer Margin="0 5 0 0" Columns="2">
|
||||
<Label Text="{Loc 'health-analyzer-window-entity-status-text'}" />
|
||||
<Label Name="StatusLabel" />
|
||||
<Label Text="{Loc 'health-analyzer-window-entity-temperature-text'}" />
|
||||
<Label Name="TemperatureLabel" />
|
||||
<Label Text="{Loc 'health-analyzer-window-entity-blood-level-text'}" />
|
||||
<Label Name="BloodLabel" />
|
||||
<Label Text="{Loc 'health-analyzer-window-entity-damage-total-text'}" />
|
||||
<Label Name="DamageLabel" />
|
||||
</GridContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer Name="AlertsDivider" Visible="False" StyleClasses="LowDivider" />
|
||||
|
||||
<BoxContainer Name="AlertsContainer" Visible="False" Margin="0 5" Orientation="Vertical" HorizontalAlignment="Center">
|
||||
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
|
||||
<BoxContainer
|
||||
Name="GroupsContainer"
|
||||
Margin="0 5 0 5"
|
||||
Orientation="Vertical">
|
||||
</BoxContainer>
|
||||
|
||||
</BoxContainer>
|
||||
<ui:HealthAnalyzerControl
|
||||
Name="HealthAnalyzer"/>
|
||||
</ScrollContainer>
|
||||
</controls:FancyWindow>
|
||||
|
||||
@@ -1,241 +1,20 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.MedicalScanner;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.HealthAnalyzer.UI
|
||||
namespace Content.Client.HealthAnalyzer.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class HealthAnalyzerWindow : FancyWindow
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class HealthAnalyzerWindow : FancyWindow
|
||||
public HealthAnalyzerWindow()
|
||||
{
|
||||
private readonly IEntityManager _entityManager;
|
||||
private readonly SpriteSystem _spriteSystem;
|
||||
private readonly IPrototypeManager _prototypes;
|
||||
private readonly IResourceCache _cache;
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public HealthAnalyzerWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
var dependencies = IoCManager.Instance!;
|
||||
_entityManager = dependencies.Resolve<IEntityManager>();
|
||||
_spriteSystem = _entityManager.System<SpriteSystem>();
|
||||
_prototypes = dependencies.Resolve<IPrototypeManager>();
|
||||
_cache = dependencies.Resolve<IResourceCache>();
|
||||
}
|
||||
|
||||
public void Populate(HealthAnalyzerScannedUserMessage msg)
|
||||
{
|
||||
var target = _entityManager.GetEntity(msg.TargetEntity);
|
||||
|
||||
if (target == null
|
||||
|| !_entityManager.TryGetComponent<DamageableComponent>(target, out var damageable))
|
||||
{
|
||||
NoPatientDataText.Visible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
NoPatientDataText.Visible = false;
|
||||
|
||||
// Scan Mode
|
||||
|
||||
ScanModeLabel.Text = msg.ScanMode.HasValue
|
||||
? msg.ScanMode.Value
|
||||
? Loc.GetString("health-analyzer-window-scan-mode-active")
|
||||
: Loc.GetString("health-analyzer-window-scan-mode-inactive")
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-text");
|
||||
|
||||
ScanModeLabel.FontColorOverride = msg.ScanMode.HasValue && msg.ScanMode.Value ? Color.Green : Color.Red;
|
||||
|
||||
// Patient Information
|
||||
|
||||
SpriteView.SetEntity(target.Value);
|
||||
SpriteView.Visible = msg.ScanMode.HasValue && msg.ScanMode.Value;
|
||||
NoDataTex.Visible = !SpriteView.Visible;
|
||||
|
||||
var name = new FormattedMessage();
|
||||
name.PushColor(Color.White);
|
||||
name.AddText(_entityManager.HasComponent<MetaDataComponent>(target.Value)
|
||||
? Identity.Name(target.Value, _entityManager)
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-text"));
|
||||
NameLabel.SetMessage(name);
|
||||
|
||||
SpeciesLabel.Text =
|
||||
_entityManager.TryGetComponent<HumanoidAppearanceComponent>(target.Value,
|
||||
out var humanoidAppearanceComponent)
|
||||
? Loc.GetString(_prototypes.Index<SpeciesPrototype>(humanoidAppearanceComponent.Species).Name)
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-species-text");
|
||||
|
||||
// Basic Diagnostic
|
||||
|
||||
TemperatureLabel.Text = !float.IsNaN(msg.Temperature)
|
||||
? $"{msg.Temperature - Atmospherics.T0C:F1} °C ({msg.Temperature:F1} K)"
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-value-text");
|
||||
|
||||
BloodLabel.Text = !float.IsNaN(msg.BloodLevel)
|
||||
? $"{msg.BloodLevel * 100:F1} %"
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-value-text");
|
||||
|
||||
StatusLabel.Text =
|
||||
_entityManager.TryGetComponent<MobStateComponent>(target.Value, out var mobStateComponent)
|
||||
? GetStatus(mobStateComponent.CurrentState)
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-text");
|
||||
|
||||
// Total Damage
|
||||
|
||||
DamageLabel.Text = damageable.TotalDamage.ToString();
|
||||
|
||||
// Alerts
|
||||
|
||||
var showAlerts = msg.Unrevivable == true || msg.Bleeding == true;
|
||||
|
||||
AlertsDivider.Visible = showAlerts;
|
||||
AlertsContainer.Visible = showAlerts;
|
||||
|
||||
if (showAlerts)
|
||||
AlertsContainer.RemoveAllChildren();
|
||||
|
||||
if (msg.Unrevivable == true)
|
||||
AlertsContainer.AddChild(new RichTextLabel
|
||||
{
|
||||
Text = Loc.GetString("health-analyzer-window-entity-unrevivable-text"),
|
||||
Margin = new Thickness(0, 4),
|
||||
MaxWidth = 300
|
||||
});
|
||||
|
||||
if (msg.Bleeding == true)
|
||||
AlertsContainer.AddChild(new RichTextLabel
|
||||
{
|
||||
Text = Loc.GetString("health-analyzer-window-entity-bleeding-text"),
|
||||
Margin = new Thickness(0, 4),
|
||||
MaxWidth = 300
|
||||
});
|
||||
|
||||
// Damage Groups
|
||||
|
||||
var damageSortedGroups =
|
||||
damageable.DamagePerGroup.OrderByDescending(damage => damage.Value)
|
||||
.ToDictionary(x => x.Key, x => x.Value);
|
||||
|
||||
IReadOnlyDictionary<string, FixedPoint2> damagePerType = damageable.Damage.DamageDict;
|
||||
|
||||
DrawDiagnosticGroups(damageSortedGroups, damagePerType);
|
||||
}
|
||||
|
||||
private static string GetStatus(MobState mobState)
|
||||
{
|
||||
return mobState switch
|
||||
{
|
||||
MobState.Alive => Loc.GetString("health-analyzer-window-entity-alive-text"),
|
||||
MobState.Critical => Loc.GetString("health-analyzer-window-entity-critical-text"),
|
||||
MobState.Dead => Loc.GetString("health-analyzer-window-entity-dead-text"),
|
||||
_ => Loc.GetString("health-analyzer-window-entity-unknown-text"),
|
||||
};
|
||||
}
|
||||
|
||||
private void DrawDiagnosticGroups(
|
||||
Dictionary<string, FixedPoint2> groups,
|
||||
IReadOnlyDictionary<string, FixedPoint2> damageDict)
|
||||
{
|
||||
GroupsContainer.RemoveAllChildren();
|
||||
|
||||
foreach (var (damageGroupId, damageAmount) in groups)
|
||||
{
|
||||
if (damageAmount == 0)
|
||||
continue;
|
||||
|
||||
var groupTitleText = $"{Loc.GetString(
|
||||
"health-analyzer-window-damage-group-text",
|
||||
("damageGroup", _prototypes.Index<DamageGroupPrototype>(damageGroupId).LocalizedName),
|
||||
("amount", damageAmount)
|
||||
)}";
|
||||
|
||||
var groupContainer = new BoxContainer
|
||||
{
|
||||
Align = BoxContainer.AlignMode.Begin,
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
};
|
||||
|
||||
groupContainer.AddChild(CreateDiagnosticGroupTitle(groupTitleText, damageGroupId));
|
||||
|
||||
GroupsContainer.AddChild(groupContainer);
|
||||
|
||||
// Show the damage for each type in that group.
|
||||
var group = _prototypes.Index<DamageGroupPrototype>(damageGroupId);
|
||||
|
||||
foreach (var type in group.DamageTypes)
|
||||
{
|
||||
if (!damageDict.TryGetValue(type, out var typeAmount) || typeAmount <= 0)
|
||||
continue;
|
||||
|
||||
var damageString = Loc.GetString(
|
||||
"health-analyzer-window-damage-type-text",
|
||||
("damageType", _prototypes.Index<DamageTypePrototype>(type).LocalizedName),
|
||||
("amount", typeAmount)
|
||||
);
|
||||
|
||||
groupContainer.AddChild(CreateDiagnosticItemLabel(damageString.Insert(0, " · ")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Texture GetTexture(string texture)
|
||||
{
|
||||
var rsiPath = new ResPath("/Textures/Objects/Devices/health_analyzer.rsi");
|
||||
var rsiSprite = new SpriteSpecifier.Rsi(rsiPath, texture);
|
||||
|
||||
var rsi = _cache.GetResource<RSIResource>(rsiSprite.RsiPath).RSI;
|
||||
if (!rsi.TryGetState(rsiSprite.RsiState, out var state))
|
||||
{
|
||||
rsiSprite = new SpriteSpecifier.Rsi(rsiPath, "unknown");
|
||||
}
|
||||
|
||||
return _spriteSystem.Frame0(rsiSprite);
|
||||
}
|
||||
|
||||
private static Label CreateDiagnosticItemLabel(string text)
|
||||
{
|
||||
return new Label
|
||||
{
|
||||
Text = text,
|
||||
};
|
||||
}
|
||||
|
||||
private BoxContainer CreateDiagnosticGroupTitle(string text, string id)
|
||||
{
|
||||
var rootContainer = new BoxContainer
|
||||
{
|
||||
Margin = new Thickness(0, 6, 0, 0),
|
||||
VerticalAlignment = VAlignment.Bottom,
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
};
|
||||
|
||||
rootContainer.AddChild(new TextureRect
|
||||
{
|
||||
SetSize = new Vector2(30, 30),
|
||||
Texture = GetTexture(id.ToLower())
|
||||
});
|
||||
|
||||
rootContainer.AddChild(CreateDiagnosticItemLabel(text));
|
||||
|
||||
return rootContainer;
|
||||
}
|
||||
public void Populate(HealthAnalyzerScannedUserMessage msg)
|
||||
{
|
||||
HealthAnalyzer.Populate(msg.State);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ public sealed partial class InfoSection : BoxContainer
|
||||
{
|
||||
TitleLabel.Text = title;
|
||||
if (markup)
|
||||
Content.SetMessage(FormattedMessage.FromMarkupOrThrow(text.Trim()));
|
||||
Content.SetMessage(FormattedMessage.FromMarkupOrThrow(text.Trim()), tagsAllowed: null);
|
||||
else
|
||||
Content.SetMessage(text);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Content.Client.Info
|
||||
}
|
||||
public void SetInfoBlob(string markup)
|
||||
{
|
||||
_richTextLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(markup));
|
||||
_richTextLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(markup), tagsAllowed: null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
using Content.Shared.Kitchen.EntitySystems;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Client.Kitchen.EntitySystems;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class ReagentGrinderSystem : SharedReagentGrinderSystem;
|
||||
@@ -20,8 +20,10 @@ namespace Content.Client.Launcher
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IClipboardManager _clipboard = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
|
||||
private LauncherConnectingGui? _control;
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
private Page _currentPage;
|
||||
private string? _connectFailReason;
|
||||
@@ -61,6 +63,8 @@ namespace Content.Client.Launcher
|
||||
{
|
||||
_control = new LauncherConnectingGui(this, _random, _prototypeManager, _cfg, _clipboard);
|
||||
|
||||
_sawmill = _logManager.GetSawmill("launcher-ui");
|
||||
|
||||
_userInterfaceManager.StateRoot.AddChild(_control);
|
||||
|
||||
_clientNetManager.ConnectFailed += OnConnectFailed;
|
||||
@@ -115,12 +119,12 @@ namespace Content.Client.Launcher
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.InfoS("launcher-ui", $"Redial not possible, no Ss14Address");
|
||||
_sawmill.Info($"Redial not possible, no Ss14Address");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.ErrorS("launcher-ui", $"Redial exception: {ex}");
|
||||
_sawmill.Error($"Redial exception: {ex}");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Prometheus;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
285
Content.Client/Medical/Cryogenics/BeakerBarChart.cs
Normal file
285
Content.Client/Medical/Cryogenics/BeakerBarChart.cs
Normal file
@@ -0,0 +1,285 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
// ReSharper disable CompareOfFloatsByEqualityOperator
|
||||
|
||||
namespace Content.Client.Medical.Cryogenics;
|
||||
|
||||
|
||||
public sealed class BeakerBarChart : Control
|
||||
{
|
||||
private sealed class Entry
|
||||
{
|
||||
public float WidthFraction; // This entry's width as a fraction of the chart's total width (between 0 and 1)
|
||||
public float TargetAmount;
|
||||
public string Uid; // This UID is used to track entries between frames, for animation.
|
||||
public string? Tooltip;
|
||||
public Color Color;
|
||||
public Label Label;
|
||||
|
||||
public Entry(string uid, Label label)
|
||||
{
|
||||
Uid = uid;
|
||||
Label = label;
|
||||
}
|
||||
}
|
||||
|
||||
public float Capacity = 50;
|
||||
|
||||
public Color NotchColor = new(1, 1, 1, 0.25f);
|
||||
public Color BackgroundColor = new(0.1f, 0.1f, 0.1f);
|
||||
|
||||
public int MediumNotchInterval = 5;
|
||||
public int BigNotchInterval = 10;
|
||||
|
||||
// When we have a very large beaker (i.e. bluespace beaker) we might need to increase the distance between notches.
|
||||
// The distance between notches is increased by ScaleMultiplier when the distance between notches is less than
|
||||
// MinSmallNotchScreenDistance in UI units.
|
||||
public int MinSmallNotchScreenDistance = 2;
|
||||
public int ScaleMultiplier = 10;
|
||||
|
||||
public float SmallNotchHeight = 0.1f;
|
||||
public float MediumNotchHeight = 0.25f;
|
||||
public float BigNotchHeight = 1f;
|
||||
|
||||
// We don't animate new entries until this control has been drawn at least once.
|
||||
private bool _hasBeenDrawn = false;
|
||||
|
||||
// This is used to keep the segments of the chart in the same order as the SetEntry calls.
|
||||
// For example: In update 1 we might get cryox, alox, bic (in that order), and in update 2 we get alox, cryox, bic.
|
||||
// To keep the order of the entries the same as the order of the SetEntry calls, we let the old cryox entry
|
||||
// disappear and create a new cryox entry behind the alox entry.
|
||||
private int _nextUpdateableEntry = 0;
|
||||
|
||||
private readonly List<Entry> _entries = new();
|
||||
|
||||
|
||||
public BeakerBarChart()
|
||||
{
|
||||
MouseFilter = MouseFilterMode.Pass;
|
||||
TooltipSupplier = SupplyTooltip;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var entry in _entries)
|
||||
{
|
||||
entry.TargetAmount = 0;
|
||||
}
|
||||
|
||||
_nextUpdateableEntry = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Either adds a new entry to the chart if the UID doesn't appear yet, or updates the amount of an existing entry.
|
||||
/// </summary>
|
||||
public void SetEntry(
|
||||
string uid,
|
||||
string label,
|
||||
float amount,
|
||||
Color color,
|
||||
Color? textColor = null,
|
||||
string? tooltip = null)
|
||||
{
|
||||
// If we can find an old entry we're allowed to update, update that one.
|
||||
if (TryFindUpdateableEntry(uid, out var index))
|
||||
{
|
||||
_entries[index].TargetAmount = amount;
|
||||
_entries[index].Tooltip = tooltip;
|
||||
_entries[index].Label.Text = label;
|
||||
_nextUpdateableEntry = index + 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise create a new entry.
|
||||
if (amount <= 0)
|
||||
return;
|
||||
|
||||
// If no text color is provided, use either white or black depending on how dark the background is.
|
||||
textColor ??= (color.R + color.G + color.B < 1.5f ? Color.White : Color.Black);
|
||||
|
||||
var childLabel = new Label
|
||||
{
|
||||
Text = label,
|
||||
ClipText = true,
|
||||
FontColorOverride = textColor,
|
||||
Margin = new Thickness(4, 0, 0, 0)
|
||||
};
|
||||
AddChild(childLabel);
|
||||
|
||||
_entries.Insert(
|
||||
_nextUpdateableEntry,
|
||||
new Entry(uid, childLabel)
|
||||
{
|
||||
WidthFraction = (_hasBeenDrawn ? 0 : amount / Capacity),
|
||||
TargetAmount = amount,
|
||||
Tooltip = tooltip,
|
||||
Color = color
|
||||
}
|
||||
);
|
||||
|
||||
_nextUpdateableEntry += 1;
|
||||
}
|
||||
|
||||
private bool TryFindUpdateableEntry(string uid, out int index)
|
||||
{
|
||||
for (int i = _nextUpdateableEntry; i < _entries.Count; i++)
|
||||
{
|
||||
if (_entries[i].Uid == uid)
|
||||
{
|
||||
index = i;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
index = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
private IEnumerable<(Entry, float xMin, float xMax)> EntryRanges(float? pixelWidth = null)
|
||||
{
|
||||
float chartWidth = pixelWidth ?? PixelWidth;
|
||||
var xStart = 0f;
|
||||
|
||||
foreach (var entry in _entries)
|
||||
{
|
||||
var entryWidth = entry.WidthFraction * chartWidth;
|
||||
var xEnd = MathF.Min(xStart + entryWidth, chartWidth);
|
||||
|
||||
yield return (entry, xStart, xEnd);
|
||||
|
||||
xStart = xEnd;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryFindEntry(float x, [NotNullWhen(true)] out Entry? entry)
|
||||
{
|
||||
foreach (var (currentEntry, xMin, xMax) in EntryRanges())
|
||||
{
|
||||
if (xMin <= x && x < xMax)
|
||||
{
|
||||
entry = currentEntry;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
entry = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
// Tween the amounts to their target amounts.
|
||||
const float tweenInverseHalfLife = 8; // Half life of tween is 1/n
|
||||
var hasChanged = false;
|
||||
|
||||
foreach (var entry in _entries)
|
||||
{
|
||||
var targetWidthFraction = entry.TargetAmount / Capacity;
|
||||
|
||||
if (entry.WidthFraction == targetWidthFraction)
|
||||
continue;
|
||||
|
||||
// Tween with lerp abuse interpolation
|
||||
entry.WidthFraction = MathHelper.Lerp(
|
||||
entry.WidthFraction,
|
||||
targetWidthFraction,
|
||||
MathHelper.Clamp01(tweenInverseHalfLife * args.DeltaSeconds)
|
||||
);
|
||||
hasChanged = true;
|
||||
|
||||
if (MathF.Abs(entry.WidthFraction - targetWidthFraction) < 0.0001f)
|
||||
entry.WidthFraction = targetWidthFraction;
|
||||
}
|
||||
|
||||
if (!hasChanged)
|
||||
return;
|
||||
|
||||
InvalidateArrange();
|
||||
|
||||
// Remove old entries whose animations have finished.
|
||||
foreach (var entry in _entries)
|
||||
{
|
||||
if (entry.WidthFraction == 0 && entry.TargetAmount == 0)
|
||||
RemoveChild(entry.Label);
|
||||
}
|
||||
|
||||
_entries.RemoveAll(entry => entry.WidthFraction == 0 && entry.TargetAmount == 0);
|
||||
}
|
||||
|
||||
protected override void MouseMove(GUIMouseMoveEventArgs args)
|
||||
{
|
||||
HideTooltip();
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
handle.DrawRect(PixelSizeBox, BackgroundColor);
|
||||
|
||||
// Draw the entry backgrounds
|
||||
foreach (var (entry, xMin, xMax) in EntryRanges())
|
||||
{
|
||||
if (xMin != xMax)
|
||||
handle.DrawRect(new(xMin, 0, xMax, PixelHeight), entry.Color);
|
||||
}
|
||||
|
||||
// Draw notches
|
||||
var unitWidth = PixelWidth / Capacity;
|
||||
var unitsPerNotch = 1;
|
||||
|
||||
while (unitWidth < MinSmallNotchScreenDistance)
|
||||
{
|
||||
// This is here for 1000u bluespace beakers. If the distance between small notches is so small that it would
|
||||
// be very ugly, we reduce the amount of notches by ScaleMultiplier (currently a factor of 10).
|
||||
// (I could use an analytical algorithm here, but it would be more difficult to read with pretty much no
|
||||
// performance benefit, since it loops zero times normally and one time for the bluespace beaker)
|
||||
unitWidth *= ScaleMultiplier;
|
||||
unitsPerNotch *= ScaleMultiplier;
|
||||
}
|
||||
|
||||
for (int i = 0; i <= Capacity / unitsPerNotch; i++)
|
||||
{
|
||||
var x = i * unitWidth;
|
||||
var height = (i % BigNotchInterval == 0 ? BigNotchHeight :
|
||||
i % MediumNotchInterval == 0 ? MediumNotchHeight :
|
||||
SmallNotchHeight) * PixelHeight;
|
||||
var start = new Vector2(x, PixelHeight);
|
||||
var end = new Vector2(x, PixelHeight - height);
|
||||
handle.DrawLine(start, end, NotchColor);
|
||||
}
|
||||
|
||||
_hasBeenDrawn = true;
|
||||
}
|
||||
|
||||
protected override Vector2 ArrangeOverride(Vector2 finalSize)
|
||||
{
|
||||
foreach (var (entry, xMin, xMax) in EntryRanges(finalSize.X))
|
||||
{
|
||||
entry.Label.Arrange(new((int)xMin, 0, (int)xMax, (int)finalSize.Y));
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
private Control? SupplyTooltip(Control sender)
|
||||
{
|
||||
var globalMousePos = UserInterfaceManager.MousePositionScaled.Position;
|
||||
var mousePos = globalMousePos - GlobalPosition;
|
||||
|
||||
if (!TryFindEntry(mousePos.X, out var entry) || entry.Tooltip == null)
|
||||
return null;
|
||||
|
||||
var msg = new FormattedMessage();
|
||||
msg.AddText(entry.Tooltip);
|
||||
|
||||
var tooltip = new Tooltip();
|
||||
tooltip.SetMessage(msg);
|
||||
return tooltip;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Medical.Cryogenics;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface;
|
||||
namespace Content.Client.Medical.Cryogenics;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class CryoPodBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
private CryoPodWindow? _window;
|
||||
|
||||
public CryoPodBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
_window = this.CreateWindowCenteredLeft<CryoPodWindow>();
|
||||
_window.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName;
|
||||
_window.OnEjectPatientPressed += EjectPatientPressed;
|
||||
_window.OnEjectBeakerPressed += EjectBeakerPressed;
|
||||
_window.OnInjectPressed += InjectPressed;
|
||||
}
|
||||
|
||||
private void EjectPatientPressed()
|
||||
{
|
||||
var isLocked =
|
||||
EntMan.TryGetComponent<CryoPodComponent>(Owner, out var cryoComp)
|
||||
&& cryoComp.Locked;
|
||||
|
||||
_window?.SetEjectErrorVisible(isLocked);
|
||||
SendMessage(new CryoPodSimpleUiMessage(CryoPodSimpleUiMessage.MessageType.EjectPatient));
|
||||
}
|
||||
|
||||
private void EjectBeakerPressed()
|
||||
{
|
||||
SendMessage(new CryoPodSimpleUiMessage(CryoPodSimpleUiMessage.MessageType.EjectBeaker));
|
||||
}
|
||||
|
||||
private void InjectPressed(FixedPoint2 transferAmount)
|
||||
{
|
||||
SendMessage(new CryoPodInjectUiMessage(transferAmount));
|
||||
}
|
||||
|
||||
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
|
||||
{
|
||||
if (_window != null && message is CryoPodUserMessage cryoMsg)
|
||||
{
|
||||
_window.Populate(cryoMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ namespace Content.Client.Medical.Cryogenics;
|
||||
|
||||
public sealed class CryoPodSystem : SharedCryoPodSystem
|
||||
{
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
@@ -46,8 +45,8 @@ public sealed class CryoPodSystem : SharedCryoPodSystem
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_appearance.TryGetData<bool>(uid, CryoPodVisuals.ContainsEntity, out var isOpen, args.Component)
|
||||
|| !_appearance.TryGetData<bool>(uid, CryoPodVisuals.IsOn, out var isOn, args.Component))
|
||||
if (!Appearance.TryGetData<bool>(uid, CryoPodVisuals.ContainsEntity, out var isOpen, args.Component)
|
||||
|| !Appearance.TryGetData<bool>(uid, CryoPodVisuals.IsOn, out var isOn, args.Component))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -64,6 +63,11 @@ public sealed class CryoPodSystem : SharedCryoPodSystem
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), CryoPodVisualLayers.Cover, true);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateUi(Entity<CryoPodComponent> cryoPod)
|
||||
{
|
||||
// Atmos and health scanner aren't predicted currently...
|
||||
}
|
||||
}
|
||||
|
||||
public enum CryoPodVisualLayers : byte
|
||||
|
||||
232
Content.Client/Medical/Cryogenics/CryoPodWindow.xaml
Normal file
232
Content.Client/Medical/Cryogenics/CryoPodWindow.xaml
Normal file
@@ -0,0 +1,232 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
xmlns:health="clr-namespace:Content.Client.HealthAnalyzer.UI"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:cryogenics="clr-namespace:Content.Client.Medical.Cryogenics"
|
||||
MinSize="250 300"
|
||||
Resizable="False">
|
||||
|
||||
<Label Name="LoadingPlaceHolder"
|
||||
Text="{Loc 'cryo-pod-window-loading'}"
|
||||
Align="Center"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"/>
|
||||
|
||||
<BoxContainer Name="Sections"
|
||||
Orientation="Horizontal"
|
||||
Visible="False"
|
||||
Margin="10"
|
||||
SeparationOverride="16">
|
||||
<BoxContainer Name="CryoSection"
|
||||
VerticalExpand="True"
|
||||
Orientation="Vertical"
|
||||
MinWidth="250"
|
||||
MaxWidth="250">
|
||||
|
||||
<!-- Flavor text -->
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
SeparationOverride="10"
|
||||
Margin="8 0 0 8">
|
||||
<TextureRect StyleClasses="NTLogoDark"
|
||||
VerticalExpand="True"
|
||||
Stretch="KeepAspectCentered"
|
||||
SetSize="32 32"/>
|
||||
<BoxContainer Orientation="Vertical"
|
||||
SeparationOverride="-4">
|
||||
<Label Text="{Loc 'cryo-pod-window-product-name'}"
|
||||
StyleClasses="FontLarge"/>
|
||||
<Label Text="{Loc 'cryo-pod-window-product-subtitle'}"
|
||||
StyleClasses="LabelSubText"/>
|
||||
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Atmos info -->
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
SeparationOverride="20"
|
||||
Margin="0 0 0 4">
|
||||
<!-- Pressure -->
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Text="{Loc 'gas-analyzer-window-pressure-text'}"
|
||||
StyleClasses="LabelSubText"/>
|
||||
<Label Name="Pressure"/>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Temperature -->
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Text="{Loc 'gas-analyzer-window-temperature-text'}"
|
||||
StyleClasses="LabelSubText"/>
|
||||
<Label Name="Temperature"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Gas mix -->
|
||||
<Control Margin="0 0 0 22">
|
||||
<controls:SplitBar Name="GasMixChart"
|
||||
MinHeight="8"
|
||||
MaxHeight="8"/>
|
||||
</Control>
|
||||
|
||||
<!-- Warnings & status -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
Align="Center"
|
||||
Margin="0 0 0 14"
|
||||
SeparationOverride="20">
|
||||
|
||||
<!-- Ejection error (if the pod is locked) -->
|
||||
<PanelContainer Name="EjectError"
|
||||
Visible="False"
|
||||
HorizontalExpand="True">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BorderThickness="1" BorderColor="orange"/>
|
||||
</PanelContainer.PanelOverride>
|
||||
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="6">
|
||||
<Label Text="{Loc 'cryo-pod-window-error-header'}"
|
||||
FontColorOverride="orange"
|
||||
Align="Center"/>
|
||||
<RichTextLabel Text="{Loc 'cryo-pod-window-eject-error'}"/>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Pressure warning -->
|
||||
<PanelContainer Name="LowPressureWarning"
|
||||
Visible="False"
|
||||
HorizontalExpand="True">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BorderThickness="1" BorderColor="orange"/>
|
||||
</PanelContainer.PanelOverride>
|
||||
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="6">
|
||||
<Label Text="{Loc 'cryo-pod-window-warning-header'}"
|
||||
FontColorOverride="orange"
|
||||
Align="Center"/>
|
||||
<RichTextLabel Text="{Loc 'cryo-pod-window-low-pressure-warning'}"/>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Temperature warning -->
|
||||
<PanelContainer Name="HighTemperatureWarning"
|
||||
Visible="False"
|
||||
HorizontalExpand="True">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BorderThickness="1" BorderColor="orange"/>
|
||||
</PanelContainer.PanelOverride>
|
||||
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="6">
|
||||
<Label Text="{Loc 'cryo-pod-window-warning-header'}"
|
||||
FontColorOverride="orange"
|
||||
Align="Center"/>
|
||||
<!-- Note: This placeholder text should never be visible. -->
|
||||
<RichTextLabel Name="HighTemperatureWarningText"
|
||||
Text="Temperature too high."/>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Status checklist -->
|
||||
<BoxContainer Orientation="Vertical">
|
||||
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
SeparationOverride="8">
|
||||
<Label Text="{Loc 'cryo-pod-window-status'}"/>
|
||||
<Label Name="StatusLabel"
|
||||
Text="{Loc 'cryo-pod-window-status-not-ready'}"
|
||||
FontColorOverride="Orange"/>
|
||||
</BoxContainer>
|
||||
|
||||
<GridContainer Columns="2"
|
||||
HSeparationOverride="0"
|
||||
VSeparationOverride="6"
|
||||
Margin="6 3 0 0">
|
||||
<Label Text="⋄"
|
||||
StyleClasses="LabelSubText"/>
|
||||
<Label Name="PressureCheck"
|
||||
Text="{Loc 'cryo-pod-window-checklist-pressure'}"
|
||||
StyleClasses="LabelSubText"/>
|
||||
<Label Text="⋄"
|
||||
StyleClasses="LabelSubText"/>
|
||||
<Label Name="ChemicalsCheck"
|
||||
Text="{Loc 'cryo-pod-window-checklist-chemicals'}"
|
||||
StyleClasses="LabelSubText"
|
||||
FontColorOverride="Orange"/>
|
||||
<Label Text="⋄"
|
||||
StyleClasses="LabelSubText"/>
|
||||
<Label Name="TemperatureCheck"
|
||||
Text="{Loc 'cryo-pod-window-checklist-temperature'}"
|
||||
StyleClasses="LabelSubText"/>
|
||||
</GridContainer>
|
||||
|
||||
</BoxContainer>
|
||||
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Reagents -->
|
||||
<Control HorizontalExpand="True"
|
||||
MinHeight="30">
|
||||
<Label Name="NoBeakerText"
|
||||
Text="{Loc 'cryo-pod-window-chems-no-beaker'}"
|
||||
FontColorOverride="Gray"
|
||||
VerticalExpand="True"
|
||||
VAlign="Center"/>
|
||||
<cryogenics:BeakerBarChart Name="ChemicalsChart"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"/>
|
||||
</Control>
|
||||
|
||||
<!-- Buttons -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="-2 2 -2 0">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Button Name="Inject1"
|
||||
Text="{Loc 'cryo-pod-window-inject-1u'}"
|
||||
Disabled="True"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="OpenBoth"/>
|
||||
<Button Name="Inject5"
|
||||
Text="{Loc 'cryo-pod-window-inject-5u'}"
|
||||
Disabled="True"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="OpenBoth"/>
|
||||
<Button Name="Inject10"
|
||||
Text="{Loc 'cryo-pod-window-inject-10u'}"
|
||||
Disabled="True"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="OpenBoth"/>
|
||||
<Button Name="Inject20"
|
||||
Text="{Loc 'cryo-pod-window-inject-20u'}"
|
||||
Disabled="True"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="OpenBoth"/>
|
||||
<Button Name="EjectBeakerButton"
|
||||
Text="{Loc 'cryo-pod-window-eject-beaker'}"
|
||||
Disabled="True"
|
||||
StyleClasses="OpenBoth"/>
|
||||
</BoxContainer>
|
||||
<Button Name="EjectPatientButton"
|
||||
Text="{Loc 'cryo-pod-window-eject-patient'}"
|
||||
Disabled="True"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="OpenRight"/>
|
||||
</BoxContainer>
|
||||
|
||||
</BoxContainer>
|
||||
<BoxContainer Name="HealthSection"
|
||||
VerticalExpand="True"
|
||||
Orientation="Vertical">
|
||||
|
||||
<health:HealthAnalyzerControl Name="HealthAnalyzer"/>
|
||||
|
||||
<!-- This label is used to deal with a stray hline at the end of the health analyzer UI -->
|
||||
<Label Name="NoDamageText"
|
||||
Text="{Loc 'cryo-pod-window-health-no-damage'}"
|
||||
FontColorOverride="DeepSkyBlue"/>
|
||||
<Control VerticalExpand="True"/>
|
||||
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
260
Content.Client/Medical/Cryogenics/CryoPodWindow.xaml.cs
Normal file
260
Content.Client/Medical/Cryogenics/CryoPodWindow.xaml.cs
Normal file
@@ -0,0 +1,260 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.EntityConditions.Conditions;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Medical.Cryogenics;
|
||||
using Content.Shared.Temperature;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
namespace Content.Client.Medical.Cryogenics;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class CryoPodWindow : FancyWindow
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
public event Action? OnEjectPatientPressed;
|
||||
public event Action? OnEjectBeakerPressed;
|
||||
public event Action<FixedPoint2>? OnInjectPressed;
|
||||
|
||||
public CryoPodWindow()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
EjectPatientButton.OnPressed += _ => OnEjectPatientPressed?.Invoke();
|
||||
EjectBeakerButton.OnPressed += _ => OnEjectBeakerPressed?.Invoke();
|
||||
Inject1.OnPressed += _ => OnInjectPressed?.Invoke(1);
|
||||
Inject5.OnPressed += _ => OnInjectPressed?.Invoke(5);
|
||||
Inject10.OnPressed += _ => OnInjectPressed?.Invoke(10);
|
||||
Inject20.OnPressed += _ => OnInjectPressed?.Invoke(20);
|
||||
}
|
||||
|
||||
public void Populate(CryoPodUserMessage msg)
|
||||
{
|
||||
// Loading screen
|
||||
if (LoadingPlaceHolder.Visible)
|
||||
{
|
||||
LoadingPlaceHolder.Visible = false;
|
||||
Sections.Visible = true;
|
||||
}
|
||||
|
||||
// Atmosphere
|
||||
var hasCorrectPressure = (msg.GasMix.Pressure > Atmospherics.WarningLowPressure);
|
||||
var hasGas = (msg.GasMix.Pressure > Atmospherics.GasMinMoles);
|
||||
var showsPressureWarning = !hasCorrectPressure;
|
||||
LowPressureWarning.Visible = showsPressureWarning;
|
||||
Pressure.Text = Loc.GetString("gas-analyzer-window-pressure-val-text",
|
||||
("pressure", $"{msg.GasMix.Pressure:0.00}"));
|
||||
Temperature.Text = Loc.GetString("generic-not-available-shorthand");
|
||||
|
||||
if (hasGas)
|
||||
{
|
||||
var celsius = TemperatureHelpers.KelvinToCelsius(msg.GasMix.Temperature);
|
||||
Temperature.Text = Loc.GetString("gas-analyzer-window-temperature-val-text",
|
||||
("tempK", $"{msg.GasMix.Temperature:0.0}"),
|
||||
("tempC", $"{celsius:0.0}"));
|
||||
}
|
||||
|
||||
// Gas mix segmented bar chart
|
||||
GasMixChart.Clear();
|
||||
GasMixChart.Visible = hasGas;
|
||||
|
||||
if (msg.GasMix.Gases != null)
|
||||
{
|
||||
var totalGasAmount = msg.GasMix.Gases.Sum(gas => gas.Amount);
|
||||
|
||||
foreach (var gas in msg.GasMix.Gases)
|
||||
{
|
||||
var color = Color.FromHex($"#{gas.Color}", Color.White);
|
||||
var percent = gas.Amount / totalGasAmount * 100;
|
||||
var localizedName = Loc.GetString(gas.Name);
|
||||
var tooltip = Loc.GetString("gas-analyzer-window-molarity-percentage-text",
|
||||
("gasName", localizedName),
|
||||
("amount", $"{gas.Amount:0.##}"),
|
||||
("percentage", $"{percent:0.#}"));
|
||||
GasMixChart.AddEntry(gas.Amount, color, tooltip: tooltip);
|
||||
}
|
||||
}
|
||||
|
||||
// Health analyzer
|
||||
var maybePatient = _entityManager.GetEntity(msg.Health.TargetEntity);
|
||||
var hasPatient = msg.Health.TargetEntity.HasValue;
|
||||
var hasDamage = (hasPatient
|
||||
&& _entityManager.TryGetComponent(maybePatient, out DamageableComponent? damageable)
|
||||
&& damageable.TotalDamage > 0);
|
||||
|
||||
NoDamageText.Visible = (hasPatient && !hasDamage);
|
||||
HealthSection.Visible = hasPatient;
|
||||
EjectPatientButton.Disabled = !hasPatient;
|
||||
|
||||
if (hasPatient)
|
||||
HealthAnalyzer.Populate(msg.Health);
|
||||
|
||||
// Reagents
|
||||
float? lowestTempRequirement = null;
|
||||
ReagentId? lowestTempReagent = null;
|
||||
var totalBeakerCapacity = msg.BeakerCapacity ?? 0;
|
||||
var availableQuantity = new FixedPoint2();
|
||||
var injectingQuantity =
|
||||
msg.Injecting?.Aggregate(new FixedPoint2(), (sum, r) => sum + r.Quantity)
|
||||
?? new FixedPoint2(); // Either the sum of the reagent quantities in `msg.Injecting` or zero.
|
||||
var hasBeaker = (msg.Beaker != null);
|
||||
|
||||
ChemicalsChart.Clear();
|
||||
ChemicalsChart.Capacity = (totalBeakerCapacity < 1 ? 50 : (int)totalBeakerCapacity);
|
||||
|
||||
var chartMaxChemsQuantity = ChemicalsChart.Capacity - injectingQuantity; // Ensure space for injection buffer
|
||||
|
||||
if (hasBeaker)
|
||||
{
|
||||
foreach (var (reagent, quantity) in msg.Beaker!)
|
||||
{
|
||||
availableQuantity += quantity;
|
||||
|
||||
// Make sure we don't add too many chemicals to the chart, so that there's still enough space to
|
||||
// visualize the injection buffer.
|
||||
var chemsQuantityOvershoot = FixedPoint2.Max(0, availableQuantity - chartMaxChemsQuantity);
|
||||
var chartQuantity = FixedPoint2.Max(0, quantity - chemsQuantityOvershoot);
|
||||
|
||||
var reagentProto = _prototypeManager.Index<ReagentPrototype>(reagent.Prototype);
|
||||
ChemicalsChart.SetEntry(
|
||||
reagent.Prototype,
|
||||
reagentProto.LocalizedName,
|
||||
(float)chartQuantity,
|
||||
reagentProto.SubstanceColor,
|
||||
tooltip: $"{quantity}u {reagentProto.LocalizedName}"
|
||||
);
|
||||
|
||||
var temp = TryFindMaxTemperatureRequirement(reagent);
|
||||
if (lowestTempRequirement == null
|
||||
|| temp < lowestTempRequirement)
|
||||
{
|
||||
lowestTempRequirement = temp;
|
||||
lowestTempReagent = reagent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (injectingQuantity != 0)
|
||||
{
|
||||
var injectingText = (injectingQuantity > 1 ? $"{injectingQuantity}u" : "");
|
||||
ChemicalsChart.SetEntry(
|
||||
"injecting",
|
||||
injectingText,
|
||||
(float)injectingQuantity,
|
||||
Color.MediumSpringGreen,
|
||||
tooltip: Loc.GetString("cryo-pod-window-chems-injecting-tooltip",
|
||||
("quantity", injectingQuantity))
|
||||
);
|
||||
}
|
||||
|
||||
var isBeakerEmpty = (injectingQuantity + availableQuantity == 0);
|
||||
var isChemicalsChartVisible = (hasBeaker || injectingQuantity != 0);
|
||||
NoBeakerText.Visible = !isChemicalsChartVisible;
|
||||
ChemicalsChart.Visible = isChemicalsChartVisible;
|
||||
Inject1.Disabled = (!hasPatient || availableQuantity < 0.1f);
|
||||
Inject5.Disabled = (!hasPatient || availableQuantity <= 1);
|
||||
Inject10.Disabled = (!hasPatient || availableQuantity <= 5);
|
||||
Inject20.Disabled = (!hasPatient || availableQuantity <= 10);
|
||||
EjectBeakerButton.Disabled = !hasBeaker;
|
||||
|
||||
// Temperature warning
|
||||
var hasCorrectTemperature = (lowestTempRequirement == null || lowestTempRequirement > msg.GasMix.Temperature);
|
||||
var showsTemperatureWarning = (!showsPressureWarning && !hasCorrectTemperature);
|
||||
|
||||
HighTemperatureWarning.Visible = showsTemperatureWarning;
|
||||
|
||||
if (showsTemperatureWarning)
|
||||
{
|
||||
var reagentName = _prototypeManager.Index<ReagentPrototype>(lowestTempReagent!.Value.Prototype)
|
||||
.LocalizedName;
|
||||
HighTemperatureWarningText.Text = Loc.GetString("cryo-pod-window-high-temperature-warning",
|
||||
("reagent", reagentName),
|
||||
("temperature", lowestTempRequirement!));
|
||||
}
|
||||
|
||||
// Status checklist
|
||||
const float fallbackTemperatureRequirement = 213;
|
||||
var hasTemperatureCheck = (hasGas && hasCorrectTemperature
|
||||
&& (lowestTempRequirement != null || msg.GasMix.Temperature < fallbackTemperatureRequirement));
|
||||
var hasChemicals = (hasBeaker && !isBeakerEmpty);
|
||||
|
||||
UpdateChecklistItem(PressureCheck, Loc.GetString("cryo-pod-window-checklist-pressure"), hasCorrectPressure);
|
||||
UpdateChecklistItem(ChemicalsCheck, Loc.GetString("cryo-pod-window-checklist-chemicals"), hasChemicals);
|
||||
UpdateChecklistItem(TemperatureCheck, Loc.GetString("cryo-pod-window-checklist-temperature"), hasTemperatureCheck);
|
||||
|
||||
var isReady = (hasCorrectPressure && hasChemicals && hasTemperatureCheck);
|
||||
var isCooling = (lowestTempRequirement != null && hasPatient
|
||||
&& msg.Health.Temperature > lowestTempRequirement);
|
||||
var isInjecting = (injectingQuantity > 0);
|
||||
StatusLabel.Text = (!isReady ? Loc.GetString("cryo-pod-window-status-not-ready") :
|
||||
isCooling ? Loc.GetString("cryo-pod-window-status-cooling") :
|
||||
isInjecting ? Loc.GetString("cryo-pod-window-status-injecting") :
|
||||
hasPatient ? Loc.GetString("cryo-pod-window-status-ready-to-inject") :
|
||||
Loc.GetString("cryo-pod-window-status-ready-for-patient"));
|
||||
StatusLabel.FontColorOverride = (isReady ? Color.DeepSkyBlue : Color.Orange);
|
||||
}
|
||||
|
||||
private void UpdateChecklistItem(Label label, string text, bool isOkay)
|
||||
{
|
||||
label.Text = (isOkay ? text : Loc.GetString("cryo-pod-window-checklist-fail", ("item", text)));
|
||||
label.FontColorOverride = (isOkay ? null : Color.Orange);
|
||||
}
|
||||
|
||||
private float? TryFindMaxTemperatureRequirement(ReagentId reagent)
|
||||
{
|
||||
var reagentProto = _prototypeManager.Index<ReagentPrototype>(reagent.Prototype);
|
||||
if (reagentProto.Metabolisms == null)
|
||||
return null;
|
||||
|
||||
float? result = null;
|
||||
|
||||
foreach (var (_, metabolism) in reagentProto.Metabolisms)
|
||||
{
|
||||
foreach (var effect in metabolism.Effects)
|
||||
{
|
||||
if (effect.Conditions == null)
|
||||
continue;
|
||||
|
||||
foreach (var condition in effect.Conditions)
|
||||
{
|
||||
// If there are multiple temperature conditions in the same reagent (which could hypothetically
|
||||
// happen, although it currently doesn't), we return the lowest max temperature.
|
||||
if (condition is TemperatureCondition tempCondition
|
||||
&& float.IsFinite(tempCondition.Max)
|
||||
&& (result == null || tempCondition.Max < result))
|
||||
{
|
||||
result = tempCondition.Max;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void SetEjectErrorVisible(bool isVisible)
|
||||
{
|
||||
EjectError.Visible = isVisible;
|
||||
}
|
||||
|
||||
protected override Vector2 MeasureOverride(Vector2 availableSize)
|
||||
{
|
||||
const float antiJiggleSlackSpace = 80;
|
||||
var oldSize = DesiredSize;
|
||||
var newSize = base.MeasureOverride(availableSize);
|
||||
|
||||
// Reduce how often the height of the window jiggles
|
||||
if (newSize.Y < oldSize.Y && newSize.Y + antiJiggleSlackSpace > oldSize.Y)
|
||||
newSize.Y = oldSize.Y;
|
||||
|
||||
return newSize;
|
||||
}
|
||||
}
|
||||
5
Content.Client/Medical/DefibrillatorSystem.cs
Normal file
5
Content.Client/Medical/DefibrillatorSystem.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using Content.Shared.Medical;
|
||||
|
||||
namespace Content.Client.Medical;
|
||||
|
||||
public sealed class DefibrillatorSystem : SharedDefibrillatorSystem;
|
||||
@@ -41,9 +41,9 @@ public sealed class NetworkConfiguratorLinkOverlay : Overlay
|
||||
if (!Colors.TryGetValue(uid, out var color))
|
||||
{
|
||||
color = new Color(
|
||||
_random.Next(0, 255),
|
||||
_random.Next(0, 255),
|
||||
_random.Next(0, 255));
|
||||
_random.NextByte(0, 255),
|
||||
_random.NextByte(0, 255),
|
||||
_random.NextByte(0, 255));
|
||||
Colors.Add(uid, color);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Content.Shared.Overlays;
|
||||
using Robust.Client.Graphics;
|
||||
using System.Linq;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Overlays;
|
||||
@@ -35,6 +33,9 @@ public sealed class ShowHealthBarsSystem : EquipmentHudSystem<ShowHealthBarsComp
|
||||
{
|
||||
base.UpdateInternal(component);
|
||||
|
||||
_overlay.DamageContainers.Clear();
|
||||
_overlay.StatusIcon = null;
|
||||
|
||||
foreach (var comp in component.Components)
|
||||
{
|
||||
foreach (var damageContainerId in comp.DamageContainers)
|
||||
|
||||
@@ -5,7 +5,6 @@ using Content.Shared.Overlays;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Content.Shared.StatusIcon.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Linq;
|
||||
using Content.Shared.Damage.Components;
|
||||
|
||||
namespace Content.Client.Overlays;
|
||||
@@ -32,9 +31,13 @@ public sealed class ShowHealthIconsSystem : EquipmentHudSystem<ShowHealthIconsCo
|
||||
{
|
||||
base.UpdateInternal(component);
|
||||
|
||||
foreach (var damageContainerId in component.Components.SelectMany(x => x.DamageContainers))
|
||||
DamageContainers.Clear();
|
||||
foreach (var comp in component.Components)
|
||||
{
|
||||
DamageContainers.Add(damageContainerId);
|
||||
foreach (var damageContainerId in comp.DamageContainers)
|
||||
{
|
||||
DamageContainers.Add(damageContainerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,8 +42,7 @@ public sealed class PdaSheetlet : Sheetlet<NanotrasenStylesheet>
|
||||
|
||||
E<PanelContainer>()
|
||||
.Class("PdaBorderRect")
|
||||
.Prop(PanelContainer.StylePropertyPanel, angleBorderRect)
|
||||
.Prop(Control.StylePropertyModulateSelf, Color.FromHex("#00000040")),
|
||||
.Prop(PanelContainer.StylePropertyPanel, angleBorderRect),
|
||||
|
||||
//PDA - Buttons
|
||||
E<PdaSettingsButton>()
|
||||
|
||||
@@ -17,7 +17,11 @@ public sealed class StationMapBoundUserInterface : BoundUserInterface
|
||||
base.Open();
|
||||
EntityUid? gridUid = null;
|
||||
|
||||
if (EntMan.TryGetComponent<TransformComponent>(Owner, out var xform))
|
||||
if (EntMan.TryGetComponent<StationMapComponent>(Owner, out var comp) && comp.TargetGrid != null)
|
||||
{
|
||||
gridUid = comp.TargetGrid;
|
||||
}
|
||||
else if (EntMan.TryGetComponent<TransformComponent>(Owner, out var xform))
|
||||
{
|
||||
gridUid = xform.GridUid;
|
||||
}
|
||||
@@ -30,8 +34,8 @@ public sealed class StationMapBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
stationName = gridMetaData.EntityName;
|
||||
}
|
||||
|
||||
if (EntMan.TryGetComponent<StationMapComponent>(Owner, out var comp) && comp.ShowLocation)
|
||||
|
||||
if (comp != null && comp.ShowLocation)
|
||||
_window.Set(stationName, gridUid, Owner);
|
||||
else
|
||||
_window.Set(stationName, gridUid, null);
|
||||
|
||||
@@ -3,5 +3,6 @@ namespace Content.Client.Power;
|
||||
/// Remains in use by portable scrubbers and lathes.
|
||||
public enum PowerDeviceVisualLayers : byte
|
||||
{
|
||||
Powered
|
||||
Powered,
|
||||
Charging
|
||||
}
|
||||
|
||||
@@ -32,8 +32,7 @@ public sealed class SSDIndicatorSystem : EntitySystem
|
||||
_cfg.GetCVar(CCVars.ICShowSSDIndicator) &&
|
||||
!_mobState.IsDead(uid) &&
|
||||
!HasComp<ActiveNPCComponent>(uid) &&
|
||||
TryComp<MindContainerComponent>(uid, out var mindContainer) &&
|
||||
mindContainer.ShowExamineInfo)
|
||||
HasComp<MindExaminableComponent>(uid))
|
||||
{
|
||||
args.StatusIcons.Add(_prototype.Index(component.Icon));
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public sealed class GenpopLockerBoundUserInterface(EntityUid owner, Enum uiKey)
|
||||
|
||||
_menu.OnConfigurationComplete += (name, time, crime) =>
|
||||
{
|
||||
SendMessage(new GenpopLockerIdConfiguredMessage(name, time, crime));
|
||||
SendPredictedMessage(new GenpopLockerIdConfiguredMessage(name, time, crime));
|
||||
Close();
|
||||
};
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ public sealed class IFFConsoleBoundUserInterface : BoundUserInterface
|
||||
|
||||
_window = this.CreateWindowCenteredLeft<IFFConsoleWindow>();
|
||||
_window.ShowIFF += SendIFFMessage;
|
||||
_window.ShowVessel += SendVesselMessage;
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
@@ -44,14 +43,6 @@ public sealed class IFFConsoleBoundUserInterface : BoundUserInterface
|
||||
});
|
||||
}
|
||||
|
||||
private void SendVesselMessage(bool obj)
|
||||
{
|
||||
SendMessage(new IFFShowVesselMessage()
|
||||
{
|
||||
Show = obj,
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
@@ -9,12 +9,6 @@
|
||||
<Button Name="ShowIFFOnButton" Text="{Loc 'iff-console-on'}" StyleClasses="OpenRight" />
|
||||
<Button Name="ShowIFFOffButton" Text="{Loc 'iff-console-off'}" StyleClasses="OpenLeft" />
|
||||
</BoxContainer>
|
||||
|
||||
<Label Name="ShowVesselLabel" Text="{Loc 'iff-console-show-vessel-label'}" HorizontalExpand="True" StyleClasses="highlight" />
|
||||
<BoxContainer Orientation="Horizontal" MinWidth="120">
|
||||
<Button Name="ShowVesselOnButton" Text="{Loc 'iff-console-on'}" StyleClasses="OpenRight" />
|
||||
<Button Name="ShowVesselOffButton" Text="{Loc 'iff-console-off'}" StyleClasses="OpenLeft" />
|
||||
</BoxContainer>
|
||||
</GridContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
|
||||
@@ -13,9 +13,7 @@ public sealed partial class IFFConsoleWindow : FancyWindow,
|
||||
IComputerWindow<IFFConsoleBoundUserInterfaceState>
|
||||
{
|
||||
private readonly ButtonGroup _showIFFButtonGroup = new();
|
||||
private readonly ButtonGroup _showVesselButtonGroup = new();
|
||||
public event Action<bool>? ShowIFF;
|
||||
public event Action<bool>? ShowVessel;
|
||||
|
||||
public IFFConsoleWindow()
|
||||
{
|
||||
@@ -24,11 +22,6 @@ public sealed partial class IFFConsoleWindow : FancyWindow,
|
||||
ShowIFFOnButton.Group = _showIFFButtonGroup;
|
||||
ShowIFFOnButton.OnPressed += args => ShowIFFPressed(true);
|
||||
ShowIFFOffButton.OnPressed += args => ShowIFFPressed(false);
|
||||
|
||||
ShowVesselOffButton.Group = _showVesselButtonGroup;
|
||||
ShowVesselOnButton.Group = _showVesselButtonGroup;
|
||||
ShowVesselOnButton.OnPressed += args => ShowVesselPressed(true);
|
||||
ShowVesselOffButton.OnPressed += args => ShowVesselPressed(false);
|
||||
}
|
||||
|
||||
private void ShowIFFPressed(bool pressed)
|
||||
@@ -36,19 +29,14 @@ public sealed partial class IFFConsoleWindow : FancyWindow,
|
||||
ShowIFF?.Invoke(pressed);
|
||||
}
|
||||
|
||||
private void ShowVesselPressed(bool pressed)
|
||||
{
|
||||
ShowVessel?.Invoke(pressed);
|
||||
}
|
||||
|
||||
public void UpdateState(IFFConsoleBoundUserInterfaceState state)
|
||||
{
|
||||
if ((state.AllowedFlags & IFFFlags.HideLabel) != 0x0)
|
||||
if ((state.AllowedFlags & IFFFlags.HideLabel) != 0x0 || (state.AllowedFlags & IFFFlags.Hide) != 0x0)
|
||||
{
|
||||
ShowIFFOffButton.Disabled = false;
|
||||
ShowIFFOnButton.Disabled = false;
|
||||
|
||||
if ((state.Flags & IFFFlags.HideLabel) != 0x0)
|
||||
if ((state.Flags & IFFFlags.HideLabel) != 0x0 || (state.Flags & IFFFlags.Hide) != 0x0)
|
||||
{
|
||||
ShowIFFOffButton.Pressed = true;
|
||||
}
|
||||
@@ -62,25 +50,5 @@ public sealed partial class IFFConsoleWindow : FancyWindow,
|
||||
ShowIFFOffButton.Disabled = true;
|
||||
ShowIFFOnButton.Disabled = true;
|
||||
}
|
||||
|
||||
if ((state.AllowedFlags & IFFFlags.Hide) != 0x0)
|
||||
{
|
||||
ShowVesselOffButton.Disabled = false;
|
||||
ShowVesselOnButton.Disabled = false;
|
||||
|
||||
if ((state.Flags & IFFFlags.Hide) != 0x0)
|
||||
{
|
||||
ShowVesselOffButton.Pressed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowVesselOnButton.Pressed = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowVesselOffButton.Disabled = true;
|
||||
ShowVesselOnButton.Disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ public sealed partial class MapScreen : BoxContainer
|
||||
break;
|
||||
}
|
||||
|
||||
if (IsFTLBlocked())
|
||||
if (IsPingBlocked())
|
||||
{
|
||||
MapRebuildButton.Disabled = true;
|
||||
ClearMapObjects();
|
||||
@@ -408,9 +408,21 @@ public sealed partial class MapScreen : BoxContainer
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if we shouldn't be able to select the Scan for Objects button.
|
||||
/// </summary>
|
||||
private bool IsPingBlocked()
|
||||
{
|
||||
return _state switch
|
||||
{
|
||||
FTLState.Available or FTLState.Cooldown => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
private void OnMapObjectPress(IMapObject mapObject)
|
||||
{
|
||||
if (IsFTLBlocked())
|
||||
if (IsPingBlocked())
|
||||
return;
|
||||
|
||||
var coordinates = _shuttles.GetMapCoordinates(mapObject);
|
||||
@@ -506,7 +518,7 @@ public sealed partial class MapScreen : BoxContainer
|
||||
BumpMapDequeue();
|
||||
}
|
||||
|
||||
if (!IsFTLBlocked() && _nextPing < curTime)
|
||||
if (!IsPingBlocked() && _nextPing < curTime)
|
||||
{
|
||||
MapRebuildButton.Disabled = false;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ public sealed partial class BorgMenu : FancyWindow
|
||||
[Dependency] private readonly IEntityManager _entity = default!;
|
||||
private readonly NameModifierSystem _nameModifier;
|
||||
private readonly PowerCellSystem _powerCell;
|
||||
private readonly PredictedBatterySystem _battery;
|
||||
private readonly SharedBatterySystem _battery;
|
||||
|
||||
public Action? BrainButtonPressed;
|
||||
public Action? EjectBatteryButtonPressed;
|
||||
@@ -44,7 +44,7 @@ public sealed partial class BorgMenu : FancyWindow
|
||||
|
||||
_nameModifier = _entity.System<NameModifierSystem>();
|
||||
_powerCell = _entity.System<PowerCellSystem>();
|
||||
_battery = _entity.System<PredictedBatterySystem>();
|
||||
_battery = _entity.System<SharedBatterySystem>();
|
||||
|
||||
_maxNameLength = _cfgManager.GetCVar(CCVars.MaxNameLength);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user