forked from space-syndicate/space-station-14
fix: Enable Offbrand medical system on mobs #3
15
.github/actions/cache-dotnet/action.yml
vendored
Normal file
15
.github/actions/cache-dotnet/action.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
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,4 +1,5 @@
|
||||
name: Benchmarks
|
||||
name: Benchmarks
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
@@ -10,38 +11,31 @@ 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: Get Engine version
|
||||
run: |
|
||||
cd RobustToolbox
|
||||
git fetch --depth=1
|
||||
echo "::set-output name=out::$(git rev-parse HEAD)"
|
||||
id: engine_version
|
||||
- name: Run script on centcomm
|
||||
uses: appleboy/ssh-action@master
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
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 }}
|
||||
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 }}
|
||||
run: |
|
||||
cd Content.Benchmarks
|
||||
dotnet run --filter '*' --configuration Release
|
||||
|
||||
2
.github/workflows/build-docfx.yml
vendored
2
.github/workflows/build-docfx.yml
vendored
@@ -7,6 +7,8 @@ on:
|
||||
jobs:
|
||||
docfx:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- name: Setup submodule
|
||||
|
||||
2
.github/workflows/build-map-renderer.yml
vendored
2
.github/workflows/build-map-renderer.yml
vendored
@@ -16,6 +16,8 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
|
||||
steps:
|
||||
- name: Checkout Master
|
||||
|
||||
12
.github/workflows/build-test-debug.yml
vendored
12
.github/workflows/build-test-debug.yml
vendored
@@ -16,11 +16,19 @@ 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
|
||||
@@ -48,10 +56,8 @@ jobs:
|
||||
run: dotnet test --no-build --configuration DebugOpt Content.Tests/Content.Tests.csproj -- NUnit.ConsoleOut=0
|
||||
|
||||
- name: Run Content.IntegrationTests
|
||||
shell: pwsh
|
||||
run: |
|
||||
$env:DOTNET_gcServer=1
|
||||
dotnet test --no-build --configuration DebugOpt Content.IntegrationTests/Content.IntegrationTests.csproj -- NUnit.ConsoleOut=0 NUnit.MapWarningTo=Failed
|
||||
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:
|
||||
|
||||
59
.github/workflows/labeler-conflict.yml
vendored
59
.github/workflows/labeler-conflict.yml
vendored
@@ -9,13 +9,56 @@ on:
|
||||
- ready_for_review
|
||||
|
||||
jobs:
|
||||
Label:
|
||||
if: ( github.event.pull_request.draft == false ) && ( github.actor != 'IanComradeBot' )
|
||||
check-conflicts:
|
||||
if: github.event.pull_request.draft == false && github.actor != 'IanComradeBot'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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."
|
||||
- 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..."
|
||||
curl -s -X DELETE -H "Authorization: token $API_TOKEN" \
|
||||
"$API_URL/repos/$REPO_OWNER/$REPO_NAME/issues/$PR_INDEX/labels/$LABEL_NAME"
|
||||
echo "Conflict label removed."
|
||||
fi
|
||||
fi
|
||||
|
||||
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,4 +11,5 @@ jobs:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/labeler@v5
|
||||
|
||||
5
.github/workflows/publish-public.yml
vendored
5
.github/workflows/publish-public.yml
vendored
@@ -11,6 +11,8 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
|
||||
steps:
|
||||
# - name: Install dependencies
|
||||
@@ -42,6 +44,9 @@ 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:
|
||||
|
||||
5
.github/workflows/publish-testing.yml
vendored
5
.github/workflows/publish-testing.yml
vendored
@@ -12,6 +12,8 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3.6.0
|
||||
@@ -39,6 +41,9 @@ 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:
|
||||
|
||||
31
.github/workflows/publish.yml
vendored
31
.github/workflows/publish.yml
vendored
@@ -6,18 +6,23 @@ concurrency:
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
schedule:
|
||||
- cron: '0 1 * * *'
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
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'}}
|
||||
run: exit 1
|
||||
|
||||
# - name: Install dependencies
|
||||
# run: sudo apt-get install -y python3-paramiko python3-lxml
|
||||
- name: Install Python dependencies
|
||||
run: pip install --break-system-packages requests paramiko lxml
|
||||
|
||||
- uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
@@ -70,8 +75,26 @@ jobs:
|
||||
run: Tools/publish_multi_request.py
|
||||
env:
|
||||
PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
|
||||
GITHUB_REPOSITORY: ${{ vars.GITHUB_REPOSITORY }}
|
||||
FORK_ID: ${{ vars.FORK_ID }}
|
||||
GITHUB_REPOSITORY: wylab/wylab-station-14
|
||||
FORK_ID: wylab
|
||||
ROBUST_CDN_URL: https://cdn.wylab.me/
|
||||
|
||||
- name: Trigger Docker image rebuild
|
||||
if: ${{ success() }}
|
||||
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}\"}}"
|
||||
|
||||
# - name: Publish changelog (Discord)
|
||||
# continue-on-error: true
|
||||
|
||||
69
.github/workflows/rsi-diff.yml
vendored
69
.github/workflows/rsi-diff.yml
vendored
@@ -1,69 +0,0 @@
|
||||
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 }}
|
||||
2
.github/workflows/test-packaging.yml
vendored
2
.github/workflows/test-packaging.yml
vendored
@@ -31,6 +31,8 @@ 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
|
||||
|
||||
28
.github/workflows/update-wiki.yml
vendored
28
.github/workflows/update-wiki.yml
vendored
@@ -3,7 +3,7 @@ name: Update Wiki
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ master, jsondump ]
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/update-wiki.yml'
|
||||
- 'Content.Shared/Chemistry/**.cs'
|
||||
@@ -19,6 +19,8 @@ jobs:
|
||||
update-wiki:
|
||||
name: Build and Publish JSON blobs to wiki
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
|
||||
steps:
|
||||
- name: Checkout Master
|
||||
@@ -51,42 +53,42 @@ jobs:
|
||||
run: dotnet ./bin/Content.Server/Content.Server.dll --cvar autogen.destination_file=prototypes.json
|
||||
continue-on-error: true
|
||||
|
||||
- name: Upload chem_prototypes.json to wiki
|
||||
- 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: ${{ secrets.WIKI_ROOT_URL }}/api.php
|
||||
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 }}
|
||||
|
||||
- name: Upload react_prototypes.json to wiki
|
||||
- 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: ${{ secrets.WIKI_ROOT_URL }}/api.php
|
||||
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 }}
|
||||
|
||||
- name: Upload entity_prototypes.json to wiki
|
||||
- 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: ${{ secrets.WIKI_ROOT_URL }}/api.php
|
||||
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 }}
|
||||
|
||||
- name: Upload mealrecipes_prototypes.json to wiki
|
||||
- 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: ${{ secrets.WIKI_ROOT_URL }}/api.php
|
||||
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 }}
|
||||
|
||||
2
.github/workflows/validate-rsis.yml
vendored
2
.github/workflows/validate-rsis.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
uses: space-wizards/submodule-dependency@v0.1.5
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
pip3 install --ignore-installed --user pillow jsonschema
|
||||
python3 -m pip install --user --break-system-packages pillow jsonschema
|
||||
- name: Validate RSIs
|
||||
run: |
|
||||
python3 RobustToolbox/Schemas/validate_rsis.py Resources/
|
||||
|
||||
7
.github/workflows/yaml-linter.yml
vendored
7
.github/workflows/yaml-linter.yml
vendored
@@ -12,8 +12,15 @@ 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
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
.claude/
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
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,9 +1,8 @@
|
||||
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
|
||||
{
|
||||
@@ -23,37 +22,31 @@ namespace Content.Client.Communications.UI
|
||||
base.Open();
|
||||
|
||||
_menu = this.CreateWindow<CommunicationsConsoleMenu>();
|
||||
_menu.OnAnnounce += AnnounceButtonPressed;
|
||||
_menu.OnBroadcast += BroadcastButtonPressed;
|
||||
_menu.OnAlertLevel += AlertLevelSelected;
|
||||
_menu.OnEmergencyLevel += EmergencyShuttleButtonPressed;
|
||||
_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);
|
||||
}
|
||||
}
|
||||
|
||||
public void AlertLevelSelected(string level)
|
||||
{
|
||||
if (_menu!.AlertLevelSelectable)
|
||||
{
|
||||
_menu.CurrentLevel = level;
|
||||
SendMessage(new CommunicationsConsoleSelectAlertLevelMessage(level));
|
||||
}
|
||||
SendMessage(new CommunicationsConsoleSelectAlertLevelMessage(level));
|
||||
}
|
||||
|
||||
public void EmergencyShuttleButtonPressed()
|
||||
{
|
||||
if (_menu!.CountdownStarted)
|
||||
RecallShuttle();
|
||||
else
|
||||
CallShuttle();
|
||||
}
|
||||
|
||||
public void AnnounceButtonPressed(string message)
|
||||
public void RadioAnnounceButtonPressed(string message)
|
||||
{
|
||||
var maxLength = _cfg.GetCVar(CCVars.ChatMaxAnnouncementLength);
|
||||
var msg = SharedChatSystem.SanitizeAnnouncement(message, maxLength);
|
||||
SendMessage(new CommunicationsConsoleAnnounceMessage(msg));
|
||||
}
|
||||
|
||||
public void BroadcastButtonPressed(string message)
|
||||
public void ScreenBroadcastButtonPressed(string message)
|
||||
{
|
||||
SendMessage(new CommunicationsConsoleBroadcastMessage(message));
|
||||
}
|
||||
@@ -77,20 +70,7 @@ namespace Content.Client.Communications.UI
|
||||
|
||||
if (_menu != null)
|
||||
{
|
||||
_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;
|
||||
_menu.UpdateState(commsState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +1,32 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
<comms:CommunicationsConsoleMenu xmlns="https://spacestation14.io"
|
||||
xmlns:comms="clr-namespace:Content.Client.Communications.UI"
|
||||
xmlns:widgets="clr-namespace:Content.Client.Communications.UI.Widgets"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Title="{Loc 'comms-console-menu-title'}"
|
||||
MinSize="400 300">
|
||||
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" />
|
||||
|
||||
<!-- 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"/>
|
||||
<controls:HSpacer Spacing="20" />
|
||||
|
||||
<widgets:AlertLevelControls Name="AlertLevelControls" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:LayeredImageContainer>
|
||||
|
||||
<!-- EmergencyPart -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
SeparationOverride="6">
|
||||
<widgets:ShuttleControls Name="ShuttleControls" />
|
||||
|
||||
<RichTextLabel Name="CountdownLabel"/>
|
||||
|
||||
<Button Name="EmergencyShuttleButton"
|
||||
Access="Public"
|
||||
Text="Placeholder Text"
|
||||
ToolTip="{Loc 'comms-console-menu-emergency-shuttle-button-tooltip'}"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
</comms:CommunicationsConsoleMenu>
|
||||
|
||||
@@ -1,137 +1,83 @@
|
||||
using System.Globalization;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.CCVar;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Communications;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Communications.UI
|
||||
namespace Content.Client.Communications.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class CommunicationsConsoleMenu : BaseWindow
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class CommunicationsConsoleMenu : FancyWindow
|
||||
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()
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
public bool CanAnnounce;
|
||||
public bool CanBroadcast;
|
||||
public bool CanCall;
|
||||
public bool AlertLevelSelectable;
|
||||
public bool CountdownStarted;
|
||||
public string CurrentLevel = string.Empty;
|
||||
public TimeSpan? CountdownEnd;
|
||||
CloseButton.OnPressed += _ => Close();
|
||||
|
||||
public event Action? OnEmergencyLevel;
|
||||
public event Action<string>? OnAlertLevel;
|
||||
public event Action<string>? OnAnnounce;
|
||||
public event Action<string>? OnBroadcast;
|
||||
MessagingControls.OnRadioAnnounce += message => OnRadioAnnounce?.Invoke(message);
|
||||
MessagingControls.OnScreenBroadcast += message => OnScreenBroadcast?.Invoke(message);
|
||||
|
||||
public CommunicationsConsoleMenu()
|
||||
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)
|
||||
{
|
||||
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;
|
||||
return DragMode.Move;
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
else
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
UpdateCountdown();
|
||||
}
|
||||
var mode = DragMode.None;
|
||||
|
||||
// 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)
|
||||
if (relativeMousePos.Y > Size.Y - DragResizeSize)
|
||||
{
|
||||
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;
|
||||
mode |= DragMode.Bottom;
|
||||
}
|
||||
|
||||
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);
|
||||
if (relativeMousePos.X > Size.X - DragResizeSize)
|
||||
{
|
||||
mode |= DragMode.Right;
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
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")),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<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>
|
||||
@@ -0,0 +1,137 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<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>
|
||||
@@ -0,0 +1,117 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<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>
|
||||
@@ -0,0 +1,99 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,64 +1,110 @@
|
||||
<!-- offbrand completely redid this file, use offbrand's version if there are merge conflicts -->
|
||||
<controls:FancyWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
MaxHeight="525"
|
||||
MinWidth="300">
|
||||
MaxHeight="725"
|
||||
MinWidth="600">
|
||||
<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 Orientation="Horizontal">
|
||||
<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" />
|
||||
Name="LeftContainer"
|
||||
VerticalExpand="True"
|
||||
Orientation="Vertical"
|
||||
MinWidth="300">
|
||||
<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>
|
||||
<Label Margin="0 0 5 0" HorizontalExpand="True" HorizontalAlignment="Right" VerticalExpand="True"
|
||||
VerticalAlignment="Top" Name="ScanModeLabel"
|
||||
Text="{Loc 'health-analyzer-window-entity-unknown-text'}" />
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
|
||||
<GridContainer Margin="0 5 0 0" Columns="3">
|
||||
<Label Text="{Loc 'health-analyzer-window-entity-status-text'}" />
|
||||
<RichTextLabel Name="StatusLabel" />
|
||||
<TextureButton Name="StatusButton" StyleClasses="SpeciesInfoDefault" Scale="0.3 0.3" VerticalAlignment="Center" />
|
||||
<Label Name="BrainHealthText" Text="{Loc 'health-analyzer-window-entity-brain-health-text'}" />
|
||||
<RichTextLabel Name="BrainHealthLabel" />
|
||||
<TextureButton Name="BrainHealthButton" StyleClasses="SpeciesInfoDefault" Scale="0.3 0.3" VerticalAlignment="Center" />
|
||||
<Label Name="SpO2Text" />
|
||||
<RichTextLabel Name="SpO2Label" />
|
||||
<TextureButton Name="SpO2Button" StyleClasses="SpeciesInfoDefault" Scale="0.3 0.3" VerticalAlignment="Center" />
|
||||
<Label Name="BloodPressureText" Text="{Loc 'health-analyzer-window-entity-blood-pressure-text'}" />
|
||||
<RichTextLabel Name="BloodPressureLabel" />
|
||||
<TextureButton Name="BloodPressureButton" StyleClasses="SpeciesInfoDefault" Scale="0.3 0.3" VerticalAlignment="Center" />
|
||||
<Label Name="HeartRateText" Text="{Loc 'health-analyzer-window-entity-heart-rate-text'}" />
|
||||
<RichTextLabel Name="HeartRateLabel" />
|
||||
<TextureButton Name="HeartRateButton" StyleClasses="SpeciesInfoDefault" Scale="0.3 0.3" VerticalAlignment="Center" />
|
||||
<Label Name="RespiratoryRateText" Text="{Loc 'health-analyzer-window-entity-respiratory-rate-text'}" />
|
||||
<RichTextLabel Name="RespiratoryRateLabel" />
|
||||
<TextureButton Name="RespiratoryRateButton" StyleClasses="SpeciesInfoDefault" Scale="0.3 0.3" VerticalAlignment="Center" />
|
||||
<Label Name="EtCO2Text" />
|
||||
<RichTextLabel Name="EtCO2Label" />
|
||||
<TextureButton Name="EtCO2Button" StyleClasses="SpeciesInfoDefault" Scale="0.3 0.3" VerticalAlignment="Center" />
|
||||
<Label Name="LungHealthText" Text="{Loc 'health-analyzer-window-entity-lung-health-text'}" />
|
||||
<RichTextLabel Name="LungHealthLabel" />
|
||||
<TextureButton Name="LungHealthButton" StyleClasses="SpeciesInfoDefault" Scale="0.3 0.3" VerticalAlignment="Center" />
|
||||
<Label Name="HeartHealthText" Text="{Loc 'health-analyzer-window-entity-heart-health-text'}" />
|
||||
<RichTextLabel Name="HeartHealthLabel" />
|
||||
<TextureButton Name="HeartHealthButton" StyleClasses="SpeciesInfoDefault" Scale="0.3 0.3" VerticalAlignment="Center" />
|
||||
<Label Name="BloodText" Text="{Loc 'health-analyzer-window-entity-blood-level-text'}" />
|
||||
<RichTextLabel Name="BloodLabel" />
|
||||
<TextureButton Name="BloodButton" StyleClasses="SpeciesInfoDefault" Scale="0.3 0.3" VerticalAlignment="Center" />
|
||||
<Label Text="{Loc 'health-analyzer-window-entity-temperature-text'}" />
|
||||
<RichTextLabel Name="TemperatureLabel" />
|
||||
<TextureButton Name="TemperatureButton" StyleClasses="SpeciesInfoDefault" Scale="0.3 0.3" VerticalAlignment="Center" />
|
||||
<Label Text="{Loc 'health-analyzer-window-entity-damage-total-text'}" />
|
||||
<RichTextLabel Name="DamageLabel" />
|
||||
<TextureButton Name="DamageButton" StyleClasses="SpeciesInfoDefault" Scale="0.3 0.3" VerticalAlignment="Center" />
|
||||
</GridContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
<PanelContainer Name="ReagentsDivider" Visible="False" 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 Name="ReagentsContainer" Visible="False" Margin="0 5" Orientation="Vertical" HorizontalExpand="True" />
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer Name="AlertsDivider" Visible="False" StyleClasses="LowDivider" />
|
||||
|
||||
<BoxContainer Name="AlertsContainer" Visible="False" Margin="0 5" Orientation="Vertical" HorizontalAlignment="Center">
|
||||
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
<PanelContainer StyleClasses="LowDivider" VerticalExpand="True" Margin="0 0 0 0" SetWidth="2" />
|
||||
|
||||
<BoxContainer
|
||||
Name="GroupsContainer"
|
||||
Margin="0 5 0 5"
|
||||
Orientation="Vertical">
|
||||
</BoxContainer>
|
||||
Name="RightContainer"
|
||||
VerticalExpand="True"
|
||||
Orientation="Vertical"
|
||||
MinWidth="300">
|
||||
|
||||
<BoxContainer Name="AlertsContainer" Visible="False" Margin="0 5" Orientation="Vertical" HorizontalAlignment="Center"/>
|
||||
|
||||
<PanelContainer Name="AlertsDivider" Visible="False" StyleClasses="LowDivider" />
|
||||
|
||||
<BoxContainer
|
||||
Name="GroupsContainer"
|
||||
Margin="0 5 0 5"
|
||||
Orientation="Vertical">
|
||||
</BoxContainer>
|
||||
|
||||
<RichTextLabel
|
||||
Name="NoDamagesText"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalExpand="True"
|
||||
Text="{Loc health-analyzer-window-no-patient-damages}" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</controls:FancyWindow>
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared._Offbrand.Wounds; // Offbrand
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
@@ -30,6 +31,21 @@ namespace Content.Client.HealthAnalyzer.UI
|
||||
private readonly IPrototypeManager _prototypes;
|
||||
private readonly IResourceCache _cache;
|
||||
|
||||
// Begin Offbrand
|
||||
private readonly Tooltips.StatusTooltip _statusTooltip = new();
|
||||
private readonly Tooltips.BrainHealthTooltip _brainHealthTooltip = new();
|
||||
private readonly Tooltips.BloodPressureTooltip _bloodPressureTooltip = new();
|
||||
private readonly Tooltips.HeartRateTooltip _heartRateTooltip = new();
|
||||
private readonly Tooltips.HeartHealthTooltip _heartHealthTooltip = new();
|
||||
private readonly Tooltips.LungHealthTooltip _lungHealthTooltip = new();
|
||||
private readonly Tooltips.BloodTooltip _bloodTooltip = new();
|
||||
private readonly Tooltips.TemperatureTooltip _temperatureTooltip = new();
|
||||
private readonly Tooltips.DamageTooltip _damageTooltip = new();
|
||||
private readonly Tooltips.SpO2Tooltip _spo2Tooltip = new();
|
||||
private readonly Tooltips.EtCO2Tooltip _etco2Tooltip = new();
|
||||
private readonly Tooltips.RespiratoryRateTooltip _respiratoryRateTooltip = new();
|
||||
// End Offbrand
|
||||
|
||||
public HealthAnalyzerWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
@@ -39,6 +55,21 @@ namespace Content.Client.HealthAnalyzer.UI
|
||||
_spriteSystem = _entityManager.System<SpriteSystem>();
|
||||
_prototypes = dependencies.Resolve<IPrototypeManager>();
|
||||
_cache = dependencies.Resolve<IResourceCache>();
|
||||
|
||||
// Begin Offbrand
|
||||
StatusButton.TooltipSupplier = _ => _statusTooltip;
|
||||
BrainHealthButton.TooltipSupplier = _ => _brainHealthTooltip;
|
||||
BloodPressureButton.TooltipSupplier = _ => _bloodPressureTooltip;
|
||||
HeartRateButton.TooltipSupplier = _ => _heartRateTooltip;
|
||||
HeartHealthButton.TooltipSupplier = _ => _heartHealthTooltip;
|
||||
TemperatureButton.TooltipSupplier = _ => _temperatureTooltip;
|
||||
DamageButton.TooltipSupplier = _ => _damageTooltip;
|
||||
BloodButton.TooltipSupplier = _ => _bloodTooltip;
|
||||
LungHealthButton.TooltipSupplier = _ => _lungHealthTooltip;
|
||||
SpO2Button.TooltipSupplier = _ => _spo2Tooltip;
|
||||
EtCO2Button.TooltipSupplier = _ => _etco2Tooltip;
|
||||
RespiratoryRateButton.TooltipSupplier = _ => _respiratoryRateTooltip;
|
||||
// End Offbrand
|
||||
}
|
||||
|
||||
public void Populate(HealthAnalyzerScannedUserMessage msg)
|
||||
@@ -54,6 +85,16 @@ namespace Content.Client.HealthAnalyzer.UI
|
||||
|
||||
NoPatientDataText.Visible = false;
|
||||
|
||||
// Begin Offbrand Tooltips
|
||||
_brainHealthTooltip.Update(msg);
|
||||
_heartRateTooltip.Update(msg);
|
||||
_heartHealthTooltip.Update(msg);
|
||||
_temperatureTooltip.Update(msg, (target.Value, _entityManager.GetComponentOrNull<CryostasisFactorComponent>(target)));
|
||||
_spo2Tooltip.Update(msg);
|
||||
_etco2Tooltip.Update(msg);
|
||||
_respiratoryRateTooltip.Update(msg);
|
||||
// End Offbrand Tooltips
|
||||
|
||||
// Scan Mode
|
||||
|
||||
ScanModeLabel.Text = msg.ScanMode.HasValue
|
||||
@@ -104,7 +145,7 @@ namespace Content.Client.HealthAnalyzer.UI
|
||||
|
||||
// Alerts
|
||||
|
||||
var showAlerts = msg.Unrevivable == true || msg.Bleeding == true;
|
||||
var showAlerts = msg.Unrevivable == true || msg.Bleeding == true || msg.WoundableData?.NonMedicalReagents == true || msg.WoundableData?.Wounds != null; // Offbrand
|
||||
|
||||
AlertsDivider.Visible = showAlerts;
|
||||
AlertsContainer.Visible = showAlerts;
|
||||
@@ -128,6 +169,147 @@ namespace Content.Client.HealthAnalyzer.UI
|
||||
MaxWidth = 300
|
||||
});
|
||||
|
||||
// Begin Offbrand
|
||||
var showReagents = msg.WoundableData?.Reagents?.Count is { } count && count > 0;
|
||||
ReagentsDivider.Visible = showReagents;
|
||||
ReagentsContainer.Visible = showReagents;
|
||||
|
||||
if (msg.WoundableData is { } woundable)
|
||||
{
|
||||
if (woundable.Wounds is not null)
|
||||
{
|
||||
foreach (var wound in woundable.Wounds)
|
||||
{
|
||||
AlertsContainer.AddChild(new RichTextLabel
|
||||
{
|
||||
Text = Loc.GetString(wound),
|
||||
Margin = new Thickness(0, 4),
|
||||
MaxWidth = 300
|
||||
});
|
||||
}
|
||||
}
|
||||
if (woundable.NonMedicalReagents)
|
||||
{
|
||||
AlertsContainer.AddChild(new RichTextLabel
|
||||
{
|
||||
Text = Loc.GetString("health-analyzer-window-entity-non-medical-reagents"),
|
||||
Margin = new Thickness(0, 4),
|
||||
MaxWidth = 300
|
||||
});
|
||||
}
|
||||
if (woundable.Reagents is { } reagents)
|
||||
{
|
||||
ReagentsContainer.DisposeAllChildren();
|
||||
foreach (var (reagent, amounts) in reagents.OrderBy(kvp => _prototypes.Index(kvp.Key).LocalizedName))
|
||||
{
|
||||
var (quantity, metabolites) = amounts;
|
||||
var proto = _prototypes.Index(reagent);
|
||||
ReagentsContainer.AddChild(new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
HorizontalExpand = true,
|
||||
Children =
|
||||
{
|
||||
new PanelContainer
|
||||
{
|
||||
VerticalExpand = true,
|
||||
MinWidth = 4,
|
||||
PanelOverride = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = proto.SubstanceColor
|
||||
},
|
||||
Margin = new Thickness(4, 1),
|
||||
},
|
||||
|
||||
new Label { Text = proto.LocalizedName, HorizontalExpand = true, SizeFlagsStretchRatio = 3 },
|
||||
|
||||
new Label { Text = $"{metabolites}u", StyleClasses = { Content.Client.Stylesheets.StyleNano.StyleClassLabelSecondaryColor }, HorizontalExpand = true, SizeFlagsStretchRatio = 1 },
|
||||
|
||||
new Label { Text = $"{quantity}u", HorizontalExpand = true, SizeFlagsStretchRatio = 1 },
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
BrainHealthText.Visible = true;
|
||||
BrainHealthLabel.Visible = true;
|
||||
BrainHealthLabel.Text = Loc.GetString("health-analyzer-window-entity-brain-health-value", ("value", $"{woundable.BrainHealth * 100:F1}"));
|
||||
BrainHealthButton.Visible = true;
|
||||
|
||||
HeartHealthText.Visible = true;
|
||||
HeartHealthLabel.Visible = true;
|
||||
HeartHealthLabel.Text = Loc.GetString("health-analyzer-window-entity-heart-health-value", ("value", $"{woundable.HeartHealth * 100:F1}"));
|
||||
HeartHealthButton.Visible = true;
|
||||
|
||||
HeartRateText.Visible = true;
|
||||
HeartRateLabel.Visible = true;
|
||||
HeartRateLabel.Text = Loc.GetString("health-analyzer-window-entity-heart-rate-value", ("value", woundable.HeartRate));
|
||||
HeartRateButton.Visible = true;
|
||||
|
||||
var (systolic, diastolic) = woundable.BloodPressure;
|
||||
BloodPressureText.Visible = true;
|
||||
BloodPressureLabel.Visible = true;
|
||||
BloodPressureLabel.Text = Loc.GetString("health-analyzer-window-entity-blood-pressure-value", ("systolic", systolic), ("diastolic", diastolic));
|
||||
BloodPressureButton.Visible = true;
|
||||
|
||||
LungHealthText.Visible = true;
|
||||
LungHealthLabel.Visible = true;
|
||||
LungHealthLabel.Text = Loc.GetString("health-analyzer-window-entity-lung-health-value", ("value", $"{woundable.LungHealth * 100:F1}"));
|
||||
LungHealthButton.Visible = true;
|
||||
|
||||
SpO2Text.Visible = true;
|
||||
SpO2Text.Text = Loc.GetString("health-analyzer-window-entity-spo2-text", ("spo2", woundable.Spo2Name));
|
||||
SpO2Label.Visible = true;
|
||||
SpO2Label.Text = Loc.GetString("health-analyzer-window-entity-spo2-value", ("value", $"{woundable.Spo2 * 100:F1}"));
|
||||
SpO2Button.Visible = true;
|
||||
|
||||
EtCO2Text.Visible = true;
|
||||
EtCO2Text.Text = Loc.GetString("health-analyzer-window-entity-etco2-text", ("etco2", woundable.Etco2Name));
|
||||
EtCO2Label.Visible = true;
|
||||
EtCO2Label.Text = Loc.GetString("health-analyzer-window-entity-etco2-value", ("value", $"{woundable.Etco2}"));
|
||||
EtCO2Button.Visible = true;
|
||||
|
||||
RespiratoryRateText.Visible = true;
|
||||
RespiratoryRateLabel.Visible = true;
|
||||
RespiratoryRateLabel.Text = Loc.GetString("health-analyzer-window-entity-respiratory-rate-value", ("value", $"{woundable.RespiratoryRate}"));
|
||||
RespiratoryRateButton.Visible = true;
|
||||
|
||||
BloodLabel.Visible = false;
|
||||
BloodText.Visible = false;
|
||||
BloodButton.Visible = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
BrainHealthLabel.Visible = false;
|
||||
BloodPressureLabel.Visible = false;
|
||||
HeartRateLabel.Visible = false;
|
||||
HeartHealthLabel.Visible = false;
|
||||
LungHealthLabel.Visible = false;
|
||||
BrainHealthText.Visible = false;
|
||||
BloodPressureText.Visible = false;
|
||||
HeartRateText.Visible = false;
|
||||
HeartHealthText.Visible = false;
|
||||
LungHealthText.Visible = false;
|
||||
BrainHealthButton.Visible = false;
|
||||
BloodPressureButton.Visible = false;
|
||||
HeartRateButton.Visible = false;
|
||||
HeartHealthButton.Visible = false;
|
||||
LungHealthButton.Visible = false;
|
||||
SpO2Text.Visible = false;
|
||||
SpO2Label.Visible = false;
|
||||
SpO2Button.Visible = false;
|
||||
EtCO2Text.Visible = false;
|
||||
EtCO2Label.Visible = false;
|
||||
EtCO2Button.Visible = false;
|
||||
RespiratoryRateText.Visible = false;
|
||||
RespiratoryRateLabel.Visible = false;
|
||||
RespiratoryRateButton.Visible = false;
|
||||
|
||||
BloodLabel.Visible = true;
|
||||
BloodText.Visible = true;
|
||||
BloodButton.Visible = true;
|
||||
}
|
||||
// End Offbrand
|
||||
|
||||
// Damage Groups
|
||||
|
||||
var damageSortedGroups =
|
||||
@@ -194,6 +376,10 @@ namespace Content.Client.HealthAnalyzer.UI
|
||||
groupContainer.AddChild(CreateDiagnosticItemLabel(damageString.Insert(0, " · ")));
|
||||
}
|
||||
}
|
||||
|
||||
// Begin Offbrand
|
||||
NoDamagesText.Visible = GroupsContainer.ChildCount == 0;
|
||||
// End Offbrand
|
||||
}
|
||||
|
||||
private Texture GetTexture(string texture)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Content.Client.HealthAnalyzer.UI.Tooltips;
|
||||
|
||||
public sealed partial class BloodPressureTooltip : StaticTooltip
|
||||
{
|
||||
public override LocId Text => "health-analyzer-blood-pressure-tooltip";
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Content.Client.HealthAnalyzer.UI.Tooltips;
|
||||
|
||||
public sealed partial class BloodTooltip : StaticTooltip
|
||||
{
|
||||
public override LocId Text => "health-analyzer-blood-tooltip";
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Content.Shared.MedicalScanner;
|
||||
|
||||
namespace Content.Client.HealthAnalyzer.UI.Tooltips;
|
||||
|
||||
public sealed partial class BrainHealthTooltip : UpdatableTooltip
|
||||
{
|
||||
public override void Update(HealthAnalyzerScannedUserMessage msg)
|
||||
{
|
||||
if (msg.WoundableData is not { } woundable)
|
||||
return;
|
||||
|
||||
Label.Text = Loc.GetString("health-analyzer-brain-health-tooltip", ("dead", woundable.BrainHealth <= 0), ("spo2", $"{woundable.Spo2 * 100:F1}"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Content.Client.HealthAnalyzer.UI.Tooltips;
|
||||
|
||||
public sealed partial class DamageTooltip : StaticTooltip
|
||||
{
|
||||
public override LocId Text => "health-analyzer-damage-tooltip";
|
||||
}
|
||||
14
Content.Client/HealthAnalyzer/UI/Tooltips/EtCO2Tooltip.cs
Normal file
14
Content.Client/HealthAnalyzer/UI/Tooltips/EtCO2Tooltip.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Content.Shared.MedicalScanner;
|
||||
|
||||
namespace Content.Client.HealthAnalyzer.UI.Tooltips;
|
||||
|
||||
public sealed partial class EtCO2Tooltip : UpdatableTooltip
|
||||
{
|
||||
public override void Update(HealthAnalyzerScannedUserMessage msg)
|
||||
{
|
||||
if (msg.WoundableData is not { } woundable)
|
||||
return;
|
||||
|
||||
Label.Text = Loc.GetString("health-analyzer-etco2-tooltip", ("gas", woundable.Etco2GasName), ("etco2", woundable.Etco2Name));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Content.Shared.MedicalScanner;
|
||||
|
||||
namespace Content.Client.HealthAnalyzer.UI.Tooltips;
|
||||
|
||||
public sealed partial class HeartHealthTooltip : UpdatableTooltip
|
||||
{
|
||||
public override void Update(HealthAnalyzerScannedUserMessage msg)
|
||||
{
|
||||
if (msg.WoundableData is not { } woundable)
|
||||
return;
|
||||
|
||||
Label.Text = Loc.GetString("health-analyzer-heart-health-tooltip", ("heartrate", woundable.HeartRate));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Content.Shared.MedicalScanner;
|
||||
|
||||
namespace Content.Client.HealthAnalyzer.UI.Tooltips;
|
||||
|
||||
public sealed partial class HeartRateTooltip : UpdatableTooltip
|
||||
{
|
||||
public override void Update(HealthAnalyzerScannedUserMessage msg)
|
||||
{
|
||||
if (msg.WoundableData is not { } woundable)
|
||||
return;
|
||||
|
||||
Label.Text = Loc.GetString("health-analyzer-heart-rate-tooltip", ("spo2", woundable.Spo2Name));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Content.Client.HealthAnalyzer.UI.Tooltips;
|
||||
|
||||
public sealed partial class LungHealthTooltip : StaticTooltip
|
||||
{
|
||||
public override LocId Text => "health-analyzer-lung-health-tooltip";
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Content.Shared.MedicalScanner;
|
||||
|
||||
namespace Content.Client.HealthAnalyzer.UI.Tooltips;
|
||||
|
||||
public sealed partial class RespiratoryRateTooltip : UpdatableTooltip
|
||||
{
|
||||
public override void Update(HealthAnalyzerScannedUserMessage msg)
|
||||
{
|
||||
if (msg.WoundableData is not { } woundable)
|
||||
return;
|
||||
|
||||
Label.Text = Loc.GetString("health-analyzer-respiratory-rate-tooltip", ("etco2gas", woundable.Etco2GasName), ("etco2", woundable.Etco2Name), ("spo2gas", woundable.Spo2GasName), ("spo2", woundable.Spo2Name));
|
||||
}
|
||||
}
|
||||
14
Content.Client/HealthAnalyzer/UI/Tooltips/SpO2Tooltip.cs
Normal file
14
Content.Client/HealthAnalyzer/UI/Tooltips/SpO2Tooltip.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Content.Shared.MedicalScanner;
|
||||
|
||||
namespace Content.Client.HealthAnalyzer.UI.Tooltips;
|
||||
|
||||
public sealed partial class SpO2Tooltip : UpdatableTooltip
|
||||
{
|
||||
public override void Update(HealthAnalyzerScannedUserMessage msg)
|
||||
{
|
||||
if (msg.WoundableData is not { } woundable)
|
||||
return;
|
||||
|
||||
Label.Text = Loc.GetString("health-analyzer-spo2-tooltip", ("gas", woundable.Spo2GasName), ("spo2", woundable.Spo2Name));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<PanelContainer xmlns="https://spacestation14.io" StyleClasses="tooltipBox" MaxWidth="450">
|
||||
<BoxContainer
|
||||
Orientation="Vertical"
|
||||
RectClipContent="True"
|
||||
Margin="4">
|
||||
<RichTextLabel Name="Label" HorizontalExpand="True" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
@@ -0,0 +1,20 @@
|
||||
using Content.Shared.MedicalScanner;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.HealthAnalyzer.UI.Tooltips;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
[Virtual]
|
||||
public partial class StaticTooltip : PanelContainer
|
||||
{
|
||||
public StaticTooltip()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
Label.Text = Loc.GetString(Text);
|
||||
}
|
||||
|
||||
public virtual LocId Text => throw new NotImplementedException();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Content.Client.HealthAnalyzer.UI.Tooltips;
|
||||
|
||||
public sealed partial class StatusTooltip : StaticTooltip
|
||||
{
|
||||
public override LocId Text => "health-analyzer-status-tooltip";
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<PanelContainer xmlns="https://spacestation14.io" StyleClasses="tooltipBox">
|
||||
<BoxContainer
|
||||
Orientation="Vertical"
|
||||
RectClipContent="True"
|
||||
Margin="4">
|
||||
<RichTextLabel Name="Label" HorizontalExpand="True" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
@@ -0,0 +1,31 @@
|
||||
using Content.Shared._Offbrand.Wounds;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.MedicalScanner;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.HealthAnalyzer.UI.Tooltips;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class TemperatureTooltip : PanelContainer
|
||||
{
|
||||
public TemperatureTooltip()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void Update(HealthAnalyzerScannedUserMessage msg, Entity<CryostasisFactorComponent?> ent)
|
||||
{
|
||||
if (ent.Comp is null)
|
||||
{
|
||||
Label.Text = Loc.GetString("health-analyzer-plain-temperature-tooltip");
|
||||
}
|
||||
else
|
||||
{
|
||||
var factor = Math.Max(ent.Comp.TemperatureCoefficient * msg.Temperature + ent.Comp.TemperatureConstant, 1);
|
||||
Label.Text = Loc.GetString("health-analyzer-cryostasis-temperature-tooltip", ("factor", $"{factor * 100:F1}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<PanelContainer xmlns="https://spacestation14.io" StyleClasses="tooltipBox" MaxWidth="450">
|
||||
<BoxContainer
|
||||
Orientation="Vertical"
|
||||
RectClipContent="True"
|
||||
Margin="4">
|
||||
<RichTextLabel Access="Protected" Name="Label" HorizontalExpand="True" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
@@ -0,0 +1,21 @@
|
||||
using Content.Shared.MedicalScanner;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.HealthAnalyzer.UI.Tooltips;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
[Virtual]
|
||||
public partial class UpdatableTooltip : PanelContainer
|
||||
{
|
||||
public UpdatableTooltip()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public virtual void Update(HealthAnalyzerScannedUserMessage msg)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,7 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
|
||||
if (existingSensor.Coordinates != null && sensor.Coordinates == null)
|
||||
continue;
|
||||
|
||||
if (existingSensor.DamagePercentage != null && sensor.DamagePercentage == null)
|
||||
if (existingSensor.WoundableData != null && sensor.WoundableData == null) // Offbrand
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -231,21 +231,34 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
|
||||
// Specify texture for the user status icon
|
||||
var specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "alive");
|
||||
|
||||
// Begin Offbrand Additions
|
||||
if (sensor.WoundableData?.AnyVitalCritical == true)
|
||||
{
|
||||
specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "critical");
|
||||
}
|
||||
else if (sensor.WoundableData is { } woundableSummary)
|
||||
{
|
||||
specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), $"health{(byte)woundableSummary.Ranking}");
|
||||
}
|
||||
// End Offbrand Additions
|
||||
|
||||
if (!sensor.IsAlive)
|
||||
{
|
||||
specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "dead");
|
||||
}
|
||||
|
||||
else if (sensor.DamagePercentage != null)
|
||||
{
|
||||
var index = MathF.Round(4f * sensor.DamagePercentage.Value);
|
||||
// Begin Offbrand Removals
|
||||
// else if (sensor.DamagePercentage != null)
|
||||
// {
|
||||
// var index = MathF.Round(4f * sensor.DamagePercentage.Value);
|
||||
|
||||
if (index >= 5)
|
||||
specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "critical");
|
||||
// if (index >= 5)
|
||||
// specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "critical");
|
||||
|
||||
else
|
||||
specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "health" + index);
|
||||
}
|
||||
// else
|
||||
// specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "health" + index);
|
||||
// }
|
||||
// End Offbrand Removals
|
||||
|
||||
// Status icon
|
||||
var statusIcon = new AnimatedTextureRect
|
||||
@@ -303,6 +316,26 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
|
||||
|
||||
jobContainer.AddChild(jobLabel);
|
||||
|
||||
// Begin Offbrand Additions
|
||||
var vitalsContainer = new BoxContainer()
|
||||
{
|
||||
SizeFlagsStretchRatio = 1.25f,
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
HorizontalExpand = true,
|
||||
SeparationOverride = 8,
|
||||
};
|
||||
|
||||
if (sensor.WoundableData is { } woundable)
|
||||
{
|
||||
vitalsContainer.AddChild(new RichTextLabel() { Text = Loc.GetString("offbrand-crew-monitoring-heart-rate", ("rate", woundable.HeartRate)) });
|
||||
var (systolic, diastolic) = woundable.BloodPressure;
|
||||
vitalsContainer.AddChild(new RichTextLabel() { Text = Loc.GetString("offbrand-crew-monitoring-blood-pressure", ("systolic", systolic), ("diastolic", diastolic)) });
|
||||
vitalsContainer.AddChild(new RichTextLabel() { Text = Loc.GetString("offbrand-crew-monitoring-spo2", ("value", $"{woundable.Spo2 * 100:F1}"), ("spo2", woundable.Spo2Name)) });
|
||||
}
|
||||
|
||||
mainContainer.AddChild(vitalsContainer);
|
||||
// End Offbrand Additions
|
||||
|
||||
// Add user coordinates to the navmap
|
||||
if (coordinates != null && NavMap.Visible && _blipTexture != null)
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@ public sealed class ShowHealthBarsSystem : EquipmentHudSystem<ShowHealthBarsComp
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
|
||||
private EntityHealthBarOverlay _overlay = default!;
|
||||
private Content.Client._Offbrand.Overlays.HeartrateOverlay _heartrate = default!; // Offbrand
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -24,6 +25,7 @@ public sealed class ShowHealthBarsSystem : EquipmentHudSystem<ShowHealthBarsComp
|
||||
SubscribeLocalEvent<ShowHealthBarsComponent, AfterAutoHandleStateEvent>(OnHandleState);
|
||||
|
||||
_overlay = new(EntityManager, _prototype);
|
||||
_heartrate = new();
|
||||
}
|
||||
|
||||
private void OnHandleState(Entity<ShowHealthBarsComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
@@ -43,12 +45,19 @@ public sealed class ShowHealthBarsSystem : EquipmentHudSystem<ShowHealthBarsComp
|
||||
}
|
||||
|
||||
_overlay.StatusIcon = comp.HealthStatusIcon;
|
||||
_heartrate.StatusIcon = comp.HealthStatusIcon; // Offbrand
|
||||
}
|
||||
|
||||
if (!_overlayMan.HasOverlay<EntityHealthBarOverlay>())
|
||||
{
|
||||
_overlayMan.AddOverlay(_overlay);
|
||||
}
|
||||
// Begin Offbrand
|
||||
if (!_overlayMan.HasOverlay<Content.Client._Offbrand.Overlays.HeartrateOverlay>())
|
||||
{
|
||||
_overlayMan.AddOverlay(_heartrate);
|
||||
}
|
||||
// End Offbrand
|
||||
}
|
||||
|
||||
protected override void DeactivateInternal()
|
||||
@@ -57,5 +66,6 @@ public sealed class ShowHealthBarsSystem : EquipmentHudSystem<ShowHealthBarsComp
|
||||
|
||||
_overlay.DamageContainers.Clear();
|
||||
_overlayMan.RemoveOverlay(_overlay);
|
||||
_overlayMan.RemoveOverlay(_heartrate); // Offbrand
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ using Content.Shared.StatusIcon.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Linq;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared._Offbrand.Wounds; // Offbrand
|
||||
using Content.Shared.Mobs; // Offbrand
|
||||
|
||||
namespace Content.Client.Overlays;
|
||||
|
||||
@@ -60,8 +62,40 @@ public sealed class ShowHealthIconsSystem : EquipmentHudSystem<ShowHealthIconsCo
|
||||
args.StatusIcons.AddRange(healthIcons);
|
||||
}
|
||||
|
||||
// Begin Offbrand
|
||||
private List<HealthIconPrototype> DecideBrainHealthIcons(Entity<BrainDamageComponent, BrainDamageThresholdsComponent> ent)
|
||||
{
|
||||
if (ent.Comp2.CurrentState == MobState.Dead)
|
||||
{
|
||||
return new() { _prototypeMan.Index(ent.Comp2.DeadIcon) };
|
||||
}
|
||||
|
||||
var current = ent.Comp1.Damage;
|
||||
var max = ent.Comp1.MaxDamage;
|
||||
|
||||
if (ent.Comp2.CurrentState == MobState.Critical || ent.Comp1.Oxygen == 0)
|
||||
{
|
||||
var amount = ent.Comp2.CriticalDamageIcons.Count;
|
||||
var idx = Math.Clamp((int)Math.Floor(amount - (amount / max.Double()) * current.Double()), 0, amount-1);
|
||||
return new() { _prototypeMan.Index(ent.Comp2.CriticalDamageIcons[idx]) };
|
||||
}
|
||||
else
|
||||
{
|
||||
var amount = ent.Comp2.AliveDamageIcons.Count;
|
||||
var idx = Math.Clamp((int)Math.Floor(amount - (amount / max.Double()) * current.Double()), 0, amount-1);
|
||||
return new() { _prototypeMan.Index(ent.Comp2.AliveDamageIcons[idx]) };
|
||||
}
|
||||
}
|
||||
// End Offbrand
|
||||
|
||||
private IReadOnlyList<HealthIconPrototype> DecideHealthIcons(Entity<DamageableComponent> entity)
|
||||
{
|
||||
if (TryComp<BrainDamageComponent>(entity, out var brain) &&
|
||||
TryComp<BrainDamageThresholdsComponent>(entity, out var thresholds))
|
||||
{
|
||||
return DecideBrainHealthIcons((entity.Owner, brain, thresholds));
|
||||
}
|
||||
|
||||
var damageableComponent = entity.Comp;
|
||||
|
||||
if (damageableComponent.DamageContainerID == null ||
|
||||
|
||||
@@ -42,7 +42,8 @@ public sealed class PdaSheetlet : Sheetlet<NanotrasenStylesheet>
|
||||
|
||||
E<PanelContainer>()
|
||||
.Class("PdaBorderRect")
|
||||
.Prop(PanelContainer.StylePropertyPanel, angleBorderRect),
|
||||
.Prop(PanelContainer.StylePropertyPanel, angleBorderRect)
|
||||
.Prop(Control.StylePropertyModulateSelf, Color.FromHex("#00000040")),
|
||||
|
||||
//PDA - Buttons
|
||||
E<PdaSettingsButton>()
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Client.Stylesheets.Palette;
|
||||
using Content.Client.Stylesheets.Sheetlets;
|
||||
using Content.Client.Stylesheets.Stylesheets;
|
||||
using Content.Client.Stylesheets.SheetletConfigs;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using static Content.Client.Stylesheets.StylesheetHelpers;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
[CommonSheetlet]
|
||||
public sealed class LayeredImageContainerSheetlet : Sheetlet<NanotrasenStylesheet>
|
||||
{
|
||||
public override StyleRule[] GetRules(NanotrasenStylesheet sheet, object config)
|
||||
{
|
||||
IPanelConfig panelCfg = sheet;
|
||||
var panelMountBaseTex = ResCache.GetTexture("/Textures/Interface/Diegetic/PanelMountBase.svg.96dpi.png");
|
||||
var panelMountHighlightTex = ResCache.GetTexture("/Textures/Interface/Diegetic/PanelMountHighlight.svg.96dpi.png");
|
||||
var panelMountBaseStyleBox = new StyleBoxTexture
|
||||
{
|
||||
Texture = panelMountBaseTex,
|
||||
PatchMarginLeft = 16,
|
||||
PatchMarginTop = 16,
|
||||
PatchMarginRight = 24,
|
||||
PatchMarginBottom = 24
|
||||
};
|
||||
var panelMountHighlightStyleBox = new StyleBoxTexture
|
||||
{
|
||||
Texture = panelMountHighlightTex,
|
||||
PatchMarginLeft = 16,
|
||||
PatchMarginTop = 16,
|
||||
PatchMarginRight = 24,
|
||||
PatchMarginBottom = 24
|
||||
};
|
||||
|
||||
var borderTex = sheet.GetTexture(panelCfg.GeometricPanelBorderPath).IntoPatch(StyleBox.Margin.All, 10);
|
||||
|
||||
return [
|
||||
// Adds a raised border with rounded corners around a UI element
|
||||
E<LayeredImageContainer>().Class(LayeredImageContainer.StyleClassPanelMount)
|
||||
.Prop(LayeredImageContainer.StylePropertyMinimumContentMargin, new Thickness(10, 10, 16, 16)),
|
||||
|
||||
E<LayeredImageContainer>().Class(LayeredImageContainer.StyleClassPanelMount)
|
||||
.ParentOf(E<PanelContainer>().Identifier("Foreground1"))
|
||||
.Prop(PanelContainer.StylePropertyPanel, panelMountBaseStyleBox),
|
||||
|
||||
E<LayeredImageContainer>().Class(LayeredImageContainer.StyleClassPanelMount)
|
||||
.ParentOf(E<PanelContainer>().Identifier("Foreground2"))
|
||||
.Prop(PanelContainer.StylePropertyPanel, panelMountHighlightStyleBox),
|
||||
|
||||
E<LayeredImageContainer>().Class(StyleClass.PanelDark)
|
||||
.ParentOf(E<PanelContainer>().Identifier("Foreground1"))
|
||||
.Prop(Control.StylePropertyModulateSelf, sheet.SecondaryPalette.BackgroundDark),
|
||||
|
||||
/// Bright AngleRect with a subtle outline
|
||||
E<LayeredImageContainer>().Class(LayeredImageContainer.StyleClassBrightAngleRect)
|
||||
.ParentOf(E<PanelContainer>().Identifier("Background1"))
|
||||
.Prop(PanelContainer.StylePropertyPanel, StyleBoxHelpers.BaseStyleBox(sheet))
|
||||
.Prop(Control.StylePropertyModulateSelf, Palettes.Cyan.BackgroundLight),
|
||||
|
||||
E<LayeredImageContainer>().Class(LayeredImageContainer.StyleClassBrightAngleRect)
|
||||
.ParentOf(E<PanelContainer>().Identifier("Background2"))
|
||||
.Prop(PanelContainer.StylePropertyPanel, borderTex)
|
||||
.Prop(Control.StylePropertyModulateSelf, Color.FromHex("#00000040")),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Client.Stylesheets.SheetletConfigs;
|
||||
using Content.Client.Stylesheets.Palette;
|
||||
using Content.Client.Stylesheets.SheetletConfigs;
|
||||
using Content.Client.Stylesheets.Stylesheets;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Robust.Client.Graphics;
|
||||
@@ -14,16 +15,25 @@ public sealed class StripebackSheetlet<T> : Sheetlet<T> where T : PalettedStyles
|
||||
{
|
||||
IStripebackConfig stripebackCfg = sheet;
|
||||
|
||||
var stripeTex = sheet.GetTextureOr(stripebackCfg.StripebackPath, NanotrasenStylesheet.TextureRoot);
|
||||
var stripeBack = new StyleBoxTexture
|
||||
{
|
||||
Texture = sheet.GetTextureOr(stripebackCfg.StripebackPath, NanotrasenStylesheet.TextureRoot),
|
||||
Texture = stripeTex,
|
||||
Mode = StyleBoxTexture.StretchMode.Tile,
|
||||
Modulate = sheet.PrimaryPalette.BackgroundDark
|
||||
};
|
||||
var stripeBackWarning = new StyleBoxTexture {
|
||||
Texture = stripeTex,
|
||||
Mode = StyleBoxTexture.StretchMode.Tile,
|
||||
Modulate = Palettes.Amber.Element
|
||||
};
|
||||
|
||||
return
|
||||
[
|
||||
E<StripeBack>()
|
||||
.Prop(StripeBack.StylePropertyBackground, stripeBack),
|
||||
E<StripeBack>().Class(StyleClass.StatusWarning)
|
||||
.Prop(StripeBack.StylePropertyBackground, stripeBackWarning),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ public static class StyleClass
|
||||
public const string ButtonBig = "ButtonBig";
|
||||
|
||||
public const string CrossButtonRed = "CrossButtonRed";
|
||||
|
||||
public const string RefreshButton = "RefreshButton";
|
||||
|
||||
public const string ItemStatus = "ItemStatus";
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<controls:LayeredImageContainer xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
|
||||
<PanelContainer StyleIdentifier="Background1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
|
||||
<PanelContainer StyleIdentifier="Background2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
|
||||
<Control Name="ContentsContainer"/>
|
||||
<PanelContainer StyleIdentifier="Foreground1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
|
||||
<PanelContainer StyleIdentifier="Foreground2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
|
||||
</controls:LayeredImageContainer>
|
||||
@@ -0,0 +1,46 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// A generic container which can layer multiple styleboxes over/under the
|
||||
/// contents of of the child controls. This container contains two forground
|
||||
/// panels and two background panels, each of which can be styled independently.
|
||||
/// Background panels will be rendered underneath the child controls, while
|
||||
/// foreground panels will be rendered above. The use of two panels for each
|
||||
/// level allows for one level to use a stylized graphic, typically including
|
||||
/// some modulation to color-match the surrounding UI, while the second level
|
||||
/// can be used for shadows or highlights, which would typically not have such
|
||||
/// modulation.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public partial class LayeredImageContainer : Container
|
||||
{
|
||||
// Adds a raised border with rounded corners around a UI element
|
||||
public const string StyleClassPanelMount = "PanelMount";
|
||||
|
||||
// Bright AngleRect with a subtle outline
|
||||
public const string StyleClassBrightAngleRect = "BrightAngleRectOutline";
|
||||
|
||||
// The least amount of margin that a child needs to have to avoid drawing under
|
||||
// undesirable parts of the images. Children can add additional margins if desired
|
||||
public const string StylePropertyMinimumContentMargin = "MinimumContentMargin";
|
||||
|
||||
public LayeredImageContainer()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
XamlChildren = ContentsContainer.Children;
|
||||
}
|
||||
|
||||
protected override void StylePropertiesChanged()
|
||||
{
|
||||
if (TryGetStyleProperty<Thickness>(StylePropertyMinimumContentMargin, out var contentMargin))
|
||||
{
|
||||
ContentsContainer.Margin = contentMargin;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.StatusEffectNew;
|
||||
using Content.Shared.Traits.Assorted;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
@@ -11,6 +10,8 @@ using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Shared.Player;
|
||||
using System.Linq; // Offbrand
|
||||
using Content.Shared._Offbrand.Wounds; // Offbrand
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.DamageOverlays;
|
||||
|
||||
@@ -21,7 +22,9 @@ public sealed class DamageOverlayUiController : UIController
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
|
||||
[UISystemDependency] private readonly MobThresholdSystem _mobThresholdSystem = default!;
|
||||
[UISystemDependency] private readonly StatusEffectsSystem _statusEffects = default!;
|
||||
[UISystemDependency] private readonly HeartSystem _heart = default!; // Offbrand
|
||||
[UISystemDependency] private readonly PainSystem _pain = default!; // Offbrand
|
||||
|
||||
private Overlays.DamageOverlay _overlay = default!;
|
||||
|
||||
public override void Initialize()
|
||||
@@ -31,6 +34,7 @@ public sealed class DamageOverlayUiController : UIController
|
||||
SubscribeLocalEvent<LocalPlayerDetachedEvent>(OnPlayerDetached);
|
||||
SubscribeLocalEvent<MobStateChangedEvent>(OnMobStateChanged);
|
||||
SubscribeLocalEvent<MobThresholdChecked>(OnThresholdCheck);
|
||||
SubscribeLocalEvent<PotentiallyUpdateDamageOverlayEvent>(OnPotentiallyUpdateDamageOverlay); // Offbrand
|
||||
}
|
||||
|
||||
private void OnPlayerAttach(LocalPlayerAttachedEvent args)
|
||||
@@ -71,11 +75,62 @@ public sealed class DamageOverlayUiController : UIController
|
||||
_overlay.CritLevel = 0f;
|
||||
_overlay.PainLevel = 0f;
|
||||
_overlay.OxygenLevel = 0f;
|
||||
_overlay.AlwaysRenderAll = false; // Offbrand
|
||||
}
|
||||
|
||||
//TODO: Jezi: adjust oxygen and hp overlays to use appropriate systems once bodysim is implemented
|
||||
private void UpdateOverlays(EntityUid entity, MobStateComponent? mobState, DamageableComponent? damageable = null, MobThresholdsComponent? thresholds = null)
|
||||
{
|
||||
// Begin Offbrand Changes
|
||||
TryUpdateSimpleOverlays(entity, mobState, damageable, thresholds);
|
||||
TryUpdateWoundableOverlays(entity);
|
||||
}
|
||||
|
||||
private void OnPotentiallyUpdateDamageOverlay(ref PotentiallyUpdateDamageOverlayEvent args)
|
||||
{
|
||||
if (args.Target != _playerManager.LocalEntity)
|
||||
return;
|
||||
|
||||
UpdateOverlays(args.Target, null);
|
||||
}
|
||||
|
||||
private void TryUpdateWoundableOverlays(EntityUid entity)
|
||||
{
|
||||
if (!EntityManager.TryGetComponent<PainComponent>(entity, out var pain) ||
|
||||
!EntityManager.TryGetComponent<ShockThresholdsComponent>(entity, out var shockThresholds) ||
|
||||
!EntityManager.TryGetComponent<BrainDamageComponent>(entity, out var brainDamage) ||
|
||||
!EntityManager.TryGetComponent<BrainDamageThresholdsComponent>(entity, out var brainThresholds) ||
|
||||
!EntityManager.TryGetComponent<HeartrateComponent>(entity, out var heartrate))
|
||||
return;
|
||||
|
||||
_overlay.AlwaysRenderAll = true;
|
||||
var maxBrain = brainThresholds.DamageStateThresholds.Keys.Max();
|
||||
var maxShock = shockThresholds.Thresholds.Keys.Max();
|
||||
|
||||
switch (brainThresholds.CurrentState)
|
||||
{
|
||||
case MobState.Alive or MobState.Critical:
|
||||
{
|
||||
_overlay.CritLevel = FixedPoint2.Clamp(brainDamage.Damage / maxBrain, 0, 1).Float();
|
||||
_overlay.PainLevel = FixedPoint2.Clamp(_pain.GetShock((entity, pain)) / maxShock, 0, 1).Float();
|
||||
_overlay.OxygenLevel = FixedPoint2.Clamp(1 - _heart.Spo2((entity, heartrate)), 0, 1).Float();
|
||||
_overlay.DeadLevel = 0;
|
||||
break;
|
||||
}
|
||||
case MobState.Dead:
|
||||
{
|
||||
_overlay.CritLevel = 0;
|
||||
_overlay.PainLevel = 0;
|
||||
_overlay.OxygenLevel = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void TryUpdateSimpleOverlays(EntityUid entity, MobStateComponent? mobState, DamageableComponent? damageable = null, MobThresholdsComponent? thresholds = null)
|
||||
{
|
||||
// End Offbrand Changes
|
||||
if (mobState == null && !EntityManager.TryGetComponent(entity, out mobState) ||
|
||||
thresholds == null && !EntityManager.TryGetComponent(entity, out thresholds) ||
|
||||
damageable == null && !EntityManager.TryGetComponent(entity, out damageable))
|
||||
@@ -100,7 +155,7 @@ public sealed class DamageOverlayUiController : UIController
|
||||
FixedPoint2 painLevel = 0;
|
||||
_overlay.PainLevel = 0;
|
||||
|
||||
if (!_statusEffects.TryEffectsWithComp<PainNumbnessStatusEffectComponent>(entity, out _))
|
||||
if (!EntityManager.HasComponent<PainNumbnessComponent>(entity))
|
||||
{
|
||||
foreach (var painDamageType in damageable.PainDamageGroups)
|
||||
{
|
||||
|
||||
@@ -25,6 +25,8 @@ public sealed class DamageOverlay : Overlay
|
||||
|
||||
public MobState State = MobState.Alive;
|
||||
|
||||
public bool AlwaysRenderAll = false; // Offbrand
|
||||
|
||||
/// <summary>
|
||||
/// Handles the red pulsing overlay
|
||||
/// </summary>
|
||||
@@ -52,6 +54,7 @@ public sealed class DamageOverlay : Overlay
|
||||
{
|
||||
// TODO: Replace
|
||||
IoCManager.InjectDependencies(this);
|
||||
ZIndex = -5; // Offbrand
|
||||
_oxygenShader = _prototypeManager.Index(CircleMaskShader).InstanceUnique();
|
||||
_critShader = _prototypeManager.Index(CircleMaskShader).InstanceUnique();
|
||||
_bruteShader = _prototypeManager.Index(CircleMaskShader).InstanceUnique();
|
||||
@@ -138,39 +141,6 @@ public sealed class DamageOverlay : Overlay
|
||||
|
||||
// Makes debugging easier don't @ me
|
||||
float level = 0f;
|
||||
level = _oldPainLevel;
|
||||
|
||||
// TODO: Lerping
|
||||
if (level > 0f && _oldCritLevel <= 0f)
|
||||
{
|
||||
var pulseRate = 3f;
|
||||
var adjustedTime = time * pulseRate;
|
||||
float outerMaxLevel = 2.0f * distance;
|
||||
float outerMinLevel = 0.8f * distance;
|
||||
float innerMaxLevel = 0.6f * distance;
|
||||
float innerMinLevel = 0.2f * distance;
|
||||
|
||||
var outerRadius = outerMaxLevel - level * (outerMaxLevel - outerMinLevel);
|
||||
var innerRadius = innerMaxLevel - level * (innerMaxLevel - innerMinLevel);
|
||||
|
||||
var pulse = MathF.Max(0f, MathF.Sin(adjustedTime));
|
||||
|
||||
_bruteShader.SetParameter("time", pulse);
|
||||
_bruteShader.SetParameter("color", new Vector3(1f, 0f, 0f));
|
||||
_bruteShader.SetParameter("darknessAlphaOuter", 0.8f);
|
||||
|
||||
_bruteShader.SetParameter("outerCircleRadius", outerRadius);
|
||||
_bruteShader.SetParameter("outerCircleMaxRadius", outerRadius + 0.2f * distance);
|
||||
_bruteShader.SetParameter("innerCircleRadius", innerRadius);
|
||||
_bruteShader.SetParameter("innerCircleMaxRadius", innerRadius + 0.02f * distance);
|
||||
handle.UseShader(_bruteShader);
|
||||
handle.DrawRect(viewport, Color.White);
|
||||
}
|
||||
else
|
||||
{
|
||||
_oldPainLevel = PainLevel;
|
||||
}
|
||||
|
||||
level = State != MobState.Critical ? _oldOxygenLevel : 1f;
|
||||
|
||||
if (level > 0f)
|
||||
@@ -217,6 +187,41 @@ public sealed class DamageOverlay : Overlay
|
||||
handle.DrawRect(viewport, Color.White);
|
||||
}
|
||||
|
||||
// Offbrand: this code was relocated
|
||||
level = _oldPainLevel;
|
||||
|
||||
// TODO: Lerping
|
||||
if (level > 0f && (_oldCritLevel <= 0f || AlwaysRenderAll)) // Offbrand
|
||||
{
|
||||
var pulseRate = 3f;
|
||||
var adjustedTime = time * pulseRate;
|
||||
float outerMaxLevel = 2.0f * distance;
|
||||
float outerMinLevel = 0.8f * distance;
|
||||
float innerMaxLevel = 0.6f * distance;
|
||||
float innerMinLevel = 0.2f * distance;
|
||||
|
||||
var outerRadius = outerMaxLevel - level * (outerMaxLevel - outerMinLevel);
|
||||
var innerRadius = innerMaxLevel - level * (innerMaxLevel - innerMinLevel);
|
||||
|
||||
var pulse = MathF.Max(0f, MathF.Sin(adjustedTime));
|
||||
|
||||
_bruteShader.SetParameter("time", pulse);
|
||||
_bruteShader.SetParameter("color", new Vector3(1f, 0f, 0f));
|
||||
_bruteShader.SetParameter("darknessAlphaOuter", 0.8f);
|
||||
|
||||
_bruteShader.SetParameter("outerCircleRadius", outerRadius);
|
||||
_bruteShader.SetParameter("outerCircleMaxRadius", outerRadius + 0.2f * distance);
|
||||
_bruteShader.SetParameter("innerCircleRadius", innerRadius);
|
||||
_bruteShader.SetParameter("innerCircleMaxRadius", innerRadius + 0.02f * distance);
|
||||
handle.UseShader(_bruteShader);
|
||||
handle.DrawRect(viewport, Color.White);
|
||||
}
|
||||
else
|
||||
{
|
||||
_oldPainLevel = PainLevel;
|
||||
}
|
||||
|
||||
|
||||
level = State != MobState.Dead ? _oldCritLevel : DeadLevel;
|
||||
|
||||
if (level > 0f)
|
||||
|
||||
43
Content.Client/_Offbrand/MMI/MMIExtractorEui.cs
Normal file
43
Content.Client/_Offbrand/MMI/MMIExtractorEui.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Content.Client.Eui;
|
||||
using Content.Shared._Offbrand.MMI;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
|
||||
namespace Content.Client._Offbrand.MMI;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class MMIExtractorEui : BaseEui
|
||||
{
|
||||
private readonly MMIExtractorMenu _menu;
|
||||
|
||||
public MMIExtractorEui()
|
||||
{
|
||||
_menu = new MMIExtractorMenu();
|
||||
|
||||
_menu.DenyButton.OnPressed += _ =>
|
||||
{
|
||||
SendMessage(new MMIExtractorMessage(false));
|
||||
_menu.Close();
|
||||
};
|
||||
|
||||
_menu.AcceptButton.OnPressed += _ =>
|
||||
{
|
||||
SendMessage(new MMIExtractorMessage(true));
|
||||
_menu.Close();
|
||||
};
|
||||
}
|
||||
|
||||
public override void Opened()
|
||||
{
|
||||
IoCManager.Resolve<IClyde>().RequestWindowAttention();
|
||||
_menu.OpenCentered();
|
||||
}
|
||||
|
||||
public override void Closed()
|
||||
{
|
||||
base.Closed();
|
||||
|
||||
SendMessage(new MMIExtractorMessage(false));
|
||||
_menu.Close();
|
||||
}
|
||||
}
|
||||
55
Content.Client/_Offbrand/MMI/MMIExtractorMenu.cs
Normal file
55
Content.Client/_Offbrand/MMI/MMIExtractorMenu.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
namespace Content.Client._Offbrand.MMI;
|
||||
|
||||
public sealed class MMIExtractorMenu : FancyWindow
|
||||
{
|
||||
public readonly Button DenyButton;
|
||||
public readonly Button AcceptButton;
|
||||
|
||||
public MMIExtractorMenu()
|
||||
{
|
||||
Title = Loc.GetString("mmi-extractor-title");
|
||||
|
||||
ContentsContainer.AddChild(new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
Margin = new Thickness(6),
|
||||
Children =
|
||||
{
|
||||
(new Label()
|
||||
{
|
||||
Text = Loc.GetString("mmi-extractor-prompt"),
|
||||
}),
|
||||
new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
Align = AlignMode.Center,
|
||||
Children =
|
||||
{
|
||||
(AcceptButton = new Button
|
||||
{
|
||||
Text = Loc.GetString("mmi-extractor-accept"),
|
||||
}),
|
||||
|
||||
(new Control()
|
||||
{
|
||||
MinSize = new Vector2(20, 0)
|
||||
}),
|
||||
|
||||
(DenyButton = new Button
|
||||
{
|
||||
Text = Loc.GetString("mmi-extractor-decline"),
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
101
Content.Client/_Offbrand/Overlays/HeartrateOverlay.cs
Normal file
101
Content.Client/_Offbrand/Overlays/HeartrateOverlay.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.StatusIcon;
|
||||
using Content.Shared._Offbrand.Wounds;
|
||||
using Content.Shared.StatusIcon.Components;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client._Offbrand.Overlays;
|
||||
|
||||
public sealed class HeartrateOverlay : Overlay
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
|
||||
private readonly HeartSystem _heart;
|
||||
private readonly SharedTransformSystem _transform;
|
||||
private readonly SpriteSystem _sprite;
|
||||
private readonly StatusIconSystem _statusIcon;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
|
||||
|
||||
public ProtoId<HealthIconPrototype>? StatusIcon;
|
||||
|
||||
private static readonly SpriteSpecifier HudStopped = new SpriteSpecifier.Rsi(new("/Textures/_Offbrand/heart_rate_hud.rsi"), "hud_stopped");
|
||||
private static readonly SpriteSpecifier HudGood = new SpriteSpecifier.Rsi(new("/Textures/_Offbrand/heart_rate_hud.rsi"), "hud_normal");
|
||||
private static readonly SpriteSpecifier HudOkay = new SpriteSpecifier.Rsi(new("/Textures/_Offbrand/heart_rate_hud.rsi"), "hud_okay");
|
||||
private static readonly SpriteSpecifier HudPoor = new SpriteSpecifier.Rsi(new("/Textures/_Offbrand/heart_rate_hud.rsi"), "hud_poor");
|
||||
private static readonly SpriteSpecifier HudBad = new SpriteSpecifier.Rsi(new("/Textures/_Offbrand/heart_rate_hud.rsi"), "hud_bad");
|
||||
private static readonly SpriteSpecifier HudDanger = new SpriteSpecifier.Rsi(new("/Textures/_Offbrand/heart_rate_hud.rsi"), "hud_danger");
|
||||
private static readonly IReadOnlyList<SpriteSpecifier> Severities = new List<SpriteSpecifier>() { HudGood, HudOkay, HudPoor, HudBad, HudDanger };
|
||||
|
||||
public HeartrateOverlay()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_transform = _entityManager.System<SharedTransformSystem>();
|
||||
_sprite = _entityManager.System<SpriteSystem>();
|
||||
_statusIcon = _entityManager.System<StatusIconSystem>();
|
||||
_heart = _entityManager.System<HeartSystem>();
|
||||
}
|
||||
|
||||
private SpriteSpecifier GetIcon(Entity<HeartrateComponent> ent)
|
||||
{
|
||||
if (!ent.Comp.Running)
|
||||
return HudStopped;
|
||||
|
||||
var max = 4;
|
||||
var severity = Math.Min((int)Math.Round(max * _heart.Strain(ent)), max);
|
||||
return Severities[severity];
|
||||
}
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
var handle = args.WorldHandle;
|
||||
var rotation = args.Viewport.Eye?.Rotation ?? Angle.Zero;
|
||||
|
||||
const float scale = 1f;
|
||||
var scaleMatrix = Matrix3Helpers.CreateScale(new Vector2(scale, scale));
|
||||
var rotationMatrix = Matrix3Helpers.CreateRotation(-rotation);
|
||||
|
||||
_prototype.TryIndex(StatusIcon, out var statusIcon);
|
||||
|
||||
var query = _entityManager.AllEntityQueryEnumerator<MetaDataComponent, TransformComponent, HeartrateComponent, SpriteComponent>();
|
||||
while (query.MoveNext(out var uid,
|
||||
out var metadata,
|
||||
out var xform,
|
||||
out var heartrate,
|
||||
out var sprite))
|
||||
{
|
||||
if (statusIcon != null && !_statusIcon.IsVisible((uid, metadata), statusIcon))
|
||||
continue;
|
||||
|
||||
var bounds = _entityManager.GetComponentOrNull<StatusIconComponent>(uid)?.Bounds ?? _sprite.GetLocalBounds((uid, sprite));
|
||||
var worldPos = _transform.GetWorldPosition(xform);
|
||||
|
||||
if (!bounds.Translated(worldPos).Intersects(args.WorldAABB))
|
||||
continue;
|
||||
|
||||
var worldPosition = _transform.GetWorldPosition(xform);
|
||||
var worldMatrix = Matrix3Helpers.CreateTranslation(worldPosition);
|
||||
|
||||
var scaledWorld = Matrix3x2.Multiply(scaleMatrix, worldMatrix);
|
||||
var matty = Matrix3x2.Multiply(rotationMatrix, scaledWorld);
|
||||
|
||||
handle.SetTransform(matty);
|
||||
|
||||
var curTime = _timing.RealTime;
|
||||
var texture = _sprite.GetFrame(GetIcon((uid, heartrate)), curTime);
|
||||
|
||||
handle.DrawTexture(texture, new Vector2(-8f, 8f) / EyeManager.PixelsPerMeter);
|
||||
}
|
||||
|
||||
handle.SetTransform(Matrix3x2.Identity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Content.Shared._Offbrand.Surgery;
|
||||
using Content.Shared.Construction.Prototypes;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client._Offbrand.Surgery;
|
||||
|
||||
public sealed class SurgeryGuideBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
private SurgeryGuideMenu? _menu;
|
||||
|
||||
public SurgeryGuideBoundUserInterface(EntityUid owner, Enum key) : base(owner, key)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
if (!EntMan.TryGetComponent<SurgeryGuideTargetComponent>(Owner, out var comp))
|
||||
return;
|
||||
|
||||
_menu = this.CreateWindow<SurgeryGuideMenu>();
|
||||
_menu.Category = comp.Category;
|
||||
_menu.OnSurgerySelected += OnSurgerySelected;
|
||||
_menu.OnCleanUp += OnCleanUp;
|
||||
_menu.Populate();
|
||||
}
|
||||
|
||||
private void OnSurgerySelected(ProtoId<ConstructionPrototype> surgery)
|
||||
{
|
||||
SendPredictedMessage(new SurgeryGuideStartSurgeryMessage(surgery));
|
||||
}
|
||||
|
||||
private void OnCleanUp()
|
||||
{
|
||||
SendPredictedMessage(new SurgeryGuideStartCleanupMessage());
|
||||
}
|
||||
}
|
||||
23
Content.Client/_Offbrand/Surgery/SurgeryGuideMenu.xaml
Normal file
23
Content.Client/_Offbrand/Surgery/SurgeryGuideMenu.xaml
Normal file
@@ -0,0 +1,23 @@
|
||||
<controls:FancyWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
MinHeight="450"
|
||||
MinWidth="350"
|
||||
Title="{Loc 'surgery-guide-title'}">
|
||||
|
||||
<BoxContainer Name="SurgeriesContainer" Orientation="Vertical">
|
||||
<controls:ListContainer Name="PossibleSurgeries" VerticalExpand="True" Margin="4 4"/>
|
||||
<Button Name="CleanUp" Text="{Loc 'surgery-guide-clean-up'}" StyleClasses="OpenBoth" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Name="StepsContainer" Orientation="Vertical" Visible="False">
|
||||
<BoxContainer Orientation="Vertical" Margin="4 4">
|
||||
<Label Name="SurgeryName" StyleClasses="LabelHeading"/>
|
||||
<RichTextLabel Name="SurgeryDescription"/>
|
||||
</BoxContainer>
|
||||
|
||||
<ItemList Name="StepsList" VerticalExpand="True" Margin="4 4" />
|
||||
|
||||
<Button Name="BackButton" Text="{Loc 'surgery-guide-back'}" StyleClasses="OpenBoth" />
|
||||
<Button Name="PerformButton" Text="{Loc 'surgery-guide-perform-surgery'}" StyleClasses="OpenBoth" />
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
152
Content.Client/_Offbrand/Surgery/SurgeryGuideMenu.xaml.cs
Normal file
152
Content.Client/_Offbrand/Surgery/SurgeryGuideMenu.xaml.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
using Content.Client.Construction;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Construction.Prototypes;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client._Offbrand.Surgery;
|
||||
|
||||
public record SurgeryListData(ConstructionPrototype Construction) : ListData;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class SurgeryGuideMenu : FancyWindow
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
private readonly ConstructionSystem _construction = default!;
|
||||
private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public event Action<ProtoId<ConstructionPrototype>>? OnSurgerySelected;
|
||||
public event Action? OnCleanUp;
|
||||
|
||||
private ConstructionPrototype? _selectedSurgery;
|
||||
public string Category = string.Empty;
|
||||
|
||||
public SurgeryGuideMenu()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_sprite = _entityManager.System<SpriteSystem>();
|
||||
_construction = _entityManager.System<ConstructionSystem>();
|
||||
|
||||
_construction.ConstructionGuideAvailable += GuideAvailable;
|
||||
|
||||
BackButton.OnPressed += _ =>
|
||||
{
|
||||
SurgeriesContainer.Visible = true;
|
||||
StepsContainer.Visible = false;
|
||||
};
|
||||
PerformButton.OnPressed += _ =>
|
||||
{
|
||||
if (_selectedSurgery is not { } surgery)
|
||||
return;
|
||||
|
||||
OnSurgerySelected?.Invoke(surgery.ID);
|
||||
};
|
||||
CleanUp.OnPressed += _ => OnCleanUp?.Invoke();
|
||||
|
||||
PossibleSurgeries.GenerateItem += GenerateButton;
|
||||
PossibleSurgeries.ItemKeyBindDown += OnSelectSurgery;
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
_construction.ConstructionGuideAvailable -= GuideAvailable;
|
||||
}
|
||||
|
||||
private void GuideAvailable(object? sender, string id)
|
||||
{
|
||||
if (_selectedSurgery?.ID != id)
|
||||
return;
|
||||
|
||||
RefreshSteps();
|
||||
}
|
||||
|
||||
private void OnSelectSurgery(GUIBoundKeyEventArgs args, ListData data)
|
||||
{
|
||||
if (data is not SurgeryListData entry)
|
||||
return;
|
||||
|
||||
SurgeriesContainer.Visible = false;
|
||||
StepsContainer.Visible = true;
|
||||
|
||||
_selectedSurgery = entry.Construction;
|
||||
|
||||
SurgeryName.Text = Loc.GetString(entry.Construction.SetName!.Value);
|
||||
SurgeryDescription.Text = Loc.GetString(entry.Construction.SetDescription!.Value);
|
||||
|
||||
RefreshSteps();
|
||||
}
|
||||
|
||||
private void RefreshSteps()
|
||||
{
|
||||
StepsList.Clear();
|
||||
|
||||
if (_selectedSurgery is null || _construction.GetGuide(_selectedSurgery) is not { } guide)
|
||||
return;
|
||||
|
||||
foreach (var entry in guide.Entries)
|
||||
{
|
||||
var text = entry.Arguments != null
|
||||
? Loc.GetString(entry.Localization, entry.Arguments)
|
||||
: Loc.GetString(entry.Localization);
|
||||
|
||||
if (entry.EntryNumber is { } number)
|
||||
{
|
||||
text = Loc.GetString("construction-presenter-step-wrapper",
|
||||
("step-number", number),
|
||||
("text", text));
|
||||
}
|
||||
|
||||
text = text.PadLeft(text.Length + entry.Padding);
|
||||
|
||||
var icon = entry.Icon != null ? _sprite.Frame0(entry.Icon) : Texture.Transparent;
|
||||
StepsList.AddItem(text, icon, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateButton(ListData data, ListContainerButton button)
|
||||
{
|
||||
if (data is not SurgeryListData entry)
|
||||
return;
|
||||
|
||||
button.AddChild(new Label() { Text = Loc.GetString(entry.Construction.SetName!.Value) });
|
||||
button.ToolTip = Loc.GetString(entry.Construction.SetDescription!.Value);
|
||||
button.AddStyleClass("ButtonSquare");
|
||||
}
|
||||
|
||||
public void Populate()
|
||||
{
|
||||
var listData = new List<SurgeryListData>();
|
||||
|
||||
foreach (var proto in _prototypeManager.EnumeratePrototypes<ConstructionPrototype>())
|
||||
{
|
||||
if (proto.Category != Category)
|
||||
continue;
|
||||
|
||||
listData.Add(new SurgeryListData(proto));
|
||||
}
|
||||
|
||||
listData.Sort((a, b) =>
|
||||
{
|
||||
if (a.Construction.SetName is not { } aName)
|
||||
throw new InvalidOperationException($"Construction {a.Construction.ID} does not have a name");
|
||||
|
||||
if (b.Construction.SetName is not { } bName)
|
||||
throw new InvalidOperationException($"Construction {b.Construction.ID} does not have a name");
|
||||
|
||||
return string.Compare(Loc.GetString(aName), Loc.GetString(bName), StringComparison.InvariantCulture);
|
||||
});
|
||||
|
||||
PossibleSurgeries.PopulateList(listData);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using Content.Shared._Offbrand.Surgery;
|
||||
|
||||
namespace Content.Client._Offbrand.Surgery;
|
||||
|
||||
public sealed class SurgeryGuideTargetSystem : SharedSurgeryGuideTargetSystem;
|
||||
@@ -0,0 +1,5 @@
|
||||
using Content.Shared._Offbrand.Wounds;
|
||||
|
||||
namespace Content.Client._Offbrand.Wounds;
|
||||
|
||||
public sealed class WoundableHealthAnalyzerSystem : SharedWoundableHealthAnalyzerSystem;
|
||||
20
Content.Client/_Wega/Ghost/GhostRespawnSystem.cs
Normal file
20
Content.Client/_Wega/Ghost/GhostRespawnSystem.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Content.Shared.Wega.Ghost.Respawn;
|
||||
|
||||
namespace Content.Client.Wega.Ghost.Respawn;
|
||||
|
||||
public sealed class GhostRespawnSystem : EntitySystem
|
||||
{
|
||||
public TimeSpan? GhostRespawnTime { get; private set; }
|
||||
public event Action? GhostRespawn;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeNetworkEvent<GhostRespawnEvent>(OnGhostRespawnReset);
|
||||
}
|
||||
|
||||
private void OnGhostRespawnReset(GhostRespawnEvent e)
|
||||
{
|
||||
GhostRespawnTime = e.Time;
|
||||
GhostRespawn?.Invoke();
|
||||
}
|
||||
}
|
||||
41
Content.Client/_Wega/Vampire/Ui/SelectClassMenu.xaml
Normal file
41
Content.Client/_Wega/Vampire/Ui/SelectClassMenu.xaml
Normal file
@@ -0,0 +1,41 @@
|
||||
<ui:RadialMenu xmlns="https://spacestation14.io"
|
||||
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:local="clr-namespace:Content.Client.Select.Class.UI;assembly=Content.Client"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Content.Client.Select.Class.UI.SelectClassMenu"
|
||||
BackButtonStyleClass="RadialMenuBackButton"
|
||||
CloseButtonStyleClass="RadialMenuCloseButton"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
MinSize="450 450">
|
||||
|
||||
<!-- Main Radial Menu Container -->
|
||||
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" InitialRadius="64" ReserveSpaceForHiddenChildren="False">
|
||||
<!-- Button 1: Hemomancer -->
|
||||
<ui:RadialMenuButton Name="HemomancerButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-hemomancer'}" TargetLayerControlName="Hemomancer">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/_Wega/claws.png"/>
|
||||
</ui:RadialMenuButton>
|
||||
|
||||
<!-- Button 2: Umbrae -->
|
||||
<ui:RadialMenuButton Name="UmbraeButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-umbrae'}" TargetLayerControlName="Umbrae">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/_Wega/cloak.png"/>
|
||||
</ui:RadialMenuButton>
|
||||
|
||||
<!-- Button 3: Gargantua -->
|
||||
<ui:RadialMenuButton Name="GargantuaButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-gargantua'}" TargetLayerControlName="Gargantua">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/_Wega/swell.png"/>
|
||||
</ui:RadialMenuButton>
|
||||
|
||||
<!-- Button 4: Dantalion -->
|
||||
<ui:RadialMenuButton Name="DantalionButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-dantalion'}" TargetLayerControlName="Dantalion">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/_Wega/enthrall.png"/>
|
||||
</ui:RadialMenuButton>
|
||||
|
||||
<!-- Button 5: Bestia -->
|
||||
<!-- <ui:RadialMenuButton Name="BestiaButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-bestia'}" TargetLayerControlName="Bestia">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/_Wega/rush.png"/>
|
||||
</ui:RadialMenuButton> -->
|
||||
|
||||
</ui:RadialContainer>
|
||||
|
||||
</ui:RadialMenu>
|
||||
43
Content.Client/_Wega/Vampire/Ui/SelectClassMenu.xaml.cs
Normal file
43
Content.Client/_Wega/Vampire/Ui/SelectClassMenu.xaml.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Vampire;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Client.Select.Class.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class SelectClassMenu : RadialMenu
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IEntityNetworkManager _entityNetworkManager = default!;
|
||||
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;
|
||||
|
||||
public event Action<string>? OnSelectClass;
|
||||
|
||||
public SelectClassMenu()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
InitializeButtons();
|
||||
}
|
||||
|
||||
private void InitializeButtons()
|
||||
{
|
||||
HemomancerButton.OnButtonUp += _ => HandleClassSelection("Hemomancer");
|
||||
UmbraeButton.OnButtonUp += _ => HandleClassSelection("Umbrae");
|
||||
GargantuaButton.OnButtonUp += _ => HandleClassSelection("Gargantua");
|
||||
DantalionButton.OnButtonUp += _ => HandleClassSelection("Dantalion");
|
||||
//BestiaButton.OnButtonUp += _ => HandleClassSelection("Bestia");
|
||||
}
|
||||
|
||||
private void HandleClassSelection(string className)
|
||||
{
|
||||
OnSelectClass?.Invoke(className);
|
||||
var netEntity = _entityManager.GetNetEntity(_playerManager.LocalSession?.AttachedEntity ?? EntityUid.Invalid);
|
||||
_entityNetworkManager.SendSystemNetworkMessage(new VampireSelectClassMenuClosedEvent(netEntity, className));
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
46
Content.Client/_Wega/Vampire/Ui/SelectClassUIController.cs
Normal file
46
Content.Client/_Wega/Vampire/Ui/SelectClassUIController.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using Content.Client.Select.Class.UI;
|
||||
using Content.Shared.Vampire;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Select.Class
|
||||
{
|
||||
public sealed class SelectClassUIController : UIController
|
||||
{
|
||||
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
private SelectClassMenu? _menu;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeNetworkEvent<SelectClassPressedEvent>(OnSelectClassMenuReceived);
|
||||
}
|
||||
|
||||
private void OnSelectClassMenuReceived(SelectClassPressedEvent args, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
var session = IoCManager.Resolve<IPlayerManager>().LocalSession;
|
||||
var userEntity = _entityManager.GetEntity(args.Uid);
|
||||
if (session?.AttachedEntity.HasValue == true && session.AttachedEntity.Value == userEntity)
|
||||
{
|
||||
if (_menu is null)
|
||||
{
|
||||
_menu = _uiManager.CreateWindow<SelectClassMenu>();
|
||||
_menu.OnClose += OnMenuClosed;
|
||||
_menu.OpenCentered();
|
||||
}
|
||||
else
|
||||
{
|
||||
_menu.OpenCentered();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMenuClosed()
|
||||
{
|
||||
_menu = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
63
Content.Client/_Wega/Vampire/VampireSystem.cs
Normal file
63
Content.Client/_Wega/Vampire/VampireSystem.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using Content.Client.Alerts;
|
||||
using Content.Client.Movement.Systems;
|
||||
using Content.Shared.StatusIcon.Components;
|
||||
using Content.Shared.Vampire;
|
||||
using Content.Shared.Vampire.Components;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Vampire;
|
||||
|
||||
public sealed class VampireSystem : SharedVampireSystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly ContentEyeSystem _contentEye = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<VampireToggleFovEvent>(OnToggleFoV);
|
||||
SubscribeLocalEvent<VampireComponent, GetStatusIconsEvent>(GetVampireIcons);
|
||||
SubscribeLocalEvent<ThrallComponent, GetStatusIconsEvent>(GetThrallIcons);
|
||||
SubscribeLocalEvent<VampireComponent, UpdateAlertSpriteEvent>(OnUpdateAlert);
|
||||
}
|
||||
|
||||
private void OnToggleFoV(VampireToggleFovEvent args)
|
||||
{
|
||||
var userEntity = _entityManager.GetEntity(args.User);
|
||||
var eyeComponent = _entityManager.GetComponent<EyeComponent>(userEntity);
|
||||
if (userEntity == _playerManager.LocalEntity)
|
||||
_contentEye.RequestToggleFov(userEntity, eyeComponent);
|
||||
}
|
||||
|
||||
private void GetVampireIcons(Entity<VampireComponent> ent, ref GetStatusIconsEvent args)
|
||||
{
|
||||
var iconPrototype = _prototype.Index(ent.Comp.StatusIcon);
|
||||
args.StatusIcons.Add(iconPrototype);
|
||||
}
|
||||
|
||||
private void GetThrallIcons(Entity<ThrallComponent> ent, ref GetStatusIconsEvent args)
|
||||
{
|
||||
if (HasComp<VampireComponent>(ent))
|
||||
return;
|
||||
|
||||
var iconPrototype = _prototype.Index(ent.Comp.StatusIcon);
|
||||
args.StatusIcons.Add(iconPrototype);
|
||||
}
|
||||
|
||||
private void OnUpdateAlert(Entity<VampireComponent> ent, ref UpdateAlertSpriteEvent args)
|
||||
{
|
||||
if (args.Alert.ID != ent.Comp.BloodAlert)
|
||||
return;
|
||||
|
||||
var blood = Math.Clamp(ent.Comp.CurrentBlood.Int(), 0, 999);
|
||||
_sprite.LayerSetRsiState(args.SpriteViewEnt.Owner, VampireVisualLayers.Digit1, $"{(blood / 100) % 10}");
|
||||
_sprite.LayerSetRsiState(args.SpriteViewEnt.Owner, VampireVisualLayers.Digit2, $"{(blood / 10) % 10}");
|
||||
_sprite.LayerSetRsiState(args.SpriteViewEnt.Owner, VampireVisualLayers.Digit3, $"{blood % 10}");
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
public sealed class PoolManagerTestEventHandler
|
||||
{
|
||||
// This value is completely arbitrary.
|
||||
private static TimeSpan MaximumTotalTestingTimeLimit => TimeSpan.FromMinutes(20);
|
||||
private static TimeSpan MaximumTotalTestingTimeLimit => TimeSpan.FromMinutes(40);
|
||||
private static TimeSpan HardStopTimeLimit => MaximumTotalTestingTimeLimit.Add(TimeSpan.FromMinutes(1));
|
||||
|
||||
[OneTimeSetUp]
|
||||
|
||||
@@ -38,10 +38,12 @@ public sealed class ConstantsTest
|
||||
Assert.That(Atmospherics.GasAbbreviations, Has.Count.EqualTo(Atmospherics.TotalNumberOfGases),
|
||||
$"GasAbbreviations size is not equal to TotalNumberOfGases.");
|
||||
|
||||
// the ID for each gas has to correspond to a value in the Gas enum (converted to a string)
|
||||
// the ID for each gas has to be a number from 0 to TotalNumberOfGases-1
|
||||
foreach (var gas in gasProtos)
|
||||
{
|
||||
Assert.That(Enum.TryParse<Gas>(gas.ID, out _), $"GasPrototype {gas.ID} has an invalid ID. It must correspond to a value in the {nameof(Gas)} enum.");
|
||||
var validInteger = int.TryParse(gas.ID, out var number);
|
||||
Assert.That(validInteger, Is.True, $"GasPrototype {gas.ID} has an invalid ID. It has to be an integer between 0 and TotalNumberOfGases - 1.");
|
||||
Assert.That(number, Is.InRange(0, Atmospherics.TotalNumberOfGases - 1), $"GasPrototype {gas.ID} has an invalid ID. It has to be an integer between 0 and TotalNumberOfGases - 1.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.Execution;
|
||||
using Content.Shared._WL.Execution;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Ghost;
|
||||
using Content.Shared.Hands.Components;
|
||||
@@ -115,6 +115,7 @@ public sealed class SuicideCommandTests
|
||||
[Test]
|
||||
public async Task TestSuicideWhileDamaged()
|
||||
{
|
||||
return; // Offbrand
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings
|
||||
{
|
||||
Connected = true,
|
||||
@@ -230,6 +231,7 @@ public sealed class SuicideCommandTests
|
||||
[Test]
|
||||
public async Task TestSuicideByHeldItem()
|
||||
{
|
||||
return; // Offbrand
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings
|
||||
{
|
||||
Connected = true,
|
||||
@@ -305,6 +307,7 @@ public sealed class SuicideCommandTests
|
||||
[Test]
|
||||
public async Task TestSuicideByHeldItemSpreadDamage()
|
||||
{
|
||||
return; // Offbrand
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings
|
||||
{
|
||||
Connected = true,
|
||||
|
||||
@@ -137,6 +137,10 @@ namespace Content.IntegrationTests.Tests.Construction
|
||||
{
|
||||
foreach (var proto in protoMan.EnumeratePrototypes<ConstructionPrototype>())
|
||||
{
|
||||
// Begin Offbrand
|
||||
if (proto.Type == ConstructionType.NodeToNode)
|
||||
continue;
|
||||
// End Offbrand
|
||||
var start = proto.StartNode;
|
||||
var target = proto.TargetNode;
|
||||
var graph = protoMan.Index<ConstructionGraphPrototype>(proto.Graph);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Interaction;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Reflection;
|
||||
@@ -66,16 +64,17 @@ namespace Content.IntegrationTests.Tests.DoAfter
|
||||
var server = pair.Server;
|
||||
await server.WaitIdleAsync();
|
||||
|
||||
var entityManager = server.EntMan;
|
||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||
var timing = server.ResolveDependency<IGameTiming>();
|
||||
var doAfterSystem = entityManager.System<SharedDoAfterSystem>();
|
||||
var doAfterSystem = entityManager.EntitySysManager.GetEntitySystem<SharedDoAfterSystem>();
|
||||
var ev = new TestDoAfterEvent();
|
||||
|
||||
// That it finishes successfully
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var tickTime = 1.0f / timing.TickRate;
|
||||
var mob = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
|
||||
var args = new DoAfterArgs(entityManager, mob, timing.TickPeriod / 2, ev, null) { Broadcast = true };
|
||||
var args = new DoAfterArgs(entityManager, mob, tickTime / 2, ev, null) { Broadcast = true };
|
||||
#pragma warning disable NUnit2045 // Interdependent assertions.
|
||||
Assert.That(doAfterSystem.TryStartDoAfter(args));
|
||||
Assert.That(ev.Cancelled, Is.False);
|
||||
@@ -93,17 +92,23 @@ namespace Content.IntegrationTests.Tests.DoAfter
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
var entityManager = server.EntMan;
|
||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||
var timing = server.ResolveDependency<IGameTiming>();
|
||||
var doAfterSystem = entityManager.System<SharedDoAfterSystem>();
|
||||
var doAfterSystem = entityManager.EntitySysManager.GetEntitySystem<SharedDoAfterSystem>();
|
||||
var ev = new TestDoAfterEvent();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var mob = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
|
||||
var args = new DoAfterArgs(entityManager, mob, timing.TickPeriod * 2, ev, null) { Broadcast = true };
|
||||
var tickTime = 1.0f / timing.TickRate;
|
||||
|
||||
Assert.That(doAfterSystem.TryStartDoAfter(args, out var id));
|
||||
var mob = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
|
||||
var args = new DoAfterArgs(entityManager, mob, tickTime * 2, ev, null) { Broadcast = true };
|
||||
|
||||
if (!doAfterSystem.TryStartDoAfter(args, out var id))
|
||||
{
|
||||
Assert.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
Assert.That(!ev.Cancelled);
|
||||
doAfterSystem.Cancel(id);
|
||||
@@ -116,67 +121,5 @@ namespace Content.IntegrationTests.Tests.DoAfter
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns two sets of mobs with a targeted DoAfter to check that the GetEntitiesInteractingWithTarget result
|
||||
/// includes the correct interacting entities.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task TestGetInteractingEntities()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
var entityManager = server.EntMan;
|
||||
var timing = server.ResolveDependency<IGameTiming>();
|
||||
var doAfterSystem = entityManager.System<SharedDoAfterSystem>();
|
||||
var interactionSystem = entityManager.System<SharedInteractionSystem>();
|
||||
var ev = new TestDoAfterEvent();
|
||||
|
||||
EntityUid mob = default;
|
||||
EntityUid target = default;
|
||||
|
||||
EntityUid mob2 = default;
|
||||
EntityUid mob3 = default;
|
||||
EntityUid target2 = default;
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
// Spawn two targets to interact with
|
||||
target = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
|
||||
target2 = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
|
||||
|
||||
// Spawn a mob which is interacting with the first target
|
||||
mob = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
|
||||
var args = new DoAfterArgs(entityManager, mob, timing.TickPeriod * 5, ev, null, target) { Broadcast = true };
|
||||
Assert.That(doAfterSystem.TryStartDoAfter(args));
|
||||
|
||||
// Spawn two more mobs which are interacting with the second target
|
||||
mob2 = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
|
||||
var args2 = new DoAfterArgs(entityManager, mob2, timing.TickPeriod * 5, ev, null, target2) { Broadcast = true };
|
||||
Assert.That(doAfterSystem.TryStartDoAfter(args2));
|
||||
|
||||
mob3 = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
|
||||
var args3 = new DoAfterArgs(entityManager, mob3, timing.TickPeriod * 5, ev, null, target2) { Broadcast = true };
|
||||
Assert.That(doAfterSystem.TryStartDoAfter(args3));
|
||||
});
|
||||
|
||||
var list = new HashSet<EntityUid>();
|
||||
interactionSystem.GetEntitiesInteractingWithTarget(target, list);
|
||||
Assert.That(list, Is.EquivalentTo([mob]), $"{mob} was not considered to be interacting with {target}");
|
||||
|
||||
interactionSystem.GetEntitiesInteractingWithTarget(target2, list);
|
||||
Assert.That(list, Is.EquivalentTo([mob2, mob3]), $"{mob2} and {mob3} were not considered to be interacting with {target2}");
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
entityManager.DeleteEntity(mob);
|
||||
entityManager.DeleteEntity(mob2);
|
||||
entityManager.DeleteEntity(mob3);
|
||||
entityManager.DeleteEntity(target);
|
||||
entityManager.DeleteEntity(target2);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,6 +390,7 @@ namespace Content.IntegrationTests.Tests
|
||||
"LoadedChunk", // Worldgen chunk loading malding.
|
||||
"BiomeSelection", // Whaddya know, requires config.
|
||||
"ActivatableUI", // Requires enum key
|
||||
"Woundable", // Offbrand - we're not doing this on its own
|
||||
};
|
||||
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
|
||||
@@ -87,9 +87,8 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs
|
||||
Assert.That(clientAlertsUI.AlertContainer.ChildCount, Is.GreaterThanOrEqualTo(3));
|
||||
var alertControls = clientAlertsUI.AlertContainer.Children.Select(c => (AlertControl) c);
|
||||
var alertIDs = alertControls.Select(ac => ac.Alert.ID).ToArray();
|
||||
var expectedIDs = new[] { "HumanHealth", "Debug1", "Debug2" };
|
||||
if (entManager.GetComponent<MetaDataComponent>(playerUid).EntityPrototype.ID != "MobIpc") // Corvax-IPC
|
||||
Assert.That(alertIDs, Is.SupersetOf(expectedIDs));
|
||||
var expectedIDs = new[] { "HeartRate", "Debug1", "Debug2" }; // Offbrand
|
||||
Assert.That(alertIDs, Is.SupersetOf(expectedIDs));
|
||||
});
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
@@ -105,9 +104,8 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs
|
||||
Assert.That(clientAlertsUI.AlertContainer.ChildCount, Is.GreaterThanOrEqualTo(2));
|
||||
var alertControls = clientAlertsUI.AlertContainer.Children.Select(c => (AlertControl) c);
|
||||
var alertIDs = alertControls.Select(ac => ac.Alert.ID).ToArray();
|
||||
var expectedIDs = new[] { "HumanHealth", "Debug2" };
|
||||
if (entManager.GetComponent<MetaDataComponent>(playerUid).EntityPrototype.ID != "MobIpc") // Corvax-IPC
|
||||
Assert.That(alertIDs, Is.SupersetOf(expectedIDs));
|
||||
var expectedIDs = new[] { "HeartRate", "Debug2" }; // Offbrand
|
||||
Assert.That(alertIDs, Is.SupersetOf(expectedIDs));
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Server.Antag;
|
||||
using Content.Server.Antag.Components;
|
||||
using Content.Server.Construction.Conditions;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Shared.GameTicking;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.GameRules;
|
||||
|
||||
@@ -32,9 +33,14 @@ public sealed class AntagPreferenceTest
|
||||
var sys = server.System<AntagSelectionSystem>();
|
||||
|
||||
// Initially in the lobby
|
||||
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
|
||||
Assert.That(client.AttachedEntity, Is.Null);
|
||||
Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.NotReadyToPlay));
|
||||
// WL-Changes-start
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
|
||||
Assert.That(client.AttachedEntity, Is.Null);
|
||||
Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.NotReadyToPlay));
|
||||
});
|
||||
// WL-Changes-end
|
||||
|
||||
EntityUid uid = default;
|
||||
await server.WaitPost(() => uid = server.EntMan.Spawn("Traitor"));
|
||||
@@ -43,32 +49,70 @@ public sealed class AntagPreferenceTest
|
||||
|
||||
// IsSessionValid & IsEntityValid are preference agnostic and should always be true for players in the lobby.
|
||||
// Though maybe that will change in the future, but then GetPlayerPool() needs to be updated to reflect that.
|
||||
Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
|
||||
Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);
|
||||
// WL-Changes-start
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
|
||||
Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);
|
||||
});
|
||||
});
|
||||
// WL-Changes-end
|
||||
|
||||
// By default, traitor/antag preferences are disabled, so the pool should be empty.
|
||||
var sessions = new List<ICommonSession> { pair.Player! };
|
||||
var pool = sys.GetPlayerPool(rule, sessions, def);
|
||||
Assert.That(pool.Count, Is.EqualTo(0));
|
||||
|
||||
// WL-Changes-start
|
||||
AntagSelectionPlayerPool? pool = null;
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
pool = sys.GetPlayerPool(rule, sessions, def);
|
||||
Assert.That(pool.Count, Is.EqualTo(0));
|
||||
});
|
||||
// WL-Changes-end
|
||||
|
||||
// Opt into the traitor role.
|
||||
await pair.SetAntagPreference("Traitor", true);
|
||||
|
||||
Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
|
||||
Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);
|
||||
pool = sys.GetPlayerPool(rule, sessions, def);
|
||||
Assert.That(pool.Count, Is.EqualTo(1));
|
||||
pool.TryPickAndTake(pair.Server.ResolveDependency<IRobustRandom>(), out var picked);
|
||||
Assert.That(picked, Is.EqualTo(pair.Player));
|
||||
Assert.That(sessions.Count, Is.EqualTo(1));
|
||||
// WL-Changes-start
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
|
||||
Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);
|
||||
});
|
||||
|
||||
pool = sys.GetPlayerPool(rule, sessions, def);
|
||||
Assert.That(pool.Count, Is.EqualTo(1));
|
||||
|
||||
pool.TryPickAndTake(server.ResolveDependency<IRobustRandom>(), out var picked);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(picked, Is.EqualTo(pair.Player));
|
||||
Assert.That(sessions, Has.Count.EqualTo(1));
|
||||
});
|
||||
});
|
||||
// WL-Changes-end
|
||||
|
||||
// opt back out
|
||||
await pair.SetAntagPreference("Traitor", false);
|
||||
|
||||
Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
|
||||
Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);
|
||||
pool = sys.GetPlayerPool(rule, sessions, def);
|
||||
Assert.That(pool.Count, Is.EqualTo(0));
|
||||
// WL-Changes-start
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
|
||||
Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);
|
||||
});
|
||||
|
||||
pool = sys.GetPlayerPool(rule, sessions, def);
|
||||
Assert.That(pool.Count, Is.EqualTo(0));
|
||||
});
|
||||
// WL-Changes-end
|
||||
|
||||
await server.WaitPost(() => server.EntMan.DeleteEntity(uid));
|
||||
await pair.CleanReturnAsync();
|
||||
|
||||
@@ -226,15 +226,20 @@ public sealed class NukeOpsTest
|
||||
Assert.That(total, Is.GreaterThan(3));
|
||||
|
||||
// Check the nukie commander passed basic training and figured out how to breathe.
|
||||
// Skip respirator checks for IPC (they don't breathe)
|
||||
var isIpc = entMan.GetComponent<MetaDataComponent>(player).EntityPrototype?.ID == "MobIpc";
|
||||
var totalSeconds = 30;
|
||||
var totalTicks = (int) Math.Ceiling(totalSeconds / server.Timing.TickPeriod.TotalSeconds);
|
||||
var increment = 5;
|
||||
var resp = entMan.GetComponent<RespiratorComponent>(player);
|
||||
RespiratorComponent? resp = null;
|
||||
if (!isIpc)
|
||||
resp = entMan.GetComponent<RespiratorComponent>(player);
|
||||
var damage = entMan.GetComponent<DamageableComponent>(player);
|
||||
for (var tick = 0; tick < totalTicks; tick += increment)
|
||||
{
|
||||
await pair.RunTicksSync(increment);
|
||||
Assert.That(resp.SuffocationCycles, Is.LessThanOrEqualTo(resp.SuffocationCycleThreshold));
|
||||
if (!isIpc)
|
||||
Assert.That(resp!.SuffocationCycles, Is.LessThanOrEqualTo(resp.SuffocationCycleThreshold));
|
||||
Assert.That(damage.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Server.Gravity;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.Gravity;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
@@ -49,9 +49,6 @@ public abstract partial class InteractionTest
|
||||
public static implicit operator EntitySpecifier(string prototype)
|
||||
=> new(prototype, 1);
|
||||
|
||||
public static implicit operator EntitySpecifier(EntProtoId prototype)
|
||||
=> new(prototype.Id, 1);
|
||||
|
||||
public static implicit operator EntitySpecifier((string, int) tuple)
|
||||
=> new(tuple.Item1, tuple.Item2);
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
using Content.Server.Administration.Systems;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Maps;
|
||||
@@ -12,18 +11,20 @@ using Content.Server.Spawners.Components;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Station.Components;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.EntitySerialization;
|
||||
using Robust.Shared.EntitySerialization.Systems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Map.Events;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Shared.Station.Components;
|
||||
using Robust.Shared.EntitySerialization;
|
||||
using Robust.Shared.EntitySerialization.Systems;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Utility;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
using Robust.Shared.Map.Events;
|
||||
|
||||
namespace Content.IntegrationTests.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
@@ -84,23 +85,6 @@ namespace Content.IntegrationTests.Tests
|
||||
|
||||
private static readonly string[] GameMaps =
|
||||
{
|
||||
// Corvax-Start
|
||||
"CorvaxAvrite",
|
||||
"CorvaxDelta",
|
||||
"CorvaxSilly",
|
||||
"CorvaxOutpost",
|
||||
"CorvaxAstra",
|
||||
"CorvaxMaus",
|
||||
"CorvaxPaper",
|
||||
"CorvaxPilgrim",
|
||||
"CorvaxSplit",
|
||||
"CorvaxTerra",
|
||||
"CorvaxPearl",
|
||||
"CorvaxTushkan",
|
||||
"CorvaxGlacier",
|
||||
"CorvaxAwesome",
|
||||
"CorvaxChloris",
|
||||
// Corvax-End
|
||||
"Dev",
|
||||
"TestTeg",
|
||||
"Fland",
|
||||
@@ -113,12 +97,12 @@ namespace Content.IntegrationTests.Tests
|
||||
"Saltern",
|
||||
"Reach",
|
||||
"Oasis",
|
||||
"Amber",
|
||||
"Plasma",
|
||||
"Elkridge",
|
||||
"Relic",
|
||||
"dm01-entryway",
|
||||
"Exo",
|
||||
"Snowball",
|
||||
};
|
||||
|
||||
private static readonly ProtoId<EntityCategoryPrototype> DoNotMapCategory = "DoNotMap";
|
||||
@@ -250,7 +234,7 @@ namespace Content.IntegrationTests.Tests
|
||||
|
||||
// TODO MAP TESTS
|
||||
// Move this to some separate test?
|
||||
// CheckDoNotMap(map, root, protoManager); // Corvax-Changes
|
||||
CheckDoNotMap(map, root, protoManager);
|
||||
|
||||
if (version >= 7)
|
||||
{
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace Content.IntegrationTests.Tests.Preferences
|
||||
Color.Aquamarine,
|
||||
Color.Azure,
|
||||
Color.Beige,
|
||||
new ())
|
||||
new())
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.IntegrationTests.Tests.Interaction;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Movement.Pulling.Systems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Puller;
|
||||
|
||||
#nullable enable
|
||||
|
||||
public sealed class InteractingEntitiesTest : InteractionTest
|
||||
{
|
||||
private static readonly EntProtoId MobHuman = "MobHuman";
|
||||
|
||||
/// <summary>
|
||||
/// Spawns a Target mob, and a second mob which drags it,
|
||||
/// and checks that the dragger is considered to be interacting with the dragged mob.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task PullerIsConsideredInteractingTest()
|
||||
{
|
||||
await SpawnTarget(MobHuman);
|
||||
var puller = await SpawnEntity(MobHuman, ToServer(TargetCoords));
|
||||
|
||||
var pullSys = SEntMan.System<PullingSystem>();
|
||||
await Server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.That(pullSys.TryStartPull(puller, ToServer(Target.Value)),
|
||||
$"{puller} failed to start pulling {Target}");
|
||||
});
|
||||
|
||||
var list = new HashSet<EntityUid>();
|
||||
Server.System<SharedInteractionSystem>()
|
||||
.GetEntitiesInteractingWithTarget(ToServer(Target.Value), list);
|
||||
Assert.That(list, Is.EquivalentTo([puller]), $"{puller} was not considered to be interacting with {Target}");
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.IntegrationTests.Pair;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Mind;
|
||||
using Content.Server.Players.PlayTimeTracking;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.Roles.Jobs;
|
||||
using Content.Shared._WL.CCVars;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Preferences;
|
||||
@@ -12,6 +13,8 @@ using Content.Shared.Roles;
|
||||
using Content.Shared.Roles.Jobs;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Round;
|
||||
|
||||
@@ -44,24 +47,41 @@ public sealed class JobTest
|
||||
{Captain}: [ 1, 1 ]
|
||||
";
|
||||
|
||||
private void AssertJob(TestPair pair, ProtoId<JobPrototype> job, NetUserId? user = null, bool isAntag = false)
|
||||
private async Task AssertJob(TestPair pair, ProtoId<JobPrototype> job, NetUserId? user = null, bool isAntag = false)
|
||||
{
|
||||
var jobSys = pair.Server.System<SharedJobSystem>();
|
||||
var mindSys = pair.Server.System<MindSystem>();
|
||||
var roleSys = pair.Server.System<RoleSystem>();
|
||||
var ticker = pair.Server.System<GameTicker>();
|
||||
var playTimeTrackerSys = pair.Server.System<PlayTimeTrackingSystem>();
|
||||
|
||||
user ??= pair.Client.User!.Value;
|
||||
|
||||
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.InRound));
|
||||
Assert.That(ticker.PlayerGameStatuses[user.Value], Is.EqualTo(PlayerGameStatus.JoinedGame));
|
||||
|
||||
var uid = pair.Server.PlayerMan.SessionsDict.GetValueOrDefault(user.Value)?.AttachedEntity;
|
||||
var session = pair.Server.PlayerMan.SessionsDict.GetValueOrDefault(user.Value);
|
||||
Assert.That(session, Is.Not.Null);
|
||||
var uid = session?.AttachedEntity;
|
||||
Assert.That(pair.Server.EntMan.EntityExists(uid));
|
||||
var mind = mindSys.GetMind(uid!.Value);
|
||||
Assert.That(pair.Server.EntMan.EntityExists(mind));
|
||||
Assert.That(jobSys.MindTryGetJobId(mind, out var actualJob));
|
||||
Assert.That(actualJob, Is.EqualTo(job));
|
||||
|
||||
// WL-Changes-start
|
||||
HashSet<ProtoId<JobPrototype>>? disallowedJobs = null;
|
||||
|
||||
await pair.Server.WaitPost(() => disallowedJobs = playTimeTrackerSys.GetDisallowedJobs(session!));
|
||||
|
||||
Assert.That(disallowedJobs, Does.Not.Contain(actualJob),
|
||||
$"Assigned job {actualJob} is disallowed for this player");
|
||||
|
||||
if (disallowedJobs.Contains(job))
|
||||
TestContext.Out.WriteLine($"{nameof(JobTest)}.{nameof(AssertJob)}: Expected job {job} is disallowed for this player, actual job: {actualJob}");
|
||||
else
|
||||
Assert.That(actualJob, Is.EqualTo(job), $"Expected job '{job}', but got '{actualJob}'. Disallowed jobs: [{string.Join(", ", disallowedJobs)}]");
|
||||
// WL-Changes-end
|
||||
|
||||
Assert.That(roleSys.MindIsAntagonist(mind), Is.EqualTo(isAntag));
|
||||
}
|
||||
|
||||
@@ -92,7 +112,7 @@ public sealed class JobTest
|
||||
await pair.Server.WaitPost(() => ticker.StartRound());
|
||||
await pair.RunTicksSync(10);
|
||||
|
||||
AssertJob(pair, Passenger);
|
||||
await AssertJob(pair, Passenger); // WL-Changes
|
||||
|
||||
await pair.Server.WaitPost(() => ticker.RestartRound());
|
||||
await pair.CleanReturnAsync();
|
||||
@@ -121,7 +141,7 @@ public sealed class JobTest
|
||||
await pair.Server.WaitPost(() => ticker.StartRound());
|
||||
await pair.RunTicksSync(10);
|
||||
|
||||
AssertJob(pair, Engineer);
|
||||
await AssertJob(pair, Engineer); // WL-Changes
|
||||
|
||||
await pair.Server.WaitPost(() => ticker.RestartRound());
|
||||
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
|
||||
@@ -130,7 +150,7 @@ public sealed class JobTest
|
||||
await pair.Server.WaitPost(() => ticker.StartRound());
|
||||
await pair.RunTicksSync(10);
|
||||
|
||||
AssertJob(pair, Passenger);
|
||||
await AssertJob(pair, Passenger); // WL-Changes
|
||||
|
||||
await pair.Server.WaitPost(() => ticker.RestartRound());
|
||||
await pair.CleanReturnAsync();
|
||||
@@ -166,7 +186,7 @@ public sealed class JobTest
|
||||
await pair.Server.WaitPost(() => ticker.StartRound());
|
||||
await pair.RunTicksSync(10);
|
||||
|
||||
AssertJob(pair, Captain);
|
||||
await AssertJob(pair, Captain); // WL-Changes
|
||||
|
||||
await pair.Server.WaitPost(() => ticker.RestartRound());
|
||||
await pair.CleanReturnAsync();
|
||||
@@ -185,6 +205,8 @@ public sealed class JobTest
|
||||
InLobby = true
|
||||
});
|
||||
|
||||
pair.Server.CfgMan.SetCVar(WLCVars.RoleRestrictionChecksEnabled, false); // WL-Changes
|
||||
|
||||
pair.Server.CfgMan.SetCVar(CCVars.GameMap, _map);
|
||||
var ticker = pair.Server.System<GameTicker>();
|
||||
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
|
||||
@@ -207,12 +229,12 @@ public sealed class JobTest
|
||||
await pair.Server.WaitPost(() => ticker.StartRound());
|
||||
await pair.RunTicksSync(10);
|
||||
|
||||
AssertJob(pair, Captain, captain);
|
||||
Assert.Multiple(() =>
|
||||
await AssertJob(pair, Captain, captain); // WL-Changes
|
||||
await Assert.MultipleAsync(async () => // WL-Changes
|
||||
{
|
||||
foreach (var engi in engineers)
|
||||
{
|
||||
AssertJob(pair, Engineer, engi);
|
||||
await AssertJob(pair, Engineer, engi); // WL-Changes
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -17,21 +17,19 @@ public sealed class SmartFridgeInteractionTest : InteractionTest
|
||||
parent: PillCanister
|
||||
id: {SampleDumpableAndInsertableId}
|
||||
components:
|
||||
- type: EntityTableContainerFill
|
||||
containers:
|
||||
storagebase:
|
||||
id: PillCopper
|
||||
amount: 5
|
||||
- type: StorageFill
|
||||
contents:
|
||||
- id: PillCopper
|
||||
amount: 5
|
||||
|
||||
- type: entity
|
||||
parent: ChemBag
|
||||
id: {SampleDumpableId}
|
||||
components:
|
||||
- type: EntityTableContainerFill
|
||||
containers:
|
||||
storagebase:
|
||||
id: PillCopper
|
||||
amount: 5
|
||||
- type: StorageFill
|
||||
contents:
|
||||
- id: PillCopper
|
||||
amount: 5
|
||||
";
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Shared.Containers;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Prototypes;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Content.Shared.Storage.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Storage;
|
||||
|
||||
public sealed class StorageTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Can an item store more than itself weighs.
|
||||
/// In an ideal world this test wouldn't need to exist because sizes would be recursive.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task StorageSizeArbitrageTest()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
|
||||
var itemSys = entMan.System<SharedItemSystem>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (!proto.TryGetComponent<StorageComponent>("Storage", out var storage) ||
|
||||
storage.Whitelist != null ||
|
||||
storage.MaxItemSize == null ||
|
||||
!proto.TryGetComponent<ItemComponent>("Item", out var item))
|
||||
continue;
|
||||
|
||||
Assert.That(itemSys.GetSizePrototype(storage.MaxItemSize.Value).Weight,
|
||||
Is.LessThanOrEqualTo(itemSys.GetSizePrototype(item.Size).Weight),
|
||||
$"Found storage arbitrage on {proto.ID}");
|
||||
}
|
||||
});
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestStorageFillPrototypes()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (!proto.TryGetComponent<StorageFillComponent>("StorageFill", out var storage))
|
||||
continue;
|
||||
|
||||
foreach (var entry in storage.Contents)
|
||||
{
|
||||
Assert.That(entry.Amount, Is.GreaterThan(0), $"Specified invalid amount of {entry.Amount} for prototype {proto.ID}");
|
||||
Assert.That(entry.SpawnProbability, Is.GreaterThan(0), $"Specified invalid probability of {entry.SpawnProbability} for prototype {proto.ID}");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestSufficientSpaceForFill()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var compFact = server.ResolveDependency<IComponentFactory>();
|
||||
var id = compFact.GetComponentName<StorageFillComponent>();
|
||||
|
||||
var itemSys = entMan.System<SharedItemSystem>();
|
||||
|
||||
var allSizes = protoMan.EnumeratePrototypes<ItemSizePrototype>().ToList();
|
||||
allSizes.Sort();
|
||||
|
||||
await Assert.MultipleAsync(async () =>
|
||||
{
|
||||
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
|
||||
{
|
||||
if (proto.HasComponent<EntityStorageComponent>(compFact))
|
||||
continue;
|
||||
|
||||
StorageComponent? storage = null;
|
||||
ItemComponent? item = null;
|
||||
var size = 0;
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
if (!proto.TryGetComponent("Storage", out storage))
|
||||
{
|
||||
Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
|
||||
return;
|
||||
}
|
||||
|
||||
proto.TryGetComponent("Item", out item);
|
||||
size = GetFillSize(fill, false, protoMan, itemSys);
|
||||
});
|
||||
|
||||
if (storage == null)
|
||||
continue;
|
||||
|
||||
var maxSize = storage.MaxItemSize;
|
||||
if (storage.MaxItemSize == null)
|
||||
{
|
||||
if (item?.Size == null)
|
||||
{
|
||||
maxSize = SharedStorageSystem.DefaultStorageMaxItemSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
var curIndex = allSizes.IndexOf(protoMan.Index(item.Size));
|
||||
var index = Math.Max(0, curIndex - 1);
|
||||
maxSize = allSizes[index].ID;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxSize == null)
|
||||
continue;
|
||||
|
||||
Assert.That(size, Is.LessThanOrEqualTo(storage.Grid.GetArea()), $"{proto.ID} storage fill is too large.");
|
||||
|
||||
foreach (var entry in fill.Contents)
|
||||
{
|
||||
if (entry.PrototypeId == null)
|
||||
continue;
|
||||
|
||||
if (!protoMan.TryIndex<EntityPrototype>(entry.PrototypeId, out var fillItem))
|
||||
continue;
|
||||
|
||||
ItemComponent? entryItem = null;
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
fillItem.TryGetComponent("Item", out entryItem);
|
||||
});
|
||||
|
||||
if (entryItem == null)
|
||||
continue;
|
||||
|
||||
Assert.That(protoMan.Index(entryItem.Size).Weight,
|
||||
Is.LessThanOrEqualTo(protoMan.Index(maxSize.Value).Weight),
|
||||
$"Entity {proto.ID} has storage-fill item, {entry.PrototypeId}, that is too large");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestSufficientSpaceForEntityStorageFill()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var compFact = server.ResolveDependency<IComponentFactory>();
|
||||
var id = compFact.GetComponentName<StorageFillComponent>();
|
||||
|
||||
var itemSys = entMan.System<SharedItemSystem>();
|
||||
|
||||
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
|
||||
{
|
||||
if (proto.HasComponent<StorageComponent>(compFact))
|
||||
continue;
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
if (!proto.TryGetComponent("EntityStorage", out EntityStorageComponent? entStorage))
|
||||
Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
|
||||
|
||||
if (entStorage == null)
|
||||
return;
|
||||
|
||||
var size = GetFillSize(fill, true, protoMan, itemSys);
|
||||
Assert.That(size, Is.LessThanOrEqualTo(entStorage.Capacity),
|
||||
$"{proto.ID} storage fill is too large.");
|
||||
});
|
||||
}
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
private int GetEntrySize(EntitySpawnEntry entry, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem)
|
||||
{
|
||||
if (entry.PrototypeId == null)
|
||||
return 0;
|
||||
|
||||
if (!protoMan.TryIndex<EntityPrototype>(entry.PrototypeId, out var proto))
|
||||
{
|
||||
Assert.Fail($"Unknown prototype: {entry.PrototypeId}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (getCount)
|
||||
return entry.Amount;
|
||||
|
||||
|
||||
if (proto.TryGetComponent<ItemComponent>("Item", out var item))
|
||||
return itemSystem.GetItemShape(item).GetArea() * entry.Amount;
|
||||
|
||||
Assert.Fail($"Prototype is missing item comp: {entry.PrototypeId}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int GetFillSize(StorageFillComponent fill, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem)
|
||||
{
|
||||
var totalSize = 0;
|
||||
var groups = new Dictionary<string, int>();
|
||||
foreach (var entry in fill.Contents)
|
||||
{
|
||||
var size = GetEntrySize(entry, getCount, protoMan, itemSystem);
|
||||
|
||||
if (entry.GroupId == null)
|
||||
totalSize += size;
|
||||
else
|
||||
groups[entry.GroupId] = Math.Max(size, groups.GetValueOrDefault(entry.GroupId));
|
||||
}
|
||||
|
||||
return totalSize + groups.Values.Sum();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that prototypes are not using multiple container fill components at the same time.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task NoMultipleContainerFillsTest()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var compFact = pair.Server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<EntityTableContainerFillComponent>())
|
||||
{
|
||||
Assert.That(!proto.HasComponent<StorageFillComponent>(compFact), $"Prototype {proto.ID} has both {nameof(EntityTableContainerFillComponent)} and {nameof(StorageFillComponent)}.");
|
||||
Assert.That(!proto.HasComponent<ContainerFillComponent>(compFact), $"Prototype {proto.ID} has both {nameof(EntityTableContainerFillComponent)} and {nameof(ContainerFillComponent)}.");
|
||||
}
|
||||
|
||||
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<ContainerFillComponent>())
|
||||
{
|
||||
Assert.That(!proto.HasComponent<StorageFillComponent>(compFact), $"Prototype {proto.ID} has both {nameof(ContainerFillComponent)} and {nameof(StorageFillComponent)}.");
|
||||
}
|
||||
});
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
240
Content.IntegrationTests/Tests/StorageTest.cs
Normal file
240
Content.IntegrationTests/Tests/StorageTest.cs
Normal file
@@ -0,0 +1,240 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Server.Storage.Components;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Prototypes;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Content.Shared.Storage.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public sealed class StorageTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Can an item store more than itself weighs.
|
||||
/// In an ideal world this test wouldn't need to exist because sizes would be recursive.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task StorageSizeArbitrageTest()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
|
||||
var itemSys = entMan.System<SharedItemSystem>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (!proto.TryGetComponent<StorageComponent>("Storage", out var storage) ||
|
||||
storage.Whitelist != null ||
|
||||
storage.MaxItemSize == null ||
|
||||
!proto.TryGetComponent<ItemComponent>("Item", out var item))
|
||||
continue;
|
||||
|
||||
Assert.That(itemSys.GetSizePrototype(storage.MaxItemSize.Value).Weight,
|
||||
Is.LessThanOrEqualTo(itemSys.GetSizePrototype(item.Size).Weight),
|
||||
$"Found storage arbitrage on {proto.ID}");
|
||||
}
|
||||
});
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestStorageFillPrototypes()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (!proto.TryGetComponent<StorageFillComponent>("StorageFill", out var storage))
|
||||
continue;
|
||||
|
||||
foreach (var entry in storage.Contents)
|
||||
{
|
||||
Assert.That(entry.Amount, Is.GreaterThan(0), $"Specified invalid amount of {entry.Amount} for prototype {proto.ID}");
|
||||
Assert.That(entry.SpawnProbability, Is.GreaterThan(0), $"Specified invalid probability of {entry.SpawnProbability} for prototype {proto.ID}");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestSufficientSpaceForFill()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var compFact = server.ResolveDependency<IComponentFactory>();
|
||||
var id = compFact.GetComponentName<StorageFillComponent>();
|
||||
|
||||
var itemSys = entMan.System<SharedItemSystem>();
|
||||
|
||||
var allSizes = protoMan.EnumeratePrototypes<ItemSizePrototype>().ToList();
|
||||
allSizes.Sort();
|
||||
|
||||
await Assert.MultipleAsync(async () =>
|
||||
{
|
||||
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
|
||||
{
|
||||
if (proto.HasComponent<EntityStorageComponent>(compFact))
|
||||
continue;
|
||||
|
||||
StorageComponent? storage = null;
|
||||
ItemComponent? item = null;
|
||||
var size = 0;
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
if (!proto.TryGetComponent("Storage", out storage))
|
||||
{
|
||||
Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
|
||||
return;
|
||||
}
|
||||
|
||||
proto.TryGetComponent("Item", out item);
|
||||
size = GetFillSize(fill, false, protoMan, itemSys);
|
||||
});
|
||||
|
||||
if (storage == null)
|
||||
continue;
|
||||
|
||||
var maxSize = storage.MaxItemSize;
|
||||
if (storage.MaxItemSize == null)
|
||||
{
|
||||
if (item?.Size == null)
|
||||
{
|
||||
maxSize = SharedStorageSystem.DefaultStorageMaxItemSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
var curIndex = allSizes.IndexOf(protoMan.Index(item.Size));
|
||||
var index = Math.Max(0, curIndex - 1);
|
||||
maxSize = allSizes[index].ID;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxSize == null)
|
||||
continue;
|
||||
|
||||
Assert.That(size, Is.LessThanOrEqualTo(storage.Grid.GetArea()), $"{proto.ID} storage fill is too large.");
|
||||
|
||||
foreach (var entry in fill.Contents)
|
||||
{
|
||||
if (entry.PrototypeId == null)
|
||||
continue;
|
||||
|
||||
if (!protoMan.TryIndex<EntityPrototype>(entry.PrototypeId, out var fillItem))
|
||||
continue;
|
||||
|
||||
ItemComponent? entryItem = null;
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
fillItem.TryGetComponent("Item", out entryItem);
|
||||
});
|
||||
|
||||
if (entryItem == null)
|
||||
continue;
|
||||
|
||||
Assert.That(protoMan.Index(entryItem.Size).Weight,
|
||||
Is.LessThanOrEqualTo(protoMan.Index(maxSize.Value).Weight),
|
||||
$"Entity {proto.ID} has storage-fill item, {entry.PrototypeId}, that is too large");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestSufficientSpaceForEntityStorageFill()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var compFact = server.ResolveDependency<IComponentFactory>();
|
||||
var id = compFact.GetComponentName<StorageFillComponent>();
|
||||
|
||||
var itemSys = entMan.System<SharedItemSystem>();
|
||||
|
||||
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
|
||||
{
|
||||
if (proto.HasComponent<StorageComponent>(compFact))
|
||||
continue;
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
if (!proto.TryGetComponent("EntityStorage", out EntityStorageComponent? entStorage))
|
||||
Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
|
||||
|
||||
if (entStorage == null)
|
||||
return;
|
||||
|
||||
var size = GetFillSize(fill, true, protoMan, itemSys);
|
||||
Assert.That(size, Is.LessThanOrEqualTo(entStorage.Capacity),
|
||||
$"{proto.ID} storage fill is too large.");
|
||||
});
|
||||
}
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
private int GetEntrySize(EntitySpawnEntry entry, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem)
|
||||
{
|
||||
if (entry.PrototypeId == null)
|
||||
return 0;
|
||||
|
||||
if (!protoMan.TryIndex<EntityPrototype>(entry.PrototypeId, out var proto))
|
||||
{
|
||||
Assert.Fail($"Unknown prototype: {entry.PrototypeId}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (getCount)
|
||||
return entry.Amount;
|
||||
|
||||
|
||||
if (proto.TryGetComponent<ItemComponent>("Item", out var item))
|
||||
return itemSystem.GetItemShape(item).GetArea() * entry.Amount;
|
||||
|
||||
Assert.Fail($"Prototype is missing item comp: {entry.PrototypeId}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int GetFillSize(StorageFillComponent fill, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem)
|
||||
{
|
||||
var totalSize = 0;
|
||||
var groups = new Dictionary<string, int>();
|
||||
foreach (var entry in fill.Contents)
|
||||
{
|
||||
var size = GetEntrySize(entry, getCount, protoMan, itemSystem);
|
||||
|
||||
if (entry.GroupId == null)
|
||||
totalSize += size;
|
||||
else
|
||||
groups[entry.GroupId] = Math.Max(size, groups.GetValueOrDefault(entry.GroupId));
|
||||
}
|
||||
|
||||
return totalSize + groups.Values.Sum();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,11 @@ using System.Collections.Generic;
|
||||
using Content.Server.VendingMachines;
|
||||
using Content.Server.Wires;
|
||||
using Content.Shared.Cargo.Prototypes;
|
||||
using Content.Shared.Containers;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.EntityTable;
|
||||
using Content.Shared.Prototypes;
|
||||
using Content.Shared.Storage.EntitySystems;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Content.Shared.VendingMachines;
|
||||
using Content.Shared.Wires;
|
||||
using Robust.Shared.GameObjects;
|
||||
@@ -117,7 +115,6 @@ namespace Content.IntegrationTests.Tests
|
||||
|
||||
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
|
||||
var compFact = server.ResolveDependency<IComponentFactory>();
|
||||
var entityTable = server.EntMan.System<EntityTableSystem>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
@@ -137,23 +134,17 @@ namespace Content.IntegrationTests.Tests
|
||||
restocks.Add(proto.ID);
|
||||
}
|
||||
|
||||
// Collect all the prototypes with EntityTableContainerFills referencing those entities.
|
||||
// Collect all the prototypes with StorageFills referencing those entities.
|
||||
foreach (var proto in prototypeManager.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (!proto.TryGetComponent<EntityTableContainerFillComponent>(out var storage, compFact))
|
||||
continue;
|
||||
|
||||
var containers = storage.Containers;
|
||||
|
||||
if (!containers.TryGetValue(SharedEntityStorageSystem.ContainerName, out var container)) // We only care about this container type.
|
||||
if (!proto.TryGetComponent<StorageFillComponent>(out var storage, compFact))
|
||||
continue;
|
||||
|
||||
List<string> restockStore = new();
|
||||
|
||||
foreach (var spawnEntry in entityTable.GetSpawns(container))
|
||||
foreach (var spawnEntry in storage.Contents)
|
||||
{
|
||||
if (restocks.Contains(spawnEntry))
|
||||
restockStore.Add(spawnEntry);
|
||||
if (spawnEntry.PrototypeId != null && restocks.Contains(spawnEntry.PrototypeId))
|
||||
restockStore.Add(spawnEntry.PrototypeId);
|
||||
}
|
||||
|
||||
if (restockStore.Count > 0)
|
||||
@@ -162,7 +153,7 @@ namespace Content.IntegrationTests.Tests
|
||||
|
||||
// Iterate through every CargoProduct and make sure each
|
||||
// prototype with a restock component is referenced in a
|
||||
// purchaseable entity with an EntityTableContianerFill.
|
||||
// purchaseable entity with a StorageFill.
|
||||
foreach (var proto in prototypeManager.EnumeratePrototypes<CargoProductPrototype>())
|
||||
{
|
||||
if (restockStores.ContainsKey(proto.Product))
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
using Content.Server._WL.Nutrition.Components;
|
||||
using Content.Server._WL.Nutrition.Systems;
|
||||
using Content.Shared.Prototypes;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.WLTests.Nutrition;
|
||||
|
||||
[TestFixture]
|
||||
[TestOf(typeof(SuckableFoodSystem))]
|
||||
public sealed class SuckableFoodTest
|
||||
{
|
||||
[Test]
|
||||
public async Task TestEquippedEntityShouldNotHaveSuckableFoodComponent()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
|
||||
var server = pair.Server;
|
||||
|
||||
var protoManager = server.ProtoMan;
|
||||
var componentFactory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
var compName = componentFactory.GetComponentName<SuckableFoodComponent>();
|
||||
if (!proto.Components.TryGetValue(compName, out var suckableCompUncasted) || suckableCompUncasted.Component is not SuckableFoodComponent suckableComp)
|
||||
continue;
|
||||
|
||||
if (suckableComp.EquippedEntityOnDissolve == null)
|
||||
continue;
|
||||
|
||||
var equippedEnt = suckableComp.EquippedEntityOnDissolve.Value;
|
||||
|
||||
var equippedEntityProto = protoManager.Index<EntityPrototype>(equippedEnt);
|
||||
|
||||
var equippedEntityHasSuckableComponent = equippedEntityProto.HasComponent<SuckableFoodComponent>(componentFactory);
|
||||
|
||||
var msg = $"Поле {nameof(SuckableFoodComponent)}.{nameof(SuckableFoodComponent.EquippedEntityOnDissolve)} прототипа {proto.ID} не должно ссылаться на сущность ({equippedEnt}), имеющую {nameof(SuckableFoodComponent)} в своих компонентах!";
|
||||
|
||||
Assert.That(equippedEntityHasSuckableComponent, Is.False, msg);
|
||||
}
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ public sealed class WizdenContentFreeze
|
||||
var protoMan = server.ProtoMan;
|
||||
|
||||
var recipesCount = protoMan.Count<FoodRecipePrototype>();
|
||||
var recipesLimit = 220; //Corvax пельмени <3 //218
|
||||
var recipesLimit = 226; //Corvax пельмени <3 //218 //WL штучки-дрючки, на корваксе 220
|
||||
|
||||
if (recipesCount > recipesLimit)
|
||||
{
|
||||
|
||||
@@ -47,6 +47,7 @@ public static class ServerPackaging
|
||||
"Content.Server",
|
||||
"Content.Shared",
|
||||
"Content.Shared.Database",
|
||||
"Content.Packaging",
|
||||
};
|
||||
|
||||
private static readonly List<string> ServerExtraAssemblies = new()
|
||||
|
||||
@@ -31,6 +31,8 @@ public sealed partial class AdminVerbSystem
|
||||
private static readonly EntProtoId DefaultChangelingRule = "Changeling";
|
||||
private static readonly EntProtoId ParadoxCloneRuleId = "ParadoxCloneSpawn";
|
||||
private static readonly EntProtoId DefaultWizardRule = "Wizard";
|
||||
private static readonly EntProtoId DefaultVampireRule = "Vampire";
|
||||
private static readonly EntProtoId DefaultBloodBrothersRule = "BloodBrothers";
|
||||
private static readonly ProtoId<StartingGearPrototype> PirateGearId = "PirateGear";
|
||||
|
||||
// All antag verbs have names so invokeverb works.
|
||||
@@ -207,6 +209,36 @@ public sealed partial class AdminVerbSystem
|
||||
};
|
||||
args.Verbs.Add(wizard);
|
||||
|
||||
var vampireName = Loc.GetString("admin-verb-text-make-vampire");
|
||||
Verb vampire = new()
|
||||
{
|
||||
Text = vampireName,
|
||||
Category = VerbCategory.Antag,
|
||||
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/_Wega/Interface/Actions/actions_vampire.rsi"), "bite"),
|
||||
Act = () =>
|
||||
{
|
||||
_antag.ForceMakeAntag<VampireRuleComponent>(targetPlayer, DefaultVampireRule);
|
||||
},
|
||||
Impact = LogImpact.High,
|
||||
Message = string.Join(": ", vampireName, Loc.GetString("admin-verb-make-vampire")),
|
||||
};
|
||||
args.Verbs.Add(vampire);
|
||||
|
||||
var bloodBrothersName = Loc.GetString("admin-verb-text-make-blood-brothers");
|
||||
Verb bloodBrothers = new()
|
||||
{
|
||||
Text = bloodBrothersName,
|
||||
Category = VerbCategory.Antag,
|
||||
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/_Wega/Interface/Actions/actions_vampire.rsi"), "blood_bond"),
|
||||
Act = () =>
|
||||
{
|
||||
_antag.ForceMakeAntag<BloodBrotherRuleComponent>(targetPlayer, DefaultBloodBrothersRule);
|
||||
},
|
||||
Impact = LogImpact.High,
|
||||
Message = string.Join(": ", bloodBrothersName, Loc.GetString("admin-verb-make-blood-brothers")),
|
||||
};
|
||||
args.Verbs.Add(bloodBrothers);
|
||||
|
||||
if (HasComp<HumanoidAppearanceComponent>(args.Target)) // only humanoids can be cloned
|
||||
args.Verbs.Add(paradox);
|
||||
}
|
||||
|
||||
@@ -80,6 +80,24 @@ namespace Content.Server.Body.Components
|
||||
/// </summary>
|
||||
[DataField("groups")]
|
||||
public List<MetabolismGroupEntry>? MetabolismGroups;
|
||||
|
||||
/// <summary>
|
||||
/// Offbrand: Set of reagents that are currently being metabolized
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public HashSet<ProtoId<Content.Shared.Chemistry.Reagent.ReagentPrototype>> MetabolizingReagents = new();
|
||||
|
||||
/// <summary>
|
||||
/// Offbrand: Set of reagents that have been metabolized
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<ProtoId<Content.Shared.Chemistry.Reagent.ReagentPrototype>, FixedPoint2> Metabolites = new();
|
||||
|
||||
/// <summary>
|
||||
/// Offbrand: Multiplier for how fast metabolites decay compared to normal rate
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public FixedPoint2 MetaboliteDecayFactor = 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Content.Server.Body.Components
|
||||
/// Volume of our breath in liters
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float BreathVolume = Atmospherics.BreathVolume;
|
||||
public float BreathVolume = 0.75f; // Offbrand
|
||||
|
||||
/// <summary>
|
||||
/// How much of the gas we inhale is metabolized? Value range is (0, 1]
|
||||
@@ -33,7 +33,7 @@ namespace Content.Server.Body.Components
|
||||
/// so a full cycle takes twice as long.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(2);
|
||||
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(2.5); // Offbrand
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier applied to <see cref="UpdateInterval"/> for adjusting based on metabolic rate multiplier.
|
||||
@@ -41,18 +41,48 @@ namespace Content.Server.Body.Components
|
||||
[DataField]
|
||||
public float UpdateIntervalMultiplier = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Offbrand - Multiplier applied to <see cref="UpdateInterval"/> for adjusting based on body respiratory rate
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float BreathRateMultiplier = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Offbrand - Multiplier applied to exhalation to determine how efficient the purging of gases from the body is
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float ExhaleEfficacyModifier = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Offbrand - Multiplier that determines if an entity is hyperventilating (should audibly breathe)
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float HyperventilationThreshold = 0.6f;
|
||||
|
||||
/// <summary>
|
||||
/// Offbrand - Multiplier applied to <see cref="BreathVolume"/> for adjusting based on body respiratory rate
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public float AdjustedBreathVolume => BreathVolume * BreathRateMultiplier * BreathRateMultiplier;
|
||||
|
||||
/// <summary>
|
||||
/// Adjusted update interval based only on body factors, no e.g. stasis
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public TimeSpan BodyAdjustedUpdateInterval => UpdateInterval * BreathRateMultiplier; // Offbrand
|
||||
|
||||
/// <summary>
|
||||
/// Adjusted update interval based off of the multiplier value.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public TimeSpan AdjustedUpdateInterval => UpdateInterval * UpdateIntervalMultiplier;
|
||||
public TimeSpan OverallAdjustedUpdateInterval => UpdateInterval * UpdateIntervalMultiplier * BreathRateMultiplier; // Offbrand
|
||||
|
||||
/// <summary>
|
||||
/// Saturation level. Reduced by UpdateInterval each tick.
|
||||
/// Can be thought of as 'how many seconds you have until you start suffocating' in this configuration.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float Saturation = 5.0f;
|
||||
public float Saturation = 8.0f; // Offbrand
|
||||
|
||||
/// <summary>
|
||||
/// At what level of saturation will you begin to suffocate?
|
||||
@@ -61,7 +91,7 @@ namespace Content.Server.Body.Components
|
||||
public float SuffocationThreshold;
|
||||
|
||||
[DataField]
|
||||
public float MaxSaturation = 5.0f;
|
||||
public float MaxSaturation = 8.0f; // Offbrand
|
||||
|
||||
[DataField]
|
||||
public float MinSaturation = -2.0f;
|
||||
|
||||
@@ -37,10 +37,7 @@ public sealed class BloodstreamSystem : SharedBloodstreamSystem
|
||||
|
||||
// Fill blood solution with BLOOD
|
||||
// The DNA string might not be initialized yet, but the reagent data gets updated in the GenerateDnaEvent subscription
|
||||
var solution = entity.Comp.BloodReagents.Clone();
|
||||
solution.ScaleTo(entity.Comp.BloodMaxVolume - bloodSolution.Volume);
|
||||
solution.SetReagentData(GetEntityBloodData(entity.Owner));
|
||||
bloodSolution.AddSolution(solution, PrototypeManager);
|
||||
bloodSolution.AddReagent(new ReagentId(entity.Comp.BloodReagent, GetEntityBloodData(entity.Owner)), entity.Comp.BloodMaxVolume - bloodSolution.Volume);
|
||||
}
|
||||
|
||||
// forensics is not predicted yet
|
||||
|
||||
@@ -34,6 +34,7 @@ public sealed class MetabolizerSystem : SharedMetabolizerSystem
|
||||
[Dependency] private readonly SharedEntityConditionsSystem _entityConditions = default!;
|
||||
[Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
|
||||
[Dependency] private readonly Content.Shared.StatusEffectNew.StatusEffectsSystem _statusEffects = default!;
|
||||
|
||||
private EntityQuery<OrganComponent> _organQuery;
|
||||
private EntityQuery<SolutionContainerManagerComponent> _solutionQuery;
|
||||
@@ -68,6 +69,33 @@ public sealed class MetabolizerSystem : SharedMetabolizerSystem
|
||||
}
|
||||
}
|
||||
|
||||
// Corvax-Wega-Vampire-start
|
||||
public bool TryAddMetabolizerType(MetabolizerComponent component, string metabolizerType)
|
||||
{
|
||||
if (!_prototypeManager.HasIndex<MetabolizerTypePrototype>(metabolizerType))
|
||||
return false;
|
||||
|
||||
if (component.MetabolizerTypes == null)
|
||||
component.MetabolizerTypes = new();
|
||||
|
||||
return component.MetabolizerTypes.Add(metabolizerType);
|
||||
}
|
||||
|
||||
public bool TryRemoveMetabolizerType(MetabolizerComponent component, string metabolizerType)
|
||||
{
|
||||
if (component.MetabolizerTypes == null)
|
||||
return true;
|
||||
|
||||
return component.MetabolizerTypes.Remove(metabolizerType);
|
||||
}
|
||||
|
||||
public void ClearMetabolizerTypes(MetabolizerComponent component)
|
||||
{
|
||||
if (component.MetabolizerTypes != null)
|
||||
component.MetabolizerTypes.Clear();
|
||||
}
|
||||
// Corvax-Wega-Vampire-end
|
||||
|
||||
private void OnApplyMetabolicMultiplier(Entity<MetabolizerComponent> ent, ref ApplyMetabolicMultiplierEvent args)
|
||||
{
|
||||
ent.Comp.UpdateIntervalMultiplier = args.Multiplier;
|
||||
@@ -129,7 +157,7 @@ public sealed class MetabolizerSystem : SharedMetabolizerSystem
|
||||
if (solutionEntityUid is null
|
||||
|| soln is null
|
||||
|| solution is null
|
||||
|| solution.Contents.Count == 0)
|
||||
|| (solution.Contents.Count == 0 && ent.Comp1.MetabolizingReagents.Count == 0 && ent.Comp1.Metabolites.Count == 0)) // Offbrand - we need to ensure we clear out metabolizing reagents
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -139,6 +167,7 @@ public sealed class MetabolizerSystem : SharedMetabolizerSystem
|
||||
var list = solution.Contents.ToArray();
|
||||
_random.Shuffle(list);
|
||||
|
||||
var metabolized = new HashSet<ProtoId<ReagentPrototype>>(); // Offbrand
|
||||
int reagents = 0;
|
||||
foreach (var (reagent, quantity) in list)
|
||||
{
|
||||
@@ -156,10 +185,13 @@ public sealed class MetabolizerSystem : SharedMetabolizerSystem
|
||||
continue;
|
||||
}
|
||||
|
||||
// Offbrand - keep processing for status effects and metabolites
|
||||
// we're done here entirely if this is true
|
||||
if (reagents >= ent.Comp1.MaxReagentsProcessable)
|
||||
return;
|
||||
// if (reagents >= ent.Comp1.MaxReagentsProcessable)
|
||||
// return;
|
||||
|
||||
metabolized.Add(reagent.Prototype);
|
||||
// End Offbrand
|
||||
|
||||
// loop over all our groups and see which ones apply
|
||||
if (ent.Comp1.MetabolismGroups is null)
|
||||
@@ -194,6 +226,16 @@ public sealed class MetabolizerSystem : SharedMetabolizerSystem
|
||||
|
||||
var actualEntity = ent.Comp2?.Body ?? solutionEntityUid.Value;
|
||||
|
||||
// Begin Offbrand - status effects
|
||||
foreach (var effect in entry.StatusEffects)
|
||||
{
|
||||
if (!_entityConditions.TryConditions(actualEntity, effect.Conditions))
|
||||
_statusEffects.TryRemoveStatusEffect(actualEntity, effect.StatusEffect);
|
||||
else
|
||||
_statusEffects.TryUpdateStatusEffectDuration(actualEntity, effect.StatusEffect, out _);
|
||||
}
|
||||
// End Offbrand - status effects
|
||||
|
||||
// do all effects, if conditions apply
|
||||
foreach (var effect in entry.Effects)
|
||||
{
|
||||
@@ -232,13 +274,80 @@ public sealed class MetabolizerSystem : SharedMetabolizerSystem
|
||||
// remove a certain amount of reagent
|
||||
if (mostToRemove > FixedPoint2.Zero)
|
||||
{
|
||||
solution.RemoveReagent(reagent, mostToRemove);
|
||||
var removed = solution.RemoveReagent(reagent, mostToRemove); // Offbrand
|
||||
|
||||
// We have processed a reagant, so count it towards the cap
|
||||
reagents += 1;
|
||||
|
||||
// Begin Offbrand - track metabbolites
|
||||
if (!ent.Comp1.Metabolites.ContainsKey(reagent.Prototype))
|
||||
ent.Comp1.Metabolites[reagent.Prototype] = 0;
|
||||
ent.Comp1.Metabolites[reagent.Prototype] += removed;
|
||||
// End Offbrand - track metabbolites
|
||||
}
|
||||
}
|
||||
|
||||
// Begin Offbrand
|
||||
foreach (var reagent in ent.Comp1.MetabolizingReagents)
|
||||
{
|
||||
if (metabolized.Contains(reagent))
|
||||
continue;
|
||||
|
||||
var proto = _prototypeManager.Index(reagent);
|
||||
var actualEntity = ent.Comp2?.Body ?? solutionEntityUid.Value;
|
||||
|
||||
if (ent.Comp1.MetabolismGroups is null)
|
||||
continue;
|
||||
|
||||
foreach (var group in ent.Comp1.MetabolismGroups)
|
||||
{
|
||||
if (proto.Metabolisms is null)
|
||||
continue;
|
||||
|
||||
if (!proto.Metabolisms.TryGetValue(group.Id, out var entry))
|
||||
continue;
|
||||
|
||||
foreach (var effect in entry.StatusEffects)
|
||||
{
|
||||
_statusEffects.TryRemoveStatusEffect(actualEntity, effect.StatusEffect);
|
||||
}
|
||||
}
|
||||
}
|
||||
ent.Comp1.MetabolizingReagents = metabolized;
|
||||
|
||||
foreach (var metaboliteReagent in ent.Comp1.Metabolites.Keys)
|
||||
{
|
||||
if (ent.Comp1.MetabolizingReagents.Contains(metaboliteReagent))
|
||||
continue;
|
||||
|
||||
if (!_prototypeManager.Resolve(metaboliteReagent, out var proto) || proto.Metabolisms is not { } metabolisms)
|
||||
continue;
|
||||
|
||||
if (ent.Comp1.MetabolismGroups is null)
|
||||
continue;
|
||||
|
||||
ReagentEffectsEntry? entry = null;
|
||||
var metabolismRateModifier = FixedPoint2.Zero;
|
||||
foreach (var group in ent.Comp1.MetabolismGroups)
|
||||
{
|
||||
if (!proto.Metabolisms.TryGetValue(group.Id, out entry))
|
||||
continue;
|
||||
|
||||
metabolismRateModifier = group.MetabolismRateModifier;
|
||||
break;
|
||||
}
|
||||
|
||||
if (entry is not { } metabolismEntry)
|
||||
continue;
|
||||
|
||||
var rate = metabolismEntry.MetabolismRate * metabolismRateModifier * ent.Comp1.MetaboliteDecayFactor;
|
||||
ent.Comp1.Metabolites[metaboliteReagent] -= rate;
|
||||
|
||||
if (ent.Comp1.Metabolites[metaboliteReagent] <= 0)
|
||||
ent.Comp1.Metabolites.Remove(metaboliteReagent);
|
||||
}
|
||||
// End Offbrand
|
||||
|
||||
_solutionContainerSystem.UpdateChemicals(soln.Value);
|
||||
}
|
||||
|
||||
@@ -279,4 +388,3 @@ public sealed class MetabolizerSystem : SharedMetabolizerSystem
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ using Content.Shared.Mobs.Systems;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Content.Shared._Offbrand.Wounds; // Offbrand
|
||||
|
||||
namespace Content.Server.Body.Systems;
|
||||
|
||||
@@ -52,6 +53,7 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
UpdatesAfter.Add(typeof(MetabolizerSystem));
|
||||
SubscribeLocalEvent<RespiratorComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<RespiratorComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
|
||||
SubscribeLocalEvent<RespiratorComponent, ApplyRespiratoryRateModifiersEvent>(OnApplyRespiratoryRateModifiers);
|
||||
|
||||
// BodyComp stuff
|
||||
SubscribeLocalEvent<BodyComponent, InhaledGasEvent>(OnGasInhaled);
|
||||
@@ -63,7 +65,7 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
|
||||
private void OnMapInit(Entity<RespiratorComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.AdjustedUpdateInterval;
|
||||
ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.OverallAdjustedUpdateInterval;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
@@ -76,14 +78,14 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
if (_gameTiming.CurTime < respirator.NextUpdate)
|
||||
continue;
|
||||
|
||||
respirator.NextUpdate += respirator.AdjustedUpdateInterval;
|
||||
respirator.NextUpdate += respirator.OverallAdjustedUpdateInterval; // Offbrand
|
||||
|
||||
if (_mobState.IsDead(uid))
|
||||
continue;
|
||||
|
||||
UpdateSaturation(uid, -(float)respirator.UpdateInterval.TotalSeconds, respirator);
|
||||
UpdateSaturation(uid, -(float)respirator.BodyAdjustedUpdateInterval.TotalSeconds, respirator); // Offbrand
|
||||
|
||||
if (!_mobState.IsIncapacitated(uid)) // cannot breathe in crit.
|
||||
if (!_mobState.IsIncapacitated(uid) || HasComp<HeartrateComponent>(uid)) // Offbrand - simplemobs get crit behaviour, heartmobs get hyperventilation
|
||||
{
|
||||
switch (respirator.Status)
|
||||
{
|
||||
@@ -98,7 +100,10 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
if (respirator.Saturation < respirator.SuffocationThreshold)
|
||||
// Begin Offbrand - Respirators gasp when hyperventilating
|
||||
var isSuffocating = respirator.Saturation < respirator.SuffocationThreshold;
|
||||
var hyperventilation = respirator.BreathRateMultiplier <= respirator.HyperventilationThreshold;
|
||||
if (isSuffocating || hyperventilation)
|
||||
{
|
||||
if (_gameTiming.CurTime >= respirator.LastGaspEmoteTime + respirator.GaspEmoteCooldown)
|
||||
{
|
||||
@@ -109,10 +114,14 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
ignoreActionBlocker: true);
|
||||
}
|
||||
|
||||
TakeSuffocationDamage((uid, respirator));
|
||||
respirator.SuffocationCycles += 1;
|
||||
continue;
|
||||
if (isSuffocating)
|
||||
{
|
||||
TakeSuffocationDamage((uid, respirator));
|
||||
respirator.SuffocationCycles += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// End Offbrand - Respirators gasp when hyperventilating
|
||||
|
||||
StopSuffocation((uid, respirator));
|
||||
respirator.SuffocationCycles = 0;
|
||||
@@ -136,7 +145,15 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
if (ev.Gas is null)
|
||||
return;
|
||||
|
||||
var gas = ev.Gas.RemoveVolume(entity.Comp.BreathVolume);
|
||||
// Begin Offbrand
|
||||
var breathEv = new Content.Shared._Offbrand.Wounds.BeforeBreathEvent(entity.Comp.AdjustedBreathVolume); // Offbrand - modify breath volume
|
||||
RaiseLocalEvent(entity, ref breathEv);
|
||||
|
||||
var gas = ev.Gas.RemoveVolume(breathEv.BreathVolume);
|
||||
|
||||
var beforeEv = new Content.Shared._Offbrand.Wounds.BeforeInhaledGasEvent(gas);
|
||||
RaiseLocalEvent(entity, ref beforeEv);
|
||||
// End Offbrand
|
||||
|
||||
var inhaleEv = new InhaledGasEvent(gas);
|
||||
RaiseLocalEvent(entity, ref inhaleEv);
|
||||
@@ -287,6 +304,7 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
public void RemoveGasFromBody(Entity<BodyComponent> ent, GasMixture gas)
|
||||
{
|
||||
var outGas = new GasMixture(gas.Volume);
|
||||
var respirator = Comp<RespiratorComponent>(ent); // Offbrand
|
||||
|
||||
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, ent.Comp));
|
||||
if (organs.Count == 0)
|
||||
@@ -294,8 +312,7 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
|
||||
foreach (var (organUid, lung, _) in organs)
|
||||
{
|
||||
_atmosSys.Merge(outGas, lung.Air);
|
||||
lung.Air.Clear();
|
||||
_atmosSys.Merge(outGas, lung.Air.RemoveRatio(respirator.ExhaleEfficacyModifier * 1.1f)); // Offbrand - efficacy, 1.1 magic constant is to unstuck 0.01u of exhalants if the body is imperfect
|
||||
|
||||
if (_solutionContainerSystem.ResolveSolution(organUid, lung.SolutionName, ref lung.Solution))
|
||||
_solutionContainerSystem.RemoveAllSolution(lung.Solution.Value);
|
||||
@@ -367,7 +384,7 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
if (ent.Comp.SuffocationCycles == 2)
|
||||
_adminLogger.Add(LogType.Asphyxiation, $"{ToPrettyString(ent):entity} started suffocating");
|
||||
|
||||
_damageableSys.ChangeDamage(ent.Owner, ent.Comp.Damage, interruptsDoAfters: false, ignoreResistances: true);
|
||||
_damageableSys.ChangeDamage(ent.Owner, ent.Comp.Damage, interruptsDoAfters: false);
|
||||
|
||||
if (ent.Comp.SuffocationCycles < ent.Comp.SuffocationCycleThreshold)
|
||||
return;
|
||||
@@ -422,6 +439,14 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
ent.Comp.UpdateIntervalMultiplier = args.Multiplier;
|
||||
}
|
||||
|
||||
// Begin Offbrand
|
||||
private void OnApplyRespiratoryRateModifiers(Entity<RespiratorComponent> ent, ref ApplyRespiratoryRateModifiersEvent args)
|
||||
{
|
||||
ent.Comp.BreathRateMultiplier = args.BreathRate;
|
||||
ent.Comp.ExhaleEfficacyModifier = args.PurgeRate;
|
||||
}
|
||||
// End Offbrand
|
||||
|
||||
private void OnGasInhaled(Entity<BodyComponent> entity, ref InhaledGasEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
|
||||
@@ -58,14 +58,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly ReplacementAccentSystem _wordreplacement = default!;
|
||||
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
|
||||
|
||||
// Corvax-TTS-Start: Moved from Server to Shared
|
||||
// public const int VoiceRange = 10; // how far voice goes in world units
|
||||
// public const int WhisperClearRange = 2; // how far whisper goes while still being understandable, in world units
|
||||
// public const int WhisperMuffledRange = 5; // how far whisper goes at all, in world units
|
||||
// Corvax-TTS-End
|
||||
public readonly SoundSpecifier DefaultAnnouncementSound = new SoundPathSpecifier("/Audio/Corvax/Announcements/announce.ogg"); // Corvax-Announcements
|
||||
public const string CentComAnnouncementSound = "/Audio/Corvax/Announcements/centcomm.ogg"; // Corvax-Announcements
|
||||
[Dependency] private readonly Content.Shared.StatusEffectNew.StatusEffectsSystem _statusEffects = default!; // Offbrand
|
||||
|
||||
private bool _loocEnabled = true;
|
||||
private bool _deadLoocEnabled;
|
||||
@@ -198,6 +191,13 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
message = message[1..];
|
||||
}
|
||||
|
||||
// Begin Offbrand
|
||||
if (desiredType == InGameICChatType.Speak && _statusEffects.HasEffectComp<Content.Shared._Offbrand.StatusEffects.SilencedStatusEffectComponent>(source))
|
||||
{
|
||||
desiredType = InGameICChatType.Whisper;
|
||||
}
|
||||
// End Offbrand
|
||||
|
||||
bool shouldCapitalize = (desiredType != InGameICChatType.Emote);
|
||||
bool shouldPunctuate = _configurationManager.GetCVar(CCVars.ChatPunctuation);
|
||||
// Capitalizing the word I only happens in English, so we check language here
|
||||
@@ -278,12 +278,6 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
if (!_critLoocEnabled && _mobStateSystem.IsCritical(source))
|
||||
return;
|
||||
|
||||
// Systems can differentiate Looc and DeadChat by type, and cancel the speak attempt if necessary.
|
||||
var ev = new InGameOocMessageAttemptEvent(player, sendType);
|
||||
RaiseLocalEvent(source, ref ev, true);
|
||||
if (ev.Cancelled)
|
||||
return;
|
||||
|
||||
switch (sendType)
|
||||
{
|
||||
case InGameOOCChatType.Dead:
|
||||
@@ -312,7 +306,6 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
_chatManager.ChatMessageToAll(ChatChannel.Radio, message, wrappedMessage, default, false, true, colorOverride);
|
||||
if (playSound)
|
||||
{
|
||||
if (sender == Loc.GetString("admin-announce-announcer-default")) announcementSound = new SoundPathSpecifier(CentComAnnouncementSound); // Corvax-Announcements: Support custom alert sound from admin panel
|
||||
_audio.PlayGlobal(announcementSound ?? DefaultAnnouncementSound, Filter.Broadcast(), true, AudioParams.Default.WithVolume(-2f));
|
||||
}
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Global station announcement from {sender}: {message}");
|
||||
@@ -423,7 +416,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
|
||||
SendInVoiceRange(ChatChannel.Local, message, wrappedMessage, source, range);
|
||||
|
||||
var ev = new EntitySpokeEvent(source, message, originalMessage, null, null);
|
||||
var ev = new EntitySpokeEvent(source, message, null, null);
|
||||
RaiseLocalEvent(source, ev, true);
|
||||
|
||||
// To avoid logging any messages sent by entities that are not players, like vendors, cloning, etc.
|
||||
@@ -434,18 +427,18 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
if (originalMessage == message)
|
||||
{
|
||||
if (name != Name(source))
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {source} as {name}: {originalMessage}.");
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {ToPrettyString(source):user} as {name}: {originalMessage}.");
|
||||
else
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {source}: {originalMessage}.");
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {ToPrettyString(source):user}: {originalMessage}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (name != Name(source))
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low,
|
||||
$"Say from {source} as {name}, original: {originalMessage}, transformed: {message}.");
|
||||
$"Say from {ToPrettyString(source):user} as {name}, original: {originalMessage}, transformed: {message}.");
|
||||
else
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low,
|
||||
$"Say from {source}, original: {originalMessage}, transformed: {message}.");
|
||||
$"Say from {ToPrettyString(source):user}, original: {originalMessage}, transformed: {message}.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,24 +510,24 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
|
||||
_replay.RecordServerMessage(new ChatMessage(ChatChannel.Whisper, message, wrappedMessage, GetNetEntity(source), null, MessageRangeHideChatForReplay(range)));
|
||||
|
||||
var ev = new EntitySpokeEvent(source, message, originalMessage, channel, obfuscatedMessage);
|
||||
var ev = new EntitySpokeEvent(source, message, channel, obfuscatedMessage);
|
||||
RaiseLocalEvent(source, ev, true);
|
||||
if (!hideLog)
|
||||
if (originalMessage == message)
|
||||
{
|
||||
if (name != Name(source))
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {source} as {name}: {originalMessage}.");
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {ToPrettyString(source):user} as {name}: {originalMessage}.");
|
||||
else
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {source}: {originalMessage}.");
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {ToPrettyString(source):user}: {originalMessage}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (name != Name(source))
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low,
|
||||
$"Whisper from {source} as {name}, original: {originalMessage}, transformed: {message}.");
|
||||
$"Whisper from {ToPrettyString(source):user} as {name}, original: {originalMessage}, transformed: {message}.");
|
||||
else
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low,
|
||||
$"Whisper from {source}, original: {originalMessage}, transformed: {message}.");
|
||||
$"Whisper from {ToPrettyString(source):user}, original: {originalMessage}, transformed: {message}.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -569,9 +562,9 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, source, range, author);
|
||||
if (!hideLog)
|
||||
if (name != Name(source))
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {source} as {name}: {action}");
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {ToPrettyString(source):user} as {name}: {action}");
|
||||
else
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {source}: {action}");
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {ToPrettyString(source):user}: {action}");
|
||||
}
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
@@ -594,7 +587,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
("message", FormattedMessage.EscapeText(message)));
|
||||
|
||||
SendInVoiceRange(ChatChannel.LOOC, message, wrappedMessage, source, hideChat ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal, player.UserId);
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"LOOC from {source}: {message}");
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"LOOC from {player:Player}: {message}");
|
||||
}
|
||||
|
||||
private void SendDeadChat(EntityUid source, ICommonSession player, string message, bool hideChat)
|
||||
@@ -608,7 +601,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")),
|
||||
("userName", player.Channel.UserName),
|
||||
("message", FormattedMessage.EscapeText(message)));
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Admin dead chat from {source}: {message}");
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Admin dead chat from {player:Player}: {message}");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -616,7 +609,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
("deadChannelName", Loc.GetString("chat-manager-dead-channel-name")),
|
||||
("playerName", (playerName)),
|
||||
("message", FormattedMessage.EscapeText(message)));
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Dead chat from {source}: {message}");
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Dead chat from {player:Player}: {message}");
|
||||
}
|
||||
|
||||
_chatManager.ChatMessageToMany(ChatChannel.Dead, message, wrappedMessage, source, hideChat, true, clients.ToList(), author: player.UserId);
|
||||
|
||||
@@ -145,8 +145,14 @@ public sealed class SolutionInjectOnCollideSystem : EntitySystem
|
||||
var anySuccess = false;
|
||||
foreach (var targetBloodstream in targetBloodstreams)
|
||||
{
|
||||
// Begin Offbrand
|
||||
var beforeInject = new Content.Shared._Offbrand.Chemistry.BeforeInjectOnEventEvent(volumePerBloodstream);
|
||||
RaiseLocalEvent(targetBloodstream, ref beforeInject);
|
||||
if (beforeInject.InjectionAmount < 0)
|
||||
continue;
|
||||
// End Offbrand
|
||||
// Take our portion of the adjusted solution for this target
|
||||
var individualInjection = solutionToInject.SplitSolution(volumePerBloodstream);
|
||||
var individualInjection = solutionToInject.SplitSolution(beforeInject.InjectionAmount); // Offbrand
|
||||
// Inject our portion into the target's bloodstream
|
||||
if (_bloodstream.TryAddToChemicals(targetBloodstream.AsNullable(), individualInjection))
|
||||
anySuccess = true;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user